# This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file import email.policy from email.utils import getaddresses, parseaddr from enum import Enum, auto import logging from . import headers as HDR from ..helper import formataddr from ..settings.const import settings ACT_REPLY = 'reply' ACT_BOUNCE = 'bounce' ACT_FWD = 'forward' def determine_account(headers, action = ACT_REPLY): """ Determine the local account to use for acting on a message with the given headers. :param headers: headers of the email to inspect :type headers: `db.message._MessageHeaders` :param action: intended use case: one of the ACT_* constants :type action: str :returns: 2-tuple of (from address, account) """ # get accounts my_accounts = settings.get_accounts() assert my_accounts, 'no accounts set!' # extract list of addresses to check for my address # X-Envelope-To and Envelope-To are used to store the recipient address # if not included in other fields # Process the headers in order of importance: if a mail was sent with # account X, with account Y in e.g. CC or delivered-to, make sure that # account X is the one selected and not account Y. for candidate_header in settings.get("reply_account_header_priority"): candidate_addresses = getaddresses(headers.get_all(candidate_header)) logging.debug('candidate addresses: %s', candidate_addresses) # pick the most important account that has an address in candidates # and use that account's realname and the address found here for account in my_accounts: for seen_name, seen_address in candidate_addresses: if account.matches_address(seen_address): if settings.get(action + '_force_realname'): realname = account.realname else: realname = seen_name if settings.get(action + '_force_address'): address = str(account.address) else: address = seen_address logging.debug('using realname: "%s"', realname) logging.debug('using address: %s', address) from_value = formataddr((realname, address)) return from_value, account # revert to default account if nothing found account = my_accounts[0] realname = account.realname address = account.address logging.debug('using realname: "%s"', realname) logging.debug('using address: %s', address) from_value = formataddr((realname, str(address))) return from_value, account class ReplyMode(Enum): AUTHOR = auto() GROUP = auto() LIST = auto() def _ensure_unique_address(recipients): """ clean up a list of name,address pairs so that no address appears multiple times. """ res = dict() for name, address in getaddresses(recipients): res[address] = name logging.debug(res) urecipients = [formataddr((n, a)) for a, n in res.items()] return sorted(urecipients) def _clear_my_address(my_account, value): """return recipient header without the addresses in my_account :param my_account: my account :type my_account: :class:`Account` :param value: a list of recipient or sender strings (with or without real names as taken from email headers) :type value: list(str) :returns: a new, potentially shortend list :rtype: list(str) """ new_value = [] for name, address in getaddresses(value): if not my_account.matches_address(address): new_value.append(formataddr((name, address))) return new_value def determine_recipients(message, account, mode): # set To sender = (message.headers.get(HDR.REPLY_TO) or message.headers.get(HDR.FROM) or '') sender_address = parseaddr(sender)[1] cc = [] # check if reply is to self sent message if account.matches_address(sender_address): recipients = message.headers.get_all(HDR.TO) emsg = 'Replying to own message, set recipients to: %s' \ % recipients logging.debug(emsg) else: recipients = [sender] if mode == ReplyMode.GROUP: # make sure that our own address is not included # if the message was self-sent, then our address is not included MFT = message.headers.get_all(HDR.MAIL_FOLLOWUP_TO) followupto = _clear_my_address(account, MFT) if followupto and settings.get('honor_followup_to'): logging.debug('honor followup to: %s', ', '.join(followupto)) recipients = followupto # since Mail-Followup-To was set, ignore the Cc header else: if sender != message.headers.get(HDR.FROM): recipients.append(message.headers.get(HDR.FROM)) # append To addresses if not replying to self sent message if not account.matches_address(sender_address): cleared = _clear_my_address(account, message.headers.get_all(HDR.TO)) recipients.extend(cleared) # copy cc for group-replies if HDR.CC in message.headers: cc = _clear_my_address(account, message.headers.get_all(HDR.CC)) to = ', '.join(_ensure_unique_address(recipients)) cc = ', '.join(cc) logging.debug('reply to: %s', to) if mode == ReplyMode.LIST: # To choose the target of the reply --list # Reply-To is standart reply target RFC 2822:, RFC 1036: 2.2.1 # X-BeenThere is needed by sourceforge ML also winehq # X-Mailing-List is also standart and is used by git-send-mail to = (message.headers.get(HDR.REPLY_TO) or message.headers.get(HDR.X_BEEN_THERE) or message.headers.get(HDR.X_MAILING_LIST)) # Some mail server (gmail) will not resend you own mail, so you # have to deal with the one in sent if to is None: to = message.headers[HDR.TO] logging.debug('mail list reply to: %s', to) return to, cc def subject(message): subject = message.headers.get(HDR.SUBJECT) or '' reply_subject_hook = settings.get_hook('reply_subject') if reply_subject_hook: subject = reply_subject_hook(subject) else: rsp = settings.get('reply_subject_prefix') if not subject.lower().startswith(('re:', rsp.lower())): subject = rsp + subject return subject def body_text(message, ui): name, address = message.get_author() timestamp = message.date qf = settings.get_hook('reply_prefix') if qf: mail = email.message_from_bytes(message.as_bytes(), policy = email.policy.SMTP) quotestring = qf(name, address, timestamp, message=mail, ui=ui, dbm=ui.dbman) else: quotestring = 'Quoting %s (%s)\n' % (name or address, timestamp) mailcontent = quotestring quotehook = settings.get_hook('text_quote') if quotehook: mailcontent += quotehook(message.get_body_text()) else: quote_prefix = settings.get('quote_prefix') for line in message.get_body_text().splitlines(): mailcontent += quote_prefix + line + '\n' return mailcontent def references(message): old_references = message.headers.get(HDR.REFERENCES) references = [] if old_references: references = old_references.split() # limit to 16 references, including the one we add del references[1:-14] references.append('<%s>' % message.id) return ' '.join(references) def mail_followup_to(recipients): if not settings.get('followup_to'): return None lists = settings.get('mailinglists') # check if any recipient address matches a known mailing list if any(addr in lists for n, addr in getaddresses(recipients)): followupto = ', '.join(recipients) logging.debug('mail followup to: %s', followupto) return followupto