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

Roundup Issue Tracker: http://roundup-tracker.org/