[3356] | 1 | ############################################################################## |
---|
| 2 | # |
---|
| 3 | # Copyright (c) 2002 Zope Foundation and Contributors. |
---|
| 4 | # |
---|
| 5 | # This software is subject to the provisions of the Zope Public License, |
---|
| 6 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
---|
| 7 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
---|
| 8 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
---|
| 9 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
---|
| 10 | # FOR A PARTICULAR PURPOSE. |
---|
| 11 | # |
---|
| 12 | ############################################################################## |
---|
| 13 | """SMTP mail objects |
---|
| 14 | """ |
---|
| 15 | import logging |
---|
| 16 | from os.path import realpath |
---|
| 17 | import re |
---|
| 18 | from cStringIO import StringIO |
---|
| 19 | from copy import deepcopy |
---|
| 20 | from email.Header import Header |
---|
| 21 | from email.Charset import Charset |
---|
| 22 | from email import message_from_string |
---|
| 23 | from email.Message import Message |
---|
| 24 | from email import Encoders |
---|
| 25 | try: |
---|
| 26 | import email.utils as emailutils |
---|
| 27 | except ImportError: |
---|
| 28 | import email.Utils as emailutils |
---|
| 29 | import email.Charset |
---|
| 30 | # We import from a private module here because the email module |
---|
| 31 | # doesn't provide a good public address list parser |
---|
| 32 | import uu |
---|
| 33 | |
---|
| 34 | from threading import Lock |
---|
| 35 | import time |
---|
| 36 | |
---|
| 37 | from AccessControl.class_init import InitializeClass |
---|
| 38 | from AccessControl.SecurityInfo import ClassSecurityInfo |
---|
| 39 | from AccessControl.Permissions import change_configuration, view |
---|
| 40 | from AccessControl.Permissions import use_mailhost_services |
---|
| 41 | from Acquisition import Implicit |
---|
| 42 | from App.special_dtml import DTMLFile |
---|
| 43 | from DateTime.DateTime import DateTime |
---|
| 44 | from Persistence import Persistent |
---|
| 45 | from OFS.role import RoleManager |
---|
| 46 | from OFS.SimpleItem import Item |
---|
| 47 | |
---|
| 48 | from zope.interface import implements |
---|
| 49 | from zope.sendmail.mailer import SMTPMailer |
---|
| 50 | from zope.sendmail.maildir import Maildir |
---|
| 51 | from zope.sendmail.delivery import DirectMailDelivery, QueuedMailDelivery, \ |
---|
| 52 | QueueProcessorThread |
---|
| 53 | |
---|
| 54 | from interfaces import IMailHost |
---|
| 55 | from decorator import synchronized |
---|
| 56 | |
---|
| 57 | queue_threads = {} # maps MailHost path -> queue processor threada |
---|
| 58 | |
---|
| 59 | LOG = logging.getLogger('MailHost') |
---|
| 60 | |
---|
| 61 | # Encode utf-8 emails as Quoted Printable by default |
---|
| 62 | email.Charset.add_charset("utf-8", email.Charset.QP, email.Charset.QP, "utf-8") |
---|
| 63 | formataddr = emailutils.formataddr |
---|
| 64 | parseaddr = emailutils.parseaddr |
---|
| 65 | getaddresses = emailutils.getaddresses |
---|
| 66 | CHARSET_RE = re.compile('charset=[\'"]?([\w-]+)[\'"]?', re.IGNORECASE) |
---|
| 67 | |
---|
| 68 | |
---|
| 69 | class MailHostError(Exception): |
---|
| 70 | pass |
---|
| 71 | |
---|
| 72 | manage_addMailHostForm = DTMLFile('dtml/addMailHost_form', globals()) |
---|
| 73 | |
---|
| 74 | |
---|
| 75 | def manage_addMailHost(self, |
---|
| 76 | id, |
---|
| 77 | title='', |
---|
| 78 | smtp_host='localhost', |
---|
| 79 | localhost='localhost', |
---|
| 80 | smtp_port=25, |
---|
| 81 | timeout=1.0, |
---|
| 82 | REQUEST=None, |
---|
| 83 | ): |
---|
| 84 | """ Add a MailHost into the system. |
---|
| 85 | """ |
---|
| 86 | i = MailHost(id, title, smtp_host, smtp_port) |
---|
| 87 | self._setObject(id, i) |
---|
| 88 | |
---|
| 89 | if REQUEST is not None: |
---|
| 90 | REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main') |
---|
| 91 | |
---|
| 92 | add = manage_addMailHost |
---|
| 93 | |
---|
| 94 | |
---|
| 95 | class MailBase(Implicit, Item, RoleManager): |
---|
| 96 | """a mailhost...?""" |
---|
| 97 | |
---|
| 98 | implements(IMailHost) |
---|
| 99 | |
---|
| 100 | meta_type = 'Mail Host' |
---|
| 101 | manage = manage_main = DTMLFile('dtml/manageMailHost', globals()) |
---|
| 102 | manage_main._setName('manage_main') |
---|
| 103 | index_html = None |
---|
| 104 | security = ClassSecurityInfo() |
---|
| 105 | smtp_uid = '' # Class attributes for smooth upgrades |
---|
| 106 | smtp_pwd = '' |
---|
| 107 | smtp_queue = False |
---|
| 108 | smtp_queue_directory = '/tmp' |
---|
| 109 | force_tls = False |
---|
| 110 | lock = Lock() |
---|
| 111 | |
---|
| 112 | manage_options = ( |
---|
| 113 | ( |
---|
| 114 | {'icon': '', 'label': 'Edit', |
---|
| 115 | 'action': 'manage_main', |
---|
| 116 | 'help': ('MailHost', 'Mail-Host_Edit.stx')}, |
---|
| 117 | ) |
---|
| 118 | + RoleManager.manage_options |
---|
| 119 | + Item.manage_options |
---|
| 120 | ) |
---|
| 121 | |
---|
| 122 | def __init__(self, |
---|
| 123 | id='', |
---|
| 124 | title='', |
---|
| 125 | smtp_host='localhost', |
---|
| 126 | smtp_port=25, |
---|
| 127 | force_tls=False, |
---|
| 128 | smtp_uid='', |
---|
| 129 | smtp_pwd='', |
---|
| 130 | smtp_queue=False, |
---|
| 131 | smtp_queue_directory='/tmp', |
---|
| 132 | ): |
---|
| 133 | """Initialize a new MailHost instance. |
---|
| 134 | """ |
---|
| 135 | self.id = id |
---|
| 136 | self.title = title |
---|
| 137 | self.smtp_host = str(smtp_host) |
---|
| 138 | self.smtp_port = int(smtp_port) |
---|
| 139 | self.smtp_uid = smtp_uid |
---|
| 140 | self.smtp_pwd = smtp_pwd |
---|
| 141 | self.force_tls = force_tls |
---|
| 142 | self.smtp_queue = smtp_queue |
---|
| 143 | self.smtp_queue_directory = smtp_queue_directory |
---|
| 144 | |
---|
| 145 | |
---|
| 146 | # staying for now... (backwards compatibility) |
---|
| 147 | def _init(self, smtp_host, smtp_port): |
---|
| 148 | self.smtp_host = smtp_host |
---|
| 149 | self.smtp_port = smtp_port |
---|
| 150 | |
---|
| 151 | security.declareProtected(change_configuration, 'manage_makeChanges') |
---|
| 152 | def manage_makeChanges(self, |
---|
| 153 | title, |
---|
| 154 | smtp_host, |
---|
| 155 | smtp_port, |
---|
| 156 | smtp_uid='', |
---|
| 157 | smtp_pwd='', |
---|
| 158 | smtp_queue=False, |
---|
| 159 | smtp_queue_directory='/tmp', |
---|
| 160 | force_tls=False, |
---|
| 161 | REQUEST=None, |
---|
| 162 | ): |
---|
| 163 | """Make the changes. |
---|
| 164 | """ |
---|
| 165 | title = str(title) |
---|
| 166 | smtp_host = str(smtp_host) |
---|
| 167 | smtp_port = int(smtp_port) |
---|
| 168 | |
---|
| 169 | self.title = title |
---|
| 170 | self.smtp_host = smtp_host |
---|
| 171 | self.smtp_port = smtp_port |
---|
| 172 | self.smtp_uid = smtp_uid |
---|
| 173 | self.smtp_pwd = smtp_pwd |
---|
| 174 | self.force_tls = force_tls |
---|
| 175 | self.smtp_queue = smtp_queue |
---|
| 176 | self.smtp_queue_directory = smtp_queue_directory |
---|
| 177 | |
---|
| 178 | # restart queue processor thread |
---|
| 179 | if self.smtp_queue: |
---|
| 180 | self._stopQueueProcessorThread() |
---|
| 181 | self._startQueueProcessorThread() |
---|
| 182 | else: |
---|
| 183 | self._stopQueueProcessorThread() |
---|
| 184 | |
---|
| 185 | |
---|
| 186 | if REQUEST is not None: |
---|
| 187 | msg = 'MailHost %s updated' % self.id |
---|
| 188 | return self.manage_main(self, REQUEST, manage_tabs_message=msg) |
---|
| 189 | |
---|
| 190 | security.declareProtected(use_mailhost_services, 'sendTemplate') |
---|
| 191 | def sendTemplate(trueself, |
---|
| 192 | self, |
---|
| 193 | messageTemplate, |
---|
| 194 | statusTemplate=None, |
---|
| 195 | mto=None, |
---|
| 196 | mfrom=None, |
---|
| 197 | encode=None, |
---|
| 198 | REQUEST=None, |
---|
| 199 | immediate=False, |
---|
| 200 | charset=None, |
---|
| 201 | msg_type=None, |
---|
| 202 | ): |
---|
| 203 | """Render a mail template, then send it... |
---|
| 204 | """ |
---|
| 205 | mtemplate = getattr(self, messageTemplate) |
---|
| 206 | messageText = mtemplate(self, trueself.REQUEST) |
---|
| 207 | trueself.send(messageText, mto=mto, mfrom=mfrom, |
---|
| 208 | encode=encode, immediate=immediate, |
---|
| 209 | charset=charset, msg_type=msg_type) |
---|
| 210 | |
---|
| 211 | if not statusTemplate: |
---|
| 212 | return "SEND OK" |
---|
| 213 | try: |
---|
| 214 | stemplate = getattr(self, statusTemplate) |
---|
| 215 | return stemplate(self, trueself.REQUEST) |
---|
| 216 | except: |
---|
| 217 | return "SEND OK" |
---|
| 218 | |
---|
| 219 | security.declareProtected(use_mailhost_services, 'send') |
---|
| 220 | def send(self, |
---|
| 221 | messageText, |
---|
| 222 | mto=None, |
---|
| 223 | mfrom=None, |
---|
| 224 | subject=None, |
---|
| 225 | encode=None, |
---|
| 226 | immediate=False, |
---|
| 227 | charset=None, |
---|
| 228 | msg_type=None, |
---|
| 229 | ): |
---|
| 230 | messageText, mto, mfrom = _mungeHeaders(messageText, mto, mfrom, |
---|
| 231 | subject, charset, msg_type) |
---|
| 232 | # This encode step is mainly for BBB, encoding should be |
---|
| 233 | # automatic if charset is passed. The automated charset-based |
---|
| 234 | # encoding will be preferred if both encode and charset are |
---|
| 235 | # provided. |
---|
| 236 | messageText = _encode(messageText, encode) |
---|
| 237 | self._send(mfrom, mto, messageText, immediate) |
---|
| 238 | |
---|
| 239 | # This is here for backwards compatibility only. Possibly it could |
---|
| 240 | # be used to send messages at a scheduled future time, or via a mail queue? |
---|
| 241 | security.declareProtected(use_mailhost_services, 'scheduledSend') |
---|
| 242 | scheduledSend = send |
---|
| 243 | |
---|
| 244 | security.declareProtected(use_mailhost_services, 'simple_send') |
---|
| 245 | def simple_send(self, mto, mfrom, subject, body, immediate=False): |
---|
| 246 | body = "From: %s\nTo: %s\nSubject: %s\n\n%s" % ( |
---|
| 247 | mfrom, mto, subject, body) |
---|
| 248 | |
---|
| 249 | self._send(mfrom, mto, body, immediate) |
---|
| 250 | |
---|
| 251 | |
---|
| 252 | def _makeMailer(self): |
---|
| 253 | """ Create a SMTPMailer """ |
---|
| 254 | return SMTPMailer(hostname=self.smtp_host, |
---|
| 255 | port=int(self.smtp_port), |
---|
| 256 | username=self.smtp_uid or None, |
---|
| 257 | password=self.smtp_pwd or None, |
---|
| 258 | force_tls=self.force_tls) |
---|
| 259 | |
---|
| 260 | security.declarePrivate('_getThreadKey') |
---|
| 261 | def _getThreadKey(self): |
---|
| 262 | """ Return the key used to find our processor thread. |
---|
| 263 | """ |
---|
| 264 | return realpath(self.smtp_queue_directory) |
---|
| 265 | |
---|
| 266 | @synchronized(lock) |
---|
| 267 | def _stopQueueProcessorThread(self): |
---|
| 268 | """ Stop thread for processing the mail queue. |
---|
| 269 | """ |
---|
| 270 | key = self._getThreadKey() |
---|
| 271 | if key in queue_threads: |
---|
| 272 | thread = queue_threads[key] |
---|
| 273 | thread.stop() |
---|
| 274 | while thread.isAlive(): |
---|
| 275 | # wait until thread is really dead |
---|
| 276 | time.sleep(0.3) |
---|
| 277 | del queue_threads[key] |
---|
| 278 | LOG.info('Thread for %s stopped' % key) |
---|
| 279 | |
---|
| 280 | @synchronized(lock) |
---|
| 281 | def _startQueueProcessorThread(self): |
---|
| 282 | """ Start thread for processing the mail queue. |
---|
| 283 | """ |
---|
| 284 | key = self._getThreadKey() |
---|
| 285 | if key not in queue_threads: |
---|
| 286 | thread = QueueProcessorThread() |
---|
| 287 | thread.setMailer(self._makeMailer()) |
---|
| 288 | thread.setQueuePath(self.smtp_queue_directory) |
---|
| 289 | thread.start() |
---|
| 290 | queue_threads[key] = thread |
---|
| 291 | LOG.info('Thread for %s started' % key) |
---|
| 292 | |
---|
| 293 | security.declareProtected(view, 'queueLength') |
---|
| 294 | def queueLength(self): |
---|
| 295 | """ return length of mail queue """ |
---|
| 296 | |
---|
| 297 | try: |
---|
| 298 | maildir = Maildir(self.smtp_queue_directory) |
---|
| 299 | return len([item for item in maildir]) |
---|
| 300 | except ValueError: |
---|
| 301 | return 'n/a - %s is not a maildir - please verify your ' \ |
---|
| 302 | 'configuration' % self.smtp_queue_directory |
---|
| 303 | |
---|
| 304 | |
---|
| 305 | security.declareProtected(view, 'queueThreadAlive') |
---|
| 306 | def queueThreadAlive(self): |
---|
| 307 | """ return True/False is queue thread is working |
---|
| 308 | """ |
---|
| 309 | th = queue_threads.get(self._getThreadKey()) |
---|
| 310 | if th: |
---|
| 311 | return th.isAlive() |
---|
| 312 | return False |
---|
| 313 | |
---|
| 314 | security.declareProtected(change_configuration, |
---|
| 315 | 'manage_restartQueueThread') |
---|
| 316 | def manage_restartQueueThread(self, action='start', REQUEST=None): |
---|
| 317 | """ Restart the queue processor thread """ |
---|
| 318 | |
---|
| 319 | if action == 'stop': |
---|
| 320 | self._stopQueueProcessorThread() |
---|
| 321 | elif action == 'start': |
---|
| 322 | self._startQueueProcessorThread() |
---|
| 323 | else: |
---|
| 324 | raise ValueError('Unsupported action %s' % action) |
---|
| 325 | |
---|
| 326 | if REQUEST is not None: |
---|
| 327 | msg = 'Queue processor thread %s' % \ |
---|
| 328 | (action == 'stop' and 'stopped' or 'started') |
---|
| 329 | return self.manage_main(self, REQUEST, manage_tabs_message=msg) |
---|
| 330 | |
---|
| 331 | |
---|
| 332 | security.declarePrivate('_send') |
---|
| 333 | def _send(self, mfrom, mto, messageText, immediate=False): |
---|
| 334 | """ Send the message """ |
---|
| 335 | |
---|
| 336 | if immediate: |
---|
| 337 | self._makeMailer().send(mfrom, mto, messageText) |
---|
| 338 | else: |
---|
| 339 | if self.smtp_queue: |
---|
| 340 | # Start queue processor thread, if necessary |
---|
| 341 | self._startQueueProcessorThread() |
---|
| 342 | delivery = QueuedMailDelivery(self.smtp_queue_directory) |
---|
| 343 | else: |
---|
| 344 | delivery = DirectMailDelivery(self._makeMailer()) |
---|
| 345 | |
---|
| 346 | delivery.send(mfrom, mto, messageText) |
---|
| 347 | |
---|
| 348 | InitializeClass(MailBase) |
---|
| 349 | |
---|
| 350 | |
---|
| 351 | class MailHost(Persistent, MailBase): |
---|
| 352 | """persistent version""" |
---|
| 353 | |
---|
| 354 | |
---|
| 355 | def uu_encoder(msg): |
---|
| 356 | """For BBB only, don't send uuencoded emails""" |
---|
| 357 | orig = StringIO(msg.get_payload()) |
---|
| 358 | encdata = StringIO() |
---|
| 359 | uu.encode(orig, encdata) |
---|
| 360 | msg.set_payload(encdata.getvalue()) |
---|
| 361 | |
---|
| 362 | # All encodings supported by mimetools for BBB |
---|
| 363 | ENCODERS = { |
---|
| 364 | 'base64': Encoders.encode_base64, |
---|
| 365 | 'quoted-printable': Encoders.encode_quopri, |
---|
| 366 | '7bit': Encoders.encode_7or8bit, |
---|
| 367 | '8bit': Encoders.encode_7or8bit, |
---|
| 368 | 'x-uuencode': uu_encoder, |
---|
| 369 | 'uuencode': uu_encoder, |
---|
| 370 | 'x-uue': uu_encoder, |
---|
| 371 | 'uue': uu_encoder, |
---|
| 372 | } |
---|
| 373 | |
---|
| 374 | |
---|
| 375 | def _encode(body, encode=None): |
---|
| 376 | """Manually sets an encoding and encodes the message if not |
---|
| 377 | already encoded.""" |
---|
| 378 | if encode is None: |
---|
| 379 | return body |
---|
| 380 | mo = message_from_string(body) |
---|
| 381 | current_coding = mo['Content-Transfer-Encoding'] |
---|
| 382 | if current_coding == encode: |
---|
| 383 | # already encoded correctly, may have been automated |
---|
| 384 | return body |
---|
| 385 | if mo['Content-Transfer-Encoding'] not in ['7bit', None]: |
---|
| 386 | raise MailHostError('Message already encoded') |
---|
| 387 | if encode in ENCODERS: |
---|
| 388 | ENCODERS[encode](mo) |
---|
| 389 | if not mo['Content-Transfer-Encoding']: |
---|
| 390 | mo['Content-Transfer-Encoding'] = encode |
---|
| 391 | if not mo['Mime-Version']: |
---|
| 392 | mo['Mime-Version'] = '1.0' |
---|
| 393 | return mo.as_string() |
---|
| 394 | |
---|
| 395 | |
---|
| 396 | def _mungeHeaders(messageText, mto=None, mfrom=None, subject=None, |
---|
| 397 | charset=None, msg_type=None): |
---|
| 398 | """Sets missing message headers, and deletes Bcc. |
---|
| 399 | returns fixed message, fixed mto and fixed mfrom""" |
---|
| 400 | # If we have been given unicode fields, attempt to encode them |
---|
| 401 | if isinstance(messageText, unicode): |
---|
| 402 | messageText = _try_encode(messageText, charset) |
---|
| 403 | if isinstance(mto, unicode): |
---|
| 404 | mto = _try_encode(mto, charset) |
---|
| 405 | if isinstance(mfrom, unicode): |
---|
| 406 | mfrom = _try_encode(mfrom, charset) |
---|
| 407 | if isinstance(subject, unicode): |
---|
| 408 | subject = _try_encode(subject, charset) |
---|
| 409 | |
---|
| 410 | if isinstance(messageText, Message): |
---|
| 411 | # We already have a message, make a copy to operate on |
---|
| 412 | mo = deepcopy(messageText) |
---|
| 413 | else: |
---|
| 414 | # Otherwise parse the input message |
---|
| 415 | mo = message_from_string(messageText) |
---|
| 416 | |
---|
| 417 | if msg_type and not mo.get('Content-Type'): |
---|
| 418 | # we don't use get_content_type because that has a default |
---|
| 419 | # value of 'text/plain' |
---|
| 420 | mo.set_type(msg_type) |
---|
| 421 | |
---|
| 422 | charset = _set_recursive_charset(mo, charset=charset) |
---|
| 423 | |
---|
| 424 | # Parameters given will *always* override headers in the messageText. |
---|
| 425 | # This is so that you can't override or add to subscribers by adding |
---|
| 426 | # them to # the message text. |
---|
| 427 | if subject: |
---|
| 428 | # remove any existing header otherwise we get two |
---|
| 429 | del mo['Subject'] |
---|
| 430 | # Perhaps we should ignore errors here and pass 8bit strings |
---|
| 431 | # on encoding errors |
---|
| 432 | mo['Subject'] = Header(subject, charset, errors='replace') |
---|
| 433 | elif not mo.get('Subject'): |
---|
| 434 | mo['Subject'] = '[No Subject]' |
---|
| 435 | |
---|
| 436 | if mto: |
---|
| 437 | if isinstance(mto, basestring): |
---|
| 438 | mto = [formataddr(addr) for addr in getaddresses((mto, ))] |
---|
| 439 | if not mo.get('To'): |
---|
| 440 | mo['To'] = ', '.join(str(_encode_address_string(e, charset)) |
---|
| 441 | for e in mto) |
---|
| 442 | else: |
---|
| 443 | # If we don't have recipients, extract them from the message |
---|
| 444 | mto = [] |
---|
| 445 | for header in ('To', 'Cc', 'Bcc'): |
---|
| 446 | v = ','.join(mo.get_all(header) or []) |
---|
| 447 | if v: |
---|
| 448 | mto += [formataddr(addr) for addr in getaddresses((v, ))] |
---|
| 449 | if not mto: |
---|
| 450 | raise MailHostError("No message recipients designated") |
---|
| 451 | |
---|
| 452 | if mfrom: |
---|
| 453 | # XXX: do we really want to override an explicitly set From |
---|
| 454 | # header in the messageText |
---|
| 455 | del mo['From'] |
---|
| 456 | mo['From'] = _encode_address_string(mfrom, charset) |
---|
| 457 | else: |
---|
| 458 | if mo.get('From') is None: |
---|
| 459 | raise MailHostError("Message missing SMTP Header 'From'") |
---|
| 460 | mfrom = mo['From'] |
---|
| 461 | |
---|
| 462 | if mo.get('Bcc'): |
---|
| 463 | del mo['Bcc'] |
---|
| 464 | |
---|
| 465 | if not mo.get('Date'): |
---|
| 466 | mo['Date'] = DateTime().rfc822() |
---|
| 467 | |
---|
| 468 | return mo.as_string(), mto, mfrom |
---|
| 469 | |
---|
| 470 | |
---|
| 471 | def _set_recursive_charset(payload, charset=None): |
---|
| 472 | """Set charset for all parts of an multipart message.""" |
---|
| 473 | def _set_payload_charset(payload, charset=None, index=None): |
---|
| 474 | payload_from_string = False |
---|
| 475 | if not isinstance(payload, Message): |
---|
| 476 | payload = message_from_string(payload) |
---|
| 477 | payload_from_string = True |
---|
| 478 | charset_match = CHARSET_RE.search(payload['Content-Type'] or '') |
---|
| 479 | if charset and not charset_match: |
---|
| 480 | # Don't change the charset if already set |
---|
| 481 | # This encodes the payload automatically based on the default |
---|
| 482 | # encoding for the charset |
---|
| 483 | if payload_from_string: |
---|
| 484 | payload.get_payload()[index] = payload |
---|
| 485 | else: |
---|
| 486 | payload.set_charset(charset) |
---|
| 487 | elif charset_match and not charset: |
---|
| 488 | # If a charset parameter was provided use it for header encoding |
---|
| 489 | # below, otherwise, try to use the charset provided in the message. |
---|
| 490 | charset = charset_match.groups()[0] |
---|
| 491 | return charset |
---|
| 492 | if payload.is_multipart(): |
---|
| 493 | for index, payload in enumerate(payload.get_payload()): |
---|
| 494 | if payload.get_filename() is None: |
---|
| 495 | if not payload.is_multipart(): |
---|
| 496 | charset = _set_payload_charset(payload, |
---|
| 497 | charset=charset, |
---|
| 498 | index=index) |
---|
| 499 | else: |
---|
| 500 | _set_recursive_charset(payload, charset=charset) |
---|
| 501 | else: |
---|
| 502 | charset = _set_payload_charset(payload, charset=charset) |
---|
| 503 | return charset |
---|
| 504 | |
---|
| 505 | |
---|
| 506 | def _try_encode(text, charset): |
---|
| 507 | """Attempt to encode using the default charset if none is |
---|
| 508 | provided. Should we permit encoding errors?""" |
---|
| 509 | if charset: |
---|
| 510 | return text.encode(charset) |
---|
| 511 | else: |
---|
| 512 | return text.encode() |
---|
| 513 | |
---|
| 514 | |
---|
| 515 | def _encode_address_string(text, charset): |
---|
| 516 | """Split the email into parts and use header encoding on the name |
---|
| 517 | part if needed. We do this because the actual addresses need to be |
---|
| 518 | ASCII with no encoding for most SMTP servers, but the non-address |
---|
| 519 | parts should be encoded appropriately.""" |
---|
| 520 | header = Header() |
---|
| 521 | name, addr = parseaddr(text) |
---|
| 522 | try: |
---|
| 523 | name.decode('us-ascii') |
---|
| 524 | except UnicodeDecodeError: |
---|
| 525 | if charset: |
---|
| 526 | charset = Charset(charset) |
---|
| 527 | name = charset.header_encode(name) |
---|
| 528 | # We again replace rather than raise an error or pass an 8bit string |
---|
| 529 | header.append(formataddr((name, addr)), errors='replace') |
---|
| 530 | return header |
---|