PageRenderTime 691ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/mailman/app/moderator.py

https://gitlab.com/noc0lour/mailman
Python | 278 lines | 219 code | 11 blank | 48 comment | 21 complexity | cf3881be542d0aa29a42a089d434262d MD5 | raw file
  1. # Copyright (C) 2007-2016 by the Free Software Foundation, Inc.
  2. #
  3. # This file is part of GNU Mailman.
  4. #
  5. # GNU Mailman is free software: you can redistribute it and/or modify it under
  6. # the terms of the GNU General Public License as published by the Free
  7. # Software Foundation, either version 3 of the License, or (at your option)
  8. # any later version.
  9. #
  10. # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  12. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  13. # more details.
  14. #
  15. # You should have received a copy of the GNU General Public License along with
  16. # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
  17. """Application support for moderators."""
  18. import time
  19. import logging
  20. from email.utils import formatdate, getaddresses, make_msgid
  21. from mailman import public
  22. from mailman.app.membership import delete_member
  23. from mailman.config import config
  24. from mailman.core.i18n import _
  25. from mailman.email.message import UserNotification
  26. from mailman.interfaces.action import Action
  27. from mailman.interfaces.listmanager import ListDeletingEvent
  28. from mailman.interfaces.member import NotAMemberError
  29. from mailman.interfaces.messages import IMessageStore
  30. from mailman.interfaces.requests import IListRequests, RequestType
  31. from mailman.utilities.datetime import now
  32. from mailman.utilities.i18n import make
  33. from zope.component import getUtility
  34. NL = '\n'
  35. vlog = logging.getLogger('mailman.vette')
  36. slog = logging.getLogger('mailman.subscribe')
  37. @public
  38. def hold_message(mlist, msg, msgdata=None, reason=None):
  39. """Hold a message for moderator approval.
  40. The message is added to the mailing list's request database.
  41. :param mlist: The mailing list to hold the message on.
  42. :param msg: The message to hold.
  43. :param msgdata: Optional message metadata to hold. If not given, a new
  44. metadata dictionary is created and held with the message.
  45. :param reason: Optional string reason why the message is being held. If
  46. not given, the empty string is used.
  47. :return: An id used to handle the held message later.
  48. """
  49. if msgdata is None:
  50. msgdata = {}
  51. else:
  52. # Make a copy of msgdata so that subsequent changes won't corrupt the
  53. # request database. TBD: remove the `filebase' key since this will
  54. # not be relevant when the message is resurrected.
  55. msgdata = msgdata.copy()
  56. if reason is None:
  57. reason = ''
  58. # Add the message to the message store. It is required to have a
  59. # Message-ID header.
  60. message_id = msg.get('message-id')
  61. if message_id is None:
  62. msg['Message-ID'] = message_id = make_msgid()
  63. elif isinstance(message_id, bytes):
  64. message_id = message_id.decode('ascii')
  65. getUtility(IMessageStore).add(msg)
  66. # Prepare the message metadata with some extra information needed only by
  67. # the moderation interface.
  68. msgdata['_mod_message_id'] = message_id
  69. msgdata['_mod_listid'] = mlist.list_id
  70. msgdata['_mod_sender'] = msg.sender
  71. msgdata['_mod_subject'] = msg.get('subject', _('(no subject)'))
  72. msgdata['_mod_reason'] = reason
  73. msgdata['_mod_hold_date'] = now().isoformat()
  74. # Now hold this request. We'll use the message_id as the key.
  75. requestsdb = IListRequests(mlist)
  76. request_id = requestsdb.hold_request(
  77. RequestType.held_message, message_id, msgdata)
  78. return request_id
  79. @public
  80. def handle_message(mlist, id, action, comment=None, forward=None):
  81. message_store = getUtility(IMessageStore)
  82. requestdb = IListRequests(mlist)
  83. key, msgdata = requestdb.get_request(id)
  84. # Handle the action.
  85. rejection = None
  86. message_id = msgdata['_mod_message_id']
  87. sender = msgdata['_mod_sender']
  88. subject = msgdata['_mod_subject']
  89. keep = False
  90. if action in (Action.defer, Action.hold):
  91. # Nothing to do, but preserve the message for later.
  92. keep = True
  93. elif action is Action.discard:
  94. rejection = 'Discarded'
  95. elif action is Action.reject:
  96. rejection = 'Refused'
  97. member = mlist.members.get_member(sender)
  98. if member:
  99. language = member.preferred_language
  100. else:
  101. language = None
  102. send_rejection(
  103. mlist, _('Posting of your message titled "$subject"'),
  104. sender, comment or _('[No reason given]'), language)
  105. elif action is Action.accept:
  106. # Start by getting the message from the message store.
  107. msg = message_store.get_message_by_id(message_id)
  108. # Delete moderation-specific entries from the message metadata.
  109. for key in list(msgdata):
  110. if key.startswith('_mod_'):
  111. del msgdata[key]
  112. # Add some metadata to indicate this message has now been approved.
  113. msgdata['approved'] = True
  114. msgdata['moderator_approved'] = True
  115. # Calculate a new filebase for the approved message, otherwise
  116. # delivery errors will cause duplicates.
  117. if 'filebase' in msgdata:
  118. del msgdata['filebase']
  119. # Queue the file for delivery. Trying to deliver the message directly
  120. # here can lead to a huge delay in web turnaround. Log the moderation
  121. # and add a header.
  122. msg['X-Mailman-Approved-At'] = formatdate(
  123. time.mktime(now().timetuple()), localtime=True)
  124. vlog.info('held message approved, message-id: %s',
  125. msg.get('message-id', 'n/a'))
  126. # Stick the message back in the incoming queue for further
  127. # processing.
  128. config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata)
  129. else:
  130. raise AssertionError('Unexpected action: {0}'.format(action))
  131. # Forward the message.
  132. if forward:
  133. # Get a copy of the original message from the message store.
  134. msg = message_store.get_message_by_id(message_id)
  135. # It's possible the forwarding address list is a comma separated list
  136. # of display_name/address pairs.
  137. addresses = [addr[1] for addr in getaddresses(forward)]
  138. language = mlist.preferred_language
  139. if len(addresses) == 1:
  140. # If the address getting the forwarded message is a member of
  141. # the list, we want the headers of the outer message to be
  142. # encoded in their language. Otherwise it'll be the preferred
  143. # language of the mailing list. This is better than sending a
  144. # separate message per recipient.
  145. member = mlist.members.get_member(addresses[0])
  146. if member:
  147. language = member.preferred_language
  148. with _.using(language.code):
  149. fmsg = UserNotification(
  150. addresses, mlist.bounces_address,
  151. _('Forward of moderated message'),
  152. lang=language)
  153. fmsg.set_type('message/rfc822')
  154. fmsg.attach(msg)
  155. fmsg.send(mlist)
  156. # Delete the request if it's not being kept.
  157. if not keep:
  158. requestdb.delete_request(id)
  159. # Log the rejection
  160. if rejection:
  161. note = """%s: %s posting:
  162. \tFrom: %s
  163. \tSubject: %s"""
  164. if comment:
  165. note += '\n\tReason: ' + comment
  166. vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
  167. @public
  168. def hold_unsubscription(mlist, email):
  169. data = dict(email=email)
  170. requestsdb = IListRequests(mlist)
  171. request_id = requestsdb.hold_request(
  172. RequestType.unsubscription, email, data)
  173. vlog.info('%s: held unsubscription request from %s',
  174. mlist.fqdn_listname, email)
  175. # Possibly notify the administrator of the hold
  176. if mlist.admin_immed_notify:
  177. subject = _(
  178. 'New unsubscription request from $mlist.display_name by $email')
  179. text = make('unsubauth.txt',
  180. mailing_list=mlist,
  181. email=email,
  182. listname=mlist.fqdn_listname,
  183. admindb_url=mlist.script_url('admindb'),
  184. )
  185. # This message should appear to come from the <list>-owner so as
  186. # to avoid any useless bounce processing.
  187. msg = UserNotification(
  188. mlist.owner_address, mlist.owner_address,
  189. subject, text, mlist.preferred_language)
  190. msg.send(mlist, tomoderators=True)
  191. return request_id
  192. @public
  193. def handle_unsubscription(mlist, id, action, comment=None):
  194. requestdb = IListRequests(mlist)
  195. key, data = requestdb.get_request(id)
  196. email = data['email']
  197. if action is Action.defer:
  198. # Nothing to do.
  199. return
  200. elif action is Action.discard:
  201. # Nothing to do except delete the request from the database.
  202. pass
  203. elif action is Action.reject:
  204. key, data = requestdb.get_request(id)
  205. send_rejection(
  206. mlist, _('Unsubscription request'), email,
  207. comment or _('[No reason given]'))
  208. elif action is Action.accept:
  209. key, data = requestdb.get_request(id)
  210. try:
  211. delete_member(mlist, email)
  212. except NotAMemberError:
  213. # User has already been unsubscribed.
  214. pass
  215. slog.info('%s: deleted %s', mlist.fqdn_listname, email)
  216. else:
  217. raise AssertionError('Unexpected action: {0}'.format(action))
  218. # Delete the request from the database.
  219. requestdb.delete_request(id)
  220. @public
  221. def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None):
  222. # As this message is going to the requester, try to set the language to
  223. # his/her language choice, if they are a member. Otherwise use the list's
  224. # preferred language.
  225. display_name = mlist.display_name # noqa
  226. if lang is None:
  227. member = mlist.members.get_member(recip)
  228. lang = (mlist.preferred_language
  229. if member is None
  230. else member.preferred_language)
  231. text = make('refuse.txt',
  232. mailing_list=mlist,
  233. language=lang.code,
  234. listname=mlist.fqdn_listname,
  235. request=request,
  236. reason=comment,
  237. adminaddr=mlist.owner_address,
  238. )
  239. with _.using(lang.code):
  240. # add in original message, but not wrap/filled
  241. if origmsg:
  242. text = NL.join(
  243. [text,
  244. '---------- ' + _('Original Message') + ' ----------',
  245. str(origmsg)
  246. ])
  247. subject = _('Request to mailing list "$display_name" rejected')
  248. msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
  249. msg.send(mlist)
  250. @public
  251. def handle_ListDeletingEvent(event):
  252. if not isinstance(event, ListDeletingEvent):
  253. return
  254. # Get the held requests database for the mailing list. Since the mailing
  255. # list is about to get deleted, we can delete all associated requests.
  256. requestsdb = IListRequests(event.mailing_list)
  257. for request in requestsdb.held_requests:
  258. requestsdb.delete_request(request.id)