PageRenderTime 43ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

/Products/listen/content/mailboxer_list.py

https://github.com/socialplanning/opencore-listen
Python | 303 lines | 236 code | 26 blank | 41 comment | 15 complexity | 8a1e8ebf082b9727718ecb0d3c1562de MD5 | raw file
  1. from Acquisition import aq_get
  2. from DateTime import DateTime
  3. from Products.CMFCore.utils import getToolByName
  4. from Products.CMFPlone.utils import base_hasattr
  5. from Products.MailBoxer.MailBoxer import FALSE
  6. from Products.MailBoxer.MailBoxer import MailBoxer
  7. from Products.MailBoxer.MailBoxer import TRUE
  8. from Products.MailBoxer.MailBoxer import setMailBoxerProperties
  9. from Products.MailBoxer.MailBoxerTools import splitMail
  10. from Products.MailBoxer.messagevalidators import setDefaultValidatorChain
  11. from Products.listen.i18n import _
  12. from Products.listen.interfaces import IMailFromString
  13. from Products.listen.interfaces import IMessageHandler
  14. from Products.listen.lib.browser_utils import getSiteEncoding
  15. from Products.listen.lib.common import construct_simple_encoded_message
  16. from email.Header import Header
  17. from plone.mail import decode_header
  18. from zope.app import zapi
  19. from zope.component import queryMultiAdapter
  20. import logging
  21. import re
  22. import rfc822
  23. import zope.event
  24. logger = logging.getLogger('listen.content.mailboxer_list')
  25. # A REGEX for messages containing mail-commands
  26. mail_command_re = re.compile('\(mail-command:([A-Za-z_-]+)',
  27. re.IGNORECASE)
  28. class MailBoxerMailingList(MailBoxer):
  29. """
  30. A slightly customized MailBoxer with some less cryptic method names
  31. """
  32. # Mailboxer wants the name of a catalog to acquire
  33. catalog = 'mail_catalog'
  34. def manage_mailboxer(self, REQUEST):
  35. """ Override to allow triggering of pluggable mail handlers
  36. """
  37. if self.checkMail(REQUEST):
  38. return FALSE
  39. # Check for subscription/unsubscription-request and confirmations
  40. if self.requestMail(REQUEST):
  41. return TRUE
  42. if self.adaptMail(REQUEST):
  43. return TRUE
  44. if self.manager_mail(REQUEST):
  45. return TRUE
  46. # Process the mail...
  47. self.processMail(REQUEST)
  48. return TRUE
  49. def manager_mail(self, REQUEST):
  50. # Intended for subclasses to override.
  51. return False
  52. def adaptMail(self, REQUEST):
  53. """Adapts an incoming request to a specialized view for handling
  54. mail if requested."""
  55. mailString = self.getMailFromRequest(REQUEST)
  56. (header, body) = splitMail(mailString)
  57. encoding = getSiteEncoding(self)
  58. subject = decode_header(str(Header(header.get('subject',''),
  59. encoding,
  60. errors='replace')))
  61. command_match = re.search(mail_command_re, subject)
  62. if command_match:
  63. command_name = command_match.groups()[0]
  64. adapter = queryMultiAdapter((self, REQUEST), IMessageHandler,
  65. name=command_name)
  66. if adapter is not None:
  67. adapter.processMail()
  68. return True
  69. return False
  70. def sendCommandRequestMail(self, address, subject, body, from_address=None, extra_headers={}):
  71. if not address:
  72. print ('Products.listen.content.MailBoxerMailingList.sendCommandRequestMail() '
  73. 'invalid address; user may have been deleted')
  74. return
  75. if from_address is None:
  76. from_address = self.mailto
  77. # Default headers:
  78. headers = {'X-Mailer': self.getValueFor('xmailer')}
  79. headers.update(extra_headers)
  80. encoding = getSiteEncoding(self)
  81. message = construct_simple_encoded_message(from_addr=from_address,
  82. to_addr=address,
  83. subject=subject,
  84. body=body,
  85. other_headers=headers,
  86. encoding=encoding)
  87. # XXX: Acquire the MailHost, yuck
  88. mh = getToolByName(self, 'MailHost')
  89. mh.send(str(message))
  90. def manage_afterAdd(self, item, container, **kw):
  91. """Setup properties and sub-objects"""
  92. # Only run on add, not rename, etc.
  93. if not base_hasattr(self, 'mqueue'):
  94. setMailBoxerProperties(self, self.REQUEST, kw)
  95. # Setup the default checkMail validator chain
  96. setDefaultValidatorChain(self)
  97. # Add Archive
  98. archive = zapi.createObject('listen.ArchiveFactory', self.storage,
  99. title=_(u'List Archive'))
  100. item._setObject(self.storage, archive)
  101. # Add moderation queue
  102. mqueue = zapi.createObject('listen.QueueFactory', self.mailqueue,
  103. title=_(u'Moderation queue'))
  104. item._setObject(self.mailqueue, mqueue)
  105. ttool = getToolByName(self, 'portal_types', None)
  106. if ttool is not None:
  107. # If the archive/queue are CMF types then we must finish
  108. # constructing them.
  109. fti = ttool.getTypeInfo(mqueue)
  110. if fti is not None:
  111. fti._finishConstruction(mqueue)
  112. fti = ttool.getTypeInfo(archive)
  113. if fti is not None:
  114. fti._finishConstruction(archive)
  115. MailBoxer.manage_afterAdd(self, self.REQUEST, kw)
  116. # modified manage_addMail from MailBoxer.py to make things more modular
  117. def addMail(self, mailString, force_id=False):
  118. """ Store mail in date based folder archive.
  119. Returns created mail. See IMailingList interface.
  120. """
  121. archive = aq_get(self, self.getValueFor('storage'), None)
  122. # no archive available? then return immediately
  123. if archive is None:
  124. return None
  125. (header, body) = splitMail(mailString)
  126. # if 'keepdate' is set, get date from mail,
  127. if self.getValueFor('keepdate'):
  128. assert header.get("date") is not None
  129. time = DateTime(header.get("date"))
  130. # ... take our own date, clients are always lying!
  131. else:
  132. time = DateTime()
  133. # now let's create the date-path (yyyy/mm)
  134. year = str(time.year()) # yyyy
  135. month = str(time.mm()) # mm
  136. title = "%s %s"%(time.Month(), year)
  137. # do we have a year folder already?
  138. if not base_hasattr(archive, year):
  139. self.addMailBoxerFolder(archive, year, year, btree=False)
  140. yearFolder=getattr(archive, year)
  141. # do we have a month folder already?
  142. if not base_hasattr(yearFolder, month):
  143. self.addMailBoxerFolder(yearFolder, month, title)
  144. mailFolder=getattr(yearFolder, month)
  145. subject = header.get('subject', _('No Subject'))
  146. sender = header.get('from',_('Unknown'))
  147. # search a free id for the mailobject
  148. id = time.millis()
  149. while base_hasattr(mailFolder, str(id)):
  150. if force_id:
  151. raise AssertionError("ID %s already exists on folder %s" % (id, mailFolder))
  152. id = id + 1
  153. id = str(id)
  154. self.addMailBoxerMail(mailFolder, id, sender, subject, time,
  155. mailString)
  156. mailObject = getattr(mailFolder, id)
  157. return mailObject
  158. # Override the original MailBoxer method
  159. manage_addMail = addMail
  160. # Componentize folder creation
  161. def addMailBoxerFolder(self, context, id, title, btree=True):
  162. """ Adds an archive-folder using a configured factory
  163. """
  164. folder = zapi.createObject('listen.FolderFactory',
  165. id, title, btree=btree)
  166. context._setObject(id, folder)
  167. # Componentize mail creation
  168. def addMailBoxerMail(self, folder, id, sender, subject, date, mail):
  169. # Strip out the list name from the subject, as it serves no purpose
  170. # in the archive.
  171. subject = subject.replace('[%s]' % self.getValueFor('title'), '')
  172. new_message = zapi.createObject('listen.MailFactory',
  173. id, sender, subject, date)
  174. folder._setObject(id, new_message)
  175. msg = getattr(folder, id)
  176. # Adapt message to provide methods for parsing mail and extracting
  177. # headers
  178. settable_msg = IMailFromString(msg)
  179. # This is ugly, but it is the MailBoxer way, last option means no
  180. # attachments.
  181. store_attachments = self.archived == 0
  182. # Set properties on message
  183. settable_msg.createMailFromMessage(mail, store_attachments)
  184. zope.event.notify(
  185. zope.app.event.objectevent.ObjectModifiedEvent(msg))
  186. return msg
  187. # For now use the builtin methods
  188. resetBounces = MailBoxer.manage_resetBounces
  189. moderateMail = MailBoxer.manage_moderateMail
  190. # Override getValueFor to always return ASCII encoded strings, as they
  191. # may be included in an email header or body. We use 7-bit encoded
  192. # in the site encoding if the string won't convert to ascii.
  193. # Our mailing list title, and email addresses may be unicode, this will
  194. # convert them
  195. def getValueFor(self, key):
  196. # value = MailBoxer.getValueFor(self, key)
  197. # Simplify: we have no need for all the strange 'getter' magic that
  198. # MailBoxer does
  199. value = self.getProperty(key)
  200. encoding = getSiteEncoding(self)
  201. try:
  202. if hasattr(value, 'encode'):
  203. value = self._encodedHeader(value, encoding)
  204. elif isinstance(value, list) or isinstance(value, tuple):
  205. value = [self._encodedHeader(v, encoding) for v in value]
  206. except (UnicodeEncodeError, UnicodeDecodeError):
  207. # Just in case one of our 'utf-8' encoding attempts fails, we
  208. # give up
  209. pass
  210. except AttributeError:
  211. # No 'encode' method on a list element, so give up
  212. pass
  213. return value
  214. @staticmethod
  215. def _encodedHeader(value, encoding):
  216. """
  217. Given a value (or list of values) and an ecoding, return it
  218. encoded as per rfc2047 for use in a MIME message header.
  219. >>> from Products.listen.content.mailboxer_list import MailBoxerMailingList
  220. If the input can be converted to ascii, it will be, regardless
  221. of the encoding argument:
  222. >>> MailBoxerMailingList._encodedHeader('blah', 'utf8')
  223. 'blah'
  224. If it can be encoded to the target encoding, it will be, and
  225. then encoded as per rfc2047:
  226. >>> input = u'\xbfhmm?'
  227. >>> MailBoxerMailingList._encodedHeader(input, 'utf8')
  228. '=?utf8?b?wr9obW0/?='
  229. >>> MailBoxerMailingList._encodedHeader(input.encode('utf8'), 'utf8')
  230. '=?utf8?b?wr9obW0/?='
  231. >>> raw = 'a string \345\276\267\345\233\275'
  232. >>> MailBoxerMailingList._encodedHeader(raw, 'utf8')
  233. '=?utf8?b?YSBzdHJpbmcg5b635Zu9?='
  234. All other cases will raise an exception. Typically this means
  235. a raw byte string in an incompatible encoding:
  236. >>> MailBoxerMailingList._encodedHeader(input.encode('latin1'), 'utf8')
  237. Traceback (most recent call last):
  238. ...
  239. UnicodeDecodeError: 'utf8' codec can't decode byte 0xbf in position 0: unexpected code byte
  240. """
  241. try:
  242. value = value.encode('ascii')
  243. except (UnicodeEncodeError, UnicodeDecodeError):
  244. try:
  245. value = Header(value.encode(encoding), encoding).encode()
  246. except UnicodeDecodeError:
  247. try:
  248. value = Header(value, encoding).encode()
  249. except UnicodeDecodeError:
  250. logger.error("Could not guess encoding of raw bytestring %r, there is probably a bug in the code that created this header." % value)
  251. raise
  252. return value