imip-agent

imiptools/mail.py

1272:65e999dd88f0
2017-09-18 Paul Boddie Added a convenience method for loading objects. Added docstrings. client-editing-simplification
     1 #!/usr/bin/env python     2      3 """     4 Mail preparation support.     5      6 Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from imiptools.config import settings    23 from email.mime.message import MIMEMessage    24 from email.mime.multipart import MIMEMultipart    25 from email.mime.text import MIMEText    26 from smtplib import LMTP, SMTP    27     28 LOCAL_PREFIX = settings["LOCAL_PREFIX"]    29 MESSAGE_SENDER = settings["MESSAGE_SENDER"]    30 OUTGOING_PREFIX = settings["OUTGOING_PREFIX"]    31     32 # Fake gettext function for strings to be translated later.    33     34 _ = lambda s: s    35     36 MESSAGE_SUBJECT = _("Calendar system message")    37     38 PREAMBLE_TEXT = _("""\    39 This message contains several different parts, one of which will contain    40 calendar information that will only be understood by a suitable program.    41 """)    42     43 class Messenger:    44     45     "Sending of outgoing messages."    46     47     def __init__(self, lmtp_socket=None, local_smtp=False, sender=None, subject=None, preamble_text=None):    48     49         """    50         Deliver to a local mail system using LMTP if 'lmtp_socket' is provided    51         or if 'local_smtp' is given as a true value.    52         """    53     54         self.lmtp_socket = lmtp_socket    55         self.local_smtp = local_smtp    56         self.sender = sender or MESSAGE_SENDER    57         self.subject = subject    58         self.preamble_text = preamble_text    59     60         # The translation method is set by the client once locale information is    61         # known.    62     63         self.gettext = None    64     65     def local_delivery(self):    66     67         "Return whether local delivery is performed using this messenger."    68     69         return self.lmtp_socket is not None or self.local_smtp    70     71     def sendmail(self, recipients, data, sender=None, outgoing_bcc=None):    72     73         """    74         Send a mail to the given 'recipients' consisting of the given 'data',    75         using the given 'sender' identity if indicated, indicating an    76         'outgoing_bcc' identity if indicated.    77     78         The 'outgoing_bcc' argument is required when sending on behalf of a user    79         from the calendar@domain address, since this will not be detected as a    80         valid participant and handled using the outgoing transport.    81         """    82     83         if self.lmtp_socket:    84             smtp = LMTP(self.lmtp_socket)    85         else:    86             smtp = SMTP("localhost")    87     88         if outgoing_bcc:    89             recipients = list(recipients) + ["%s+%s" % (OUTGOING_PREFIX, outgoing_bcc)]    90         elif self.local_smtp:    91             recipients = [self.make_local(recipient) for recipient in recipients]    92     93         smtp.sendmail(sender or self.sender, recipients, data)    94         smtp.quit()    95     96     def make_local(self, recipient):    97     98         """    99         Make the 'recipient' an address for local delivery. For this to function   100         correctly, a virtual alias or equivalent must be defined for addresses   101         of the following form:   102    103         local+NAME@DOMAIN   104    105         Such aliases should direct delivery to the local recipient.   106         """   107    108         parts = recipient.split("+", 1)   109         return "%s+%s" % (LOCAL_PREFIX, parts[-1])   110    111     def make_outgoing_message(self, parts, recipients, sender=None, outgoing_bcc=None):   112    113         """   114         Make a message from the given 'parts' for the given 'recipients', using   115         the given 'sender' identity if indicated, indicating an 'outgoing_bcc'   116         identity if indicated.   117         """   118    119         message = self._make_summary_for_parts(parts)   120    121         message["From"] = sender or self.sender   122         for recipient in recipients:   123             message["To"] = recipient   124         if outgoing_bcc:   125             message["Bcc"] = "%s+%s" % (OUTGOING_PREFIX, outgoing_bcc)   126         message["Subject"] = self.subject or \   127             self.gettext and self.gettext(MESSAGE_SUBJECT) or MESSAGE_SUBJECT   128    129         return message   130    131     def make_summary_message(self, msg, parts):   132    133         """   134         Return a simple summary using details from 'msg' and the given 'parts'.   135         Information messages provided amongst the parts by the handlers will be   136         merged into the preamble so that mail programs will show them   137         immediately.   138         """   139    140         message = self._make_summary_for_parts(parts, True)   141         self._copy_headers(message, msg)   142         return message   143    144     def wrap_message(self, msg, parts):   145    146         """   147         Wrap 'msg' and provide the given 'parts' as the primary content.   148         Information messages provided amongst the parts by the handlers will be   149         merged into the preamble so that mail programs will show them   150         immediately.   151         """   152    153         message = self._make_container_for_parts(parts, True)   154         payload = message.get_payload()   155         payload.append(MIMEMessage(msg))   156         self._copy_headers(message, msg)   157         return message   158    159     def _make_summary_for_parts(self, parts, merge=False):   160    161         """   162         Return a simple summary for the given 'parts', merging information parts if   163         'merge' is specified and set to a true value.   164         """   165    166         if len(parts) == 1:   167             return parts[0]   168         else:   169             return self._make_container_for_parts(parts, merge)   170    171     def _make_container_for_parts(self, parts, merge=False):   172    173         """   174         Return a container for the given 'parts', merging information parts if   175         'merge' is specified and set to a true value.   176         """   177    178         # Merge calendar information if requested.   179    180         if merge:   181             info, parts = self._merge_calendar_info_parts(parts)   182         else:   183             info = []   184    185         # Insert a preamble message before any calendar information messages.   186    187         info.insert(0, self.preamble_text or   188                        self.gettext and self.gettext(PREAMBLE_TEXT) or PREAMBLE_TEXT)   189    190         message = MIMEMultipart("mixed", _subparts=parts)   191         message.preamble = "\n\n".join(info)   192         return message   193    194     def _merge_calendar_info_parts(self, parts):   195    196         """   197         Return a collection of plain text calendar information messages from   198         'parts', together with a collection of the remaining parts.   199         """   200    201         info = []   202         remaining = []   203    204         for part in parts:   205    206             # Attempt to acquire informational messages.   207    208             if part.get("X-IMIP-Agent") == "info":   209    210                 # Ignore the preamble of any multipart message and just   211                 # collect its parts.   212    213                 if part.is_multipart():   214                     i, r = self._merge_calendar_info_parts(part.get_payload())   215                     remaining += r   216    217                 # Obtain any single-part messages.   218    219                 else:   220                     info.append(part.get_payload(decode=True))   221    222             # Accumulate other parts regardless of their purpose.   223    224             else:   225                 remaining.append(part)   226    227         return info, remaining   228    229     def _copy_headers(self, message, msg):   230    231         "Copy to 'message' certain headers from 'msg'."   232    233         message["From"] = msg["From"]   234         message["To"] = msg["To"]   235         message["Subject"] = msg["Subject"]   236    237 # vim: tabstop=4 expandtab shiftwidth=4