Source code for pymap.threads
from __future__ import annotations
import re
from collections.abc import Iterable, Iterator, Sequence
from re import Match, Pattern
from typing import Final, Any
from .mime import MessageHeader
__all__ = ['ThreadKey']
[docs]
class ThreadKey(Iterable[str]):
"""Represents a hashable key used to link messages as members of the same
thread.
The thread key is composed of a single message ID from the ``Message-Id``,
``In-Reply-To``, or ``References`` headers, along with a normalized version
of the ``Subject`` header. If two messages share a single thread key, they
should be assigned the same
:attr:`~pymap.interfaces.message.MessageInterface.thread_id`.
Args:
msg_id: The message ID bytestring.
subject: The normalized subject bytestring.
"""
_pattern = re.compile(r'<[^>]*>')
_whitespace = re.compile(r'\s+')
_fwd_pattern = re.compile(r'\s*fwd\s*:\s*', re.I)
_re_pattern = re.compile(r'\s*re\s*:\s*', re.I)
_listtag_pattern = re.compile(r'\s*\[.*?\]\s*')
__slots__ = ['_pair', '__weakref__']
def __init__(self, msg_id: str, subject: str) -> None:
super().__init__()
self._pair: Final = (msg_id, subject)
def __eq__(self, other: Any) -> bool:
if isinstance(other, ThreadKey):
return self._pair == other._pair
return super().__eq__(other)
def __hash__(self) -> int:
return hash(self._pair)
def __iter__(self) -> Iterator[str]:
return iter(self._pair)
@classmethod
def _encode(cls, value: str) -> str:
return cls._whitespace.sub('', value)
@classmethod
def _first_match(cls, value: str, *patterns: Pattern[str]) \
-> Match[str] | None:
for pattern in patterns:
match = pattern.match(value)
if match is not None:
return match
return None
@classmethod
def _subject(cls, value: str) -> str:
match = cls._first_match(
value, cls._fwd_pattern, cls._re_pattern, cls._listtag_pattern)
if match is None:
return cls._whitespace.sub(' ', value.strip())
else:
return cls._subject(value[match.end(0):])
[docs]
@classmethod
def get_all(cls, header: MessageHeader) -> Sequence[ThreadKey]:
"""Return all the thread keys from the message headers.
Args:
header: The message header.
"""
ret: list[ThreadKey] = []
message_id = header.parsed.message_id
in_reply_to = header.parsed.in_reply_to
references = header.parsed.references
subject = header.parsed.subject
subject_key = cls._subject(str(subject)) if subject else ''
if message_id is not None:
match = cls._pattern.search(str(message_id))
if match is not None:
ret.append(cls(cls._encode(match.group(0)), subject_key))
if in_reply_to is not None:
for match in cls._pattern.finditer(str(in_reply_to)):
ret.append(cls(cls._encode(match.group(0)), subject_key))
if references is not None:
for match in cls._pattern.finditer(str(references)):
ret.append(cls(cls._encode(match.group(0)), subject_key))
return ret