From d64e41150f6ba53a9361e8560d374bc69974ca84 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Sat, 20 Nov 2021 13:32:44 +0100 Subject: mail/envelope: add a special class for headers Handle multiple headers with ordering and case-insensitive operations. --- alot/buffers/envelope.py | 9 ++- alot/commands/envelope.py | 4 +- alot/mail/envelope.py | 136 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 106 insertions(+), 43 deletions(-) diff --git a/alot/buffers/envelope.py b/alot/buffers/envelope.py index 0bc7696f..871e88b5 100644 --- a/alot/buffers/envelope.py +++ b/alot/buffers/envelope.py @@ -119,13 +119,12 @@ class EnvelopeBuffer(Buffer): def rebuild(self): displayed_widgets = [] - hidden = settings.get('envelope_headers_blacklist') + hidden = set(map(str.lower, settings.get('envelope_headers_blacklist'))) # build lines lines = [] - for (k, vlist) in self.envelope.headers.items(): - if (k not in hidden) or self.all_headers: - for value in vlist: - lines.append((k, value)) + for k, v in self.envelope.headers.items(): + if (k.lower() not in hidden) or self.all_headers: + lines.append((k, v)) # sign/encrypt lines if self.envelope.sign: diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 4b485425..5dce6b44 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -369,9 +369,9 @@ class EditCommand(Command): headertext = '' for key in edit_headers: vlist = self.envelope.get_all(key) - if not vlist: + if len(vlist) == 0: # ensure editable headers are present in template - vlist = [''] + vlist.append('') else: # remove to be edited lines from envelope del self.envelope[key] diff --git a/alot/mail/envelope.py b/alot/mail/envelope.py index 55b3f7ff..655b5fe6 100644 --- a/alot/mail/envelope.py +++ b/alot/mail/envelope.py @@ -22,6 +22,94 @@ from ..errors import GPGProblem, GPGCode charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8') +class _EnvelopeHeaders: + """ + A dict-like container for envelope headers. + + Handles header-name case insensitivity and multiple headers. + """ + + _keys_lower = None + _keys = None + _vals = None + + def __init__(self, data = None): + self.clear() + + if data: + for key, val in data.items(): + self.add(key, val) + + def _key_to_idx(self, key): + key = key.lower() + if not key in self._keys_lower: + raise KeyError('No such header: ' + key) + return self._keys_lower.index(key) + + def __setitem__(self, key, val): + try: + idx = self._key_to_idx(key) + self._keys[idx] = key + self._vals[idx] = val + except KeyError: + self.add(key, val) + + def __getitem__(self, key): + idx = self._key_to_idx(key) + return self._vals[idx][0] + + def __delitem__(self, key): + idx = self._key_to_idx(key) + del self._keys_lower[idx] + del self._keys[idx] + del self._vals[idx] + + def __contains__(self, key): + return key.lower() in self._keys_lower + + def get(self, key, fallback = None): + return self[key] if key in self else fallback + + def get_all(self, key): + kl = key.lower() + ret = [] + + for idx in range(len(self._keys)): + if kl == self._keys_lower[idx]: + ret.append(self._vals[idx]) + + return ret + + def add(self, key, val): + self._keys_lower.append(key.lower()) + self._keys.append(key) + self._vals.append(val) + + def __iter__(self): + return iter(self._keys) + + def keys(self): + return self._keys + def values(self): + return self._vals + def items(self): + return zip(self.keys(), self.values()) + + def clear(self): + self._keys_lower = [] + self._keys = [] + self._vals = [] + + def copy(self): + ret = self.__class__() + for k, v in self.items(): + ret.add(k, v) + return ret + + def __str__(self): + return '\n'.join((k + ': ' + v\ + for k, v in self.items())) + class Envelope: """ a message that is not yet sent and still editable. @@ -77,9 +165,9 @@ class Envelope: logging.debug('PARSED TEMPLATE: %s', template) logging.debug('BODY: %s', self.body) self.body = bodytext or '' - # TODO: if this was as collections.defaultdict a number of methods - # could be simplified. - self.headers = headers or {} + + self.headers = _EnvelopeHeaders(headers) + self.attachments = list(attachments) if attachments is not None else [] self.sign = sign self.sign_key = sign_key @@ -97,12 +185,7 @@ class Envelope: return "Envelope (%s)\n%s" % (self.headers, self.body) def __setitem__(self, name, val): - """setter for header values. This allows adding header like so: - envelope['Subject'] = 'sm\xf8rebr\xf8d' - """ - if name not in self.headers: - self.headers[name] = [] - self.headers[name].append(val) + self.headers[name] = val if self.sent_time: self.modified_since_sent = True @@ -111,7 +194,7 @@ class Envelope: """getter for header values. :raises: KeyError if undefined """ - return self.headers[name][0] + return self.headers[name] def __delitem__(self, name): del self.headers[name] @@ -121,30 +204,12 @@ class Envelope: def __contains__(self, name): return name in self.headers - def get(self, key, fallback=None): - """secure getter for header values that allows specifying a `fallback` - return string (defaults to None). This returns the first matching value - and doesn't raise KeyErrors""" - if key in self.headers: - value = self.headers[key][0] - else: - value = fallback - return value - - def get_all(self, key, fallback=None): - """returns all header values for given key""" - if key in self.headers: - value = self.headers[key] - else: - value = fallback or [] - return value - + return self.headers.get(key, fallback) + def get_all(self, key): + return self.headers.get_all(key) def add(self, key, value): - """add header value""" - if key not in self.headers: - self.headers[key] = [] - self.headers[key].append(value) + self.headers.add(key, value) if self.sent_time: self.modified_since_sent = True @@ -302,9 +367,8 @@ class Envelope: headers['User-Agent'] = [uastring] # copy headers from envelope to mail - for k, vlist in headers.items(): - for v in vlist: - mail.add_header(k, v) + for k, v in headers.items(): + mail.add_header(k, v) # as we are using MIMEPart instead of EmailMessage, set the # MIME version manually @@ -329,7 +393,7 @@ class Envelope: self.modified_since_sent = True if reset: - self.headers = {} + self.headers.clear() headerEndPos = 0 if not only_body: -- cgit v1.2.3