Mercurial > p > roundup > code
comparison roundup/roundupdb.py @ 4092:4b0ddce43d08
migrate from MimeWriter to email
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 12 Mar 2009 05:55:16 +0000 |
| parents | ef0b4396888a |
| children | da682f38bad3 |
comparison
equal
deleted
inserted
replaced
| 4091:09e79cbeb827 | 4092:4b0ddce43d08 |
|---|---|
| 21 """Extending hyperdb with types specific to issue-tracking. | 21 """Extending hyperdb with types specific to issue-tracking. |
| 22 """ | 22 """ |
| 23 __docformat__ = 'restructuredtext' | 23 __docformat__ = 'restructuredtext' |
| 24 | 24 |
| 25 import re, os, smtplib, socket, time, random | 25 import re, os, smtplib, socket, time, random |
| 26 import cStringIO, base64, quopri, mimetypes | 26 import cStringIO, base64, mimetypes |
| 27 import os.path | 27 import os.path |
| 28 import logging | 28 import logging |
| 29 | 29 from email import Encoders |
| 30 from rfc2822 import encode_header | 30 from email.Utils import formataddr |
| 31 from email.Header import Header | |
| 32 from email.MIMEText import MIMEText | |
| 33 from email.MIMEBase import MIMEBase | |
| 31 | 34 |
| 32 from roundup import password, date, hyperdb | 35 from roundup import password, date, hyperdb |
| 33 from roundup.i18n import _ | 36 from roundup.i18n import _ |
| 34 | 37 |
| 35 # MessageSendError is imported for backwards compatibility | 38 # MessageSendError is imported for backwards compatibility |
| 36 from roundup.mailer import Mailer, straddr, MessageSendError | 39 from roundup.mailer import Mailer, MessageSendError, encode_quopri |
| 37 | 40 |
| 38 class Database: | 41 class Database: |
| 39 | 42 |
| 40 # remember the journal uid for the current journaltag so that: | 43 # remember the journal uid for the current journaltag so that: |
| 41 # a. we don't have to look it up every time we need it, and | 44 # a. we don't have to look it up every time we need it, and |
| 116 return userid | 119 return userid |
| 117 | 120 |
| 118 | 121 |
| 119 def log_debug(self, msg, *args, **kwargs): | 122 def log_debug(self, msg, *args, **kwargs): |
| 120 """Log a message with level DEBUG.""" | 123 """Log a message with level DEBUG.""" |
| 121 | 124 |
| 122 logger = self.get_logger() | 125 logger = self.get_logger() |
| 123 logger.debug(msg, *args, **kwargs) | 126 logger.debug(msg, *args, **kwargs) |
| 124 | 127 |
| 125 def log_info(self, msg, *args, **kwargs): | 128 def log_info(self, msg, *args, **kwargs): |
| 126 """Log a message with level INFO.""" | 129 """Log a message with level INFO.""" |
| 127 | 130 |
| 128 logger = self.get_logger() | 131 logger = self.get_logger() |
| 129 logger.info(msg, *args, **kwargs) | 132 logger.info(msg, *args, **kwargs) |
| 130 | 133 |
| 131 def get_logger(self): | 134 def get_logger(self): |
| 132 """Return the logger for this database.""" | 135 """Return the logger for this database.""" |
| 133 | 136 |
| 134 # Because getting a logger requires acquiring a lock, we want | 137 # Because getting a logger requires acquiring a lock, we want |
| 135 # to do it only once. | 138 # to do it only once. |
| 136 if not hasattr(self, '__logger'): | 139 if not hasattr(self, '__logger'): |
| 137 self.__logger = logging.getLogger('hyperdb') | 140 self.__logger = logging.getLogger('hyperdb') |
| 138 | 141 |
| 139 return self.__logger | 142 return self.__logger |
| 140 | 143 |
| 141 | 144 |
| 142 class DetectorError(RuntimeError): | 145 class DetectorError(RuntimeError): |
| 143 """ Raised by detectors that want to indicate that something's amiss | 146 """ Raised by detectors that want to indicate that something's amiss |
| 313 if not authname: | 316 if not authname: |
| 314 authname = users.get(authid, 'username', '') | 317 authname = users.get(authid, 'username', '') |
| 315 authaddr = users.get(authid, 'address', '') | 318 authaddr = users.get(authid, 'address', '') |
| 316 | 319 |
| 317 if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL: | 320 if authaddr and self.db.config.MAIL_ADD_AUTHOREMAIL: |
| 318 authaddr = " <%s>" % straddr( ('',authaddr) ) | 321 authaddr = " <%s>" % formataddr( ('',authaddr) ) |
| 319 elif authaddr: | 322 elif authaddr: |
| 320 authaddr = "" | 323 authaddr = "" |
| 321 | 324 |
| 322 # make the message body | 325 # make the message body |
| 323 m = [''] | 326 m = [''] |
| 364 | 367 |
| 365 # put in roundup's signature | 368 # put in roundup's signature |
| 366 if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom': | 369 if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom': |
| 367 m.append(self.email_signature(nodeid, msgid)) | 370 m.append(self.email_signature(nodeid, msgid)) |
| 368 | 371 |
| 369 # encode the content as quoted-printable | 372 # figure the encoding |
| 370 charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8') | 373 charset = getattr(self.db.config, 'EMAIL_CHARSET', 'utf-8') |
| 371 m = '\n'.join(m) | 374 |
| 372 if charset != 'utf-8': | 375 # construct the content and convert to unicode object |
| 373 m = unicode(m, 'utf-8').encode(charset) | 376 content = unicode('\n'.join(m), 'utf-8').encode(charset) |
| 374 content = cStringIO.StringIO(m) | |
| 375 content_encoded = cStringIO.StringIO() | |
| 376 quopri.encode(content, content_encoded, 0) | |
| 377 content_encoded = content_encoded.getvalue() | |
| 378 | 377 |
| 379 # make sure the To line is always the same (for testing mostly) | 378 # make sure the To line is always the same (for testing mostly) |
| 380 sendto.sort() | 379 sendto.sort() |
| 381 | 380 |
| 382 # make sure we have a from address | 381 # make sure we have a from address |
| 394 # send an individual message per recipient? | 393 # send an individual message per recipient? |
| 395 if self.db.config.NOSY_EMAIL_SENDING != 'single': | 394 if self.db.config.NOSY_EMAIL_SENDING != 'single': |
| 396 sendto = [[address] for address in sendto] | 395 sendto = [[address] for address in sendto] |
| 397 else: | 396 else: |
| 398 sendto = [sendto] | 397 sendto = [sendto] |
| 398 | |
| 399 tracker_name = unicode(self.db.config.TRACKER_NAME, 'utf-8') | |
| 400 tracker_name = formataddr((tracker_name, from_address)) | |
| 401 tracker_name = Header(tracker_name, charset) | |
| 399 | 402 |
| 400 # now send one or more messages | 403 # now send one or more messages |
| 401 # TODO: I believe we have to create a new message each time as we | 404 # TODO: I believe we have to create a new message each time as we |
| 402 # can't fiddle the recipients in the message ... worth testing | 405 # can't fiddle the recipients in the message ... worth testing |
| 403 # and/or fixing some day | 406 # and/or fixing some day |
| 404 first = True | 407 first = True |
| 405 for sendto in sendto: | 408 for sendto in sendto: |
| 406 # create the message | 409 # create the message |
| 407 mailer = Mailer(self.db.config) | 410 mailer = Mailer(self.db.config) |
| 408 message, writer = mailer.get_standard_message(sendto, subject, | 411 |
| 409 author) | 412 message = mailer.get_standard_message(sendto, subject, author, |
| 413 multipart=message_files) | |
| 410 | 414 |
| 411 # set reply-to to the tracker | 415 # set reply-to to the tracker |
| 412 tracker_name = self.db.config.TRACKER_NAME | 416 message['Reply-To'] = tracker_name |
| 413 if charset != 'utf-8': | |
| 414 tracker = unicode(tracker_name, 'utf-8').encode(charset) | |
| 415 tracker_name = encode_header(tracker_name, charset) | |
| 416 writer.addheader('Reply-To', straddr((tracker_name, from_address))) | |
| 417 | 417 |
| 418 # message ids | 418 # message ids |
| 419 if messageid: | 419 if messageid: |
| 420 writer.addheader('Message-Id', messageid) | 420 message['Message-Id'] = messageid |
| 421 if inreplyto: | 421 if inreplyto: |
| 422 writer.addheader('In-Reply-To', inreplyto) | 422 message['In-Reply-To'] = inreplyto |
| 423 | 423 |
| 424 # Generate a header for each link or multilink to | 424 # Generate a header for each link or multilink to |
| 425 # a class that has a name attribute | 425 # a class that has a name attribute |
| 426 for propname, prop in self.getprops().items(): | 426 for propname, prop in self.getprops().items(): |
| 427 if not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)): | 427 if not isinstance(prop, (hyperdb.Link, hyperdb.Multilink)): |
| 438 values = self.get(nodeid, propname) | 438 values = self.get(nodeid, propname) |
| 439 if not values: | 439 if not values: |
| 440 continue | 440 continue |
| 441 values = [cl.get(v, 'name') for v in values] | 441 values = [cl.get(v, 'name') for v in values] |
| 442 values = ', '.join(values) | 442 values = ', '.join(values) |
| 443 writer.addheader("X-Roundup-%s-%s" % (self.classname, propname), | 443 header = "X-Roundup-%s-%s"%(self.classname, propname) |
| 444 values) | 444 try: |
| 445 message[header] = values.encode('ascii') | |
| 446 except UnicodeError: | |
| 447 message[header] = Header(values, charset) | |
| 448 | |
| 445 if not inreplyto: | 449 if not inreplyto: |
| 446 # Default the reply to the first message | 450 # Default the reply to the first message |
| 447 msgs = self.get(nodeid, 'messages') | 451 msgs = self.get(nodeid, 'messages') |
| 448 # Assume messages are sorted by increasing message number here | 452 # Assume messages are sorted by increasing message number here |
| 449 # If the issue is just being created, and the submitter didn't | 453 # If the issue is just being created, and the submitter didn't |
| 450 # provide a message, then msgs will be empty. | 454 # provide a message, then msgs will be empty. |
| 451 if msgs and msgs[0] != nodeid: | 455 if msgs and msgs[0] != nodeid: |
| 452 inreplyto = messages.get(msgs[0], 'messageid') | 456 inreplyto = messages.get(msgs[0], 'messageid') |
| 453 if inreplyto: | 457 if inreplyto: |
| 454 writer.addheader('In-Reply-To', inreplyto) | 458 message['In-Reply-To'] = inreplyto |
| 455 | 459 |
| 456 # attach files | 460 # attach files |
| 457 if message_files: | 461 if message_files: |
| 458 part = writer.startmultipartbody('mixed') | 462 # first up the text as a part |
| 459 part = writer.nextpart() | 463 part = MIMEText(content) |
| 460 part.addheader('Content-Transfer-Encoding', 'quoted-printable') | 464 encode_quopri(part) |
| 461 body = part.startbody('text/plain; charset=%s'%charset) | 465 message.attach(part) |
| 462 body.write(content_encoded) | 466 |
| 463 for fileid in message_files: | 467 for fileid in message_files: |
| 464 name = files.get(fileid, 'name') | 468 name = files.get(fileid, 'name') |
| 465 mime_type = files.get(fileid, 'type') | 469 mime_type = files.get(fileid, 'type') |
| 466 content = files.get(fileid, 'content') | 470 content = files.get(fileid, 'content') |
| 467 part = writer.nextpart() | |
| 468 if mime_type == 'text/plain': | 471 if mime_type == 'text/plain': |
| 469 part.addheader('Content-Disposition', | |
| 470 'attachment;\n filename="%s"'%name) | |
| 471 try: | 472 try: |
| 472 content.decode('ascii') | 473 content.decode('ascii') |
| 473 except UnicodeError: | 474 except UnicodeError: |
| 474 # the content cannot be 7bit-encoded. | 475 # the content cannot be 7bit-encoded. |
| 475 # use quoted printable | 476 # use quoted printable |
| 476 part.addheader('Content-Transfer-Encoding', | 477 # XXX stuffed if we know the charset though :( |
| 477 'quoted-printable') | 478 part = MIMEText(content) |
| 478 body = part.startbody('text/plain') | 479 encode_quopri(part) |
| 479 body.write(quopri.encodestring(content)) | |
| 480 else: | 480 else: |
| 481 part.addheader('Content-Transfer-Encoding', '7bit') | 481 part = MIMEText(content) |
| 482 body = part.startbody('text/plain') | 482 part['Content-Transfer-Encoding'] = '7bit' |
| 483 body.write(content) | |
| 484 else: | 483 else: |
| 485 # some other type, so encode it | 484 # some other type, so encode it |
| 486 if not mime_type: | 485 if not mime_type: |
| 487 # this should have been done when the file was saved | 486 # this should have been done when the file was saved |
| 488 mime_type = mimetypes.guess_type(name)[0] | 487 mime_type = mimetypes.guess_type(name)[0] |
| 489 if mime_type is None: | 488 if mime_type is None: |
| 490 mime_type = 'application/octet-stream' | 489 mime_type = 'application/octet-stream' |
| 491 part.addheader('Content-Disposition', | 490 main, sub = mime_type.split('/') |
| 492 'attachment;\n filename="%s"'%name) | 491 part = MIMEBase(main, sub) |
| 493 part.addheader('Content-Transfer-Encoding', 'base64') | 492 part.set_payload(content) |
| 494 body = part.startbody(mime_type) | 493 Encoders.encode_base64(part) |
| 495 body.write(base64.encodestring(content)) | 494 part['Content-Disposition'] = 'attachment;\n filename="%s"'%name |
| 496 writer.lastpart() | 495 message.attach(part) |
| 496 | |
| 497 else: | 497 else: |
| 498 writer.addheader('Content-Transfer-Encoding', | 498 message.set_payload(content) |
| 499 'quoted-printable') | 499 encode_quopri(message) |
| 500 body = writer.startbody('text/plain; charset=%s'%charset) | |
| 501 body.write(content_encoded) | |
| 502 | 500 |
| 503 if first: | 501 if first: |
| 504 mailer.smtp_send(sendto + bcc_sendto, message) | 502 mailer.smtp_send(sendto + bcc_sendto, message) |
| 505 else: | 503 else: |
| 506 mailer.smtp_send(sendto, message) | 504 mailer.smtp_send(sendto, message) |
| 520 if not base.endswith('/'): | 518 if not base.endswith('/'): |
| 521 base = base + '/' | 519 base = base + '/' |
| 522 web = base + self.classname + nodeid | 520 web = base + self.classname + nodeid |
| 523 | 521 |
| 524 # ensure the email address is properly quoted | 522 # ensure the email address is properly quoted |
| 525 email = straddr((self.db.config.TRACKER_NAME, | 523 email = formataddr((self.db.config.TRACKER_NAME, |
| 526 self.db.config.TRACKER_EMAIL)) | 524 self.db.config.TRACKER_EMAIL)) |
| 527 | 525 |
| 528 line = '_' * max(len(web)+2, len(email)) | 526 line = '_' * max(len(web)+2, len(email)) |
| 529 return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line) | 527 return '\n%s\n%s\n<%s>\n%s'%(line, email, web, line) |
| 530 | 528 |
