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