PageRenderTime 55ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/src/mailman/runners/command.py

https://gitlab.com/aadityashukla1989/mailman
Python | 235 lines | 185 code | 13 blank | 37 comment | 13 complexity | bd8cb251d7b377768f79bdfa4e4ad93f MD5 | raw file
  1. # Copyright (C) 1998-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. """-request robot command runner."""
  18. # See the delivery diagram in IncomingRunner.py. This module handles all
  19. # email destined for mylist-request, -join, and -leave. It no longer handles
  20. # bounce messages (i.e. -admin or -bounces), nor does it handle mail to
  21. # -owner.
  22. import re
  23. import logging
  24. from email.errors import HeaderParseError
  25. from email.header import decode_header, make_header
  26. from email.iterators import typed_subpart_iterator
  27. from io import StringIO
  28. from mailman import public
  29. from mailman.config import config
  30. from mailman.core.i18n import _
  31. from mailman.core.runner import Runner
  32. from mailman.email.message import UserNotification
  33. from mailman.interfaces.command import ContinueProcessing, IEmailResults
  34. from mailman.interfaces.languages import ILanguageManager
  35. from zope.component import getUtility
  36. from zope.interface import implementer
  37. NL = '\n'
  38. log = logging.getLogger('mailman.vette')
  39. class CommandFinder:
  40. """Generate commands from the content of a message."""
  41. def __init__(self, msg, msgdata, results):
  42. self.command_lines = []
  43. self.ignored_lines = []
  44. self.processed_lines = []
  45. # Depending on where the message was destined to, add some implicit
  46. # commands. For example, if this was sent to the -join or -leave
  47. # addresses, it's the same as if 'join' or 'leave' commands were sent
  48. # to the -request address.
  49. subaddress = msgdata.get('subaddress')
  50. if subaddress == 'join':
  51. self.command_lines.append('join')
  52. elif subaddress == 'leave':
  53. self.command_lines.append('leave')
  54. elif subaddress == 'confirm':
  55. mo = re.match(config.mta.verp_confirm_regexp, msg.get('to', ''))
  56. if mo:
  57. self.command_lines.append('confirm ' + mo.group('cookie'))
  58. # Extract the subject header and do RFC 2047 decoding.
  59. raw_subject = msg.get('subject', '')
  60. try:
  61. subject = str(make_header(decode_header(raw_subject)))
  62. # Mail commands must be ASCII.
  63. self.command_lines.append(subject.encode('us-ascii'))
  64. except (HeaderParseError, UnicodeError, LookupError):
  65. # The Subject header was unparseable or not ASCII. If the raw
  66. # subject is a unicode object, convert it to ASCII ignoring all
  67. # bogus characters. Otherwise, there's nothing in the subject
  68. # that we can use.
  69. if isinstance(raw_subject, str):
  70. safe_subject = raw_subject.encode('us-ascii', 'ignore')
  71. self.command_lines.append(safe_subject)
  72. # Find the first text/plain part of the message.
  73. part = None
  74. for part in typed_subpart_iterator(msg, 'text', 'plain'):
  75. break
  76. if part is None or part is not msg:
  77. # Either there was no text/plain part or we ignored some
  78. # non-text/plain parts.
  79. print(_('Ignoring non-text/plain MIME parts'), file=results)
  80. if part is None:
  81. # There was no text/plain part to be found.
  82. return
  83. body = part.get_payload()
  84. # text/plain parts better have string payloads.
  85. assert isinstance(body, (bytes, str)), 'Non-string decoded payload'
  86. lines = body.splitlines()
  87. # Use no more lines than specified
  88. max_lines = int(config.mailman.email_commands_max_lines)
  89. self.command_lines.extend(lines[:max_lines])
  90. self.ignored_lines.extend(lines[max_lines:])
  91. def __iter__(self):
  92. """Return each command line, split into space separated arguments."""
  93. while self.command_lines:
  94. line = self.command_lines.pop(0)
  95. self.processed_lines.append(line)
  96. parts = line.strip().split()
  97. if len(parts) == 0:
  98. continue
  99. # Ensure that all the parts are unicodes. Since we only accept
  100. # ASCII commands and arguments, ignore anything else.
  101. parts = [(part
  102. if isinstance(part, str)
  103. else part.decode('ascii', 'ignore'))
  104. for part in parts]
  105. yield parts
  106. @public
  107. @implementer(IEmailResults)
  108. class Results:
  109. """The email command results."""
  110. def __init__(self, charset='us-ascii'):
  111. self._output = StringIO()
  112. self.charset = charset
  113. print(_("""\
  114. The results of your email command are provided below.
  115. """), file=self._output)
  116. def write(self, text):
  117. if isinstance(text, bytes):
  118. text = text.decode(self.charset, 'ignore')
  119. self._output.write(text)
  120. def __str__(self):
  121. value = self._output.getvalue()
  122. assert isinstance(value, str), 'Not a string: %r' % value
  123. return value
  124. @public
  125. class CommandRunner(Runner):
  126. """The email command runner."""
  127. def _dispose(self, mlist, msg, msgdata):
  128. message_id = msg.get('message-id', 'n/a')
  129. # The policy here is similar to the Replybot policy. If a message has
  130. # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
  131. # the command message.
  132. precedence = msg.get('precedence', '').lower()
  133. ack = msg.get('x-ack', '').lower()
  134. if ack != 'yes' and precedence in ('bulk', 'junk', 'list'):
  135. log.info('%s Precedence: %s message discarded by: %s',
  136. message_id, precedence, mlist.request_address)
  137. return False
  138. # Do replybot for commands.
  139. replybot = config.handlers['replybot']
  140. replybot.process(mlist, msg, msgdata)
  141. if mlist.autorespond_requests == 1:
  142. # Respond and discard.
  143. log.info('%s -request message replied and discard', message_id)
  144. return False
  145. # Now craft the response and process the command lines.
  146. charset = msg.get_param('charset')
  147. if charset is None:
  148. charset = 'us-ascii'
  149. results = Results(charset)
  150. # Include just a few key pieces of information from the original: the
  151. # sender, date, and message id.
  152. print(_('- Original message details:'), file=results)
  153. subject = msg.get('subject', 'n/a') # noqa
  154. date = msg.get('date', 'n/a') # noqa
  155. from_ = msg.get('from', 'n/a') # noqa
  156. print(_(' From: $from_'), file=results)
  157. print(_(' Subject: $subject'), file=results)
  158. print(_(' Date: $date'), file=results)
  159. print(_(' Message-ID: $message_id'), file=results)
  160. print(_('\n- Results:'), file=results)
  161. finder = CommandFinder(msg, msgdata, results)
  162. for parts in finder:
  163. command = None
  164. # Try to find a command on this line. There may be a Re: prefix
  165. # (possibly internationalized) so try with the first and second
  166. # words on the line.
  167. if len(parts) > 0:
  168. command_name = parts.pop(0)
  169. command = config.commands.get(command_name)
  170. if command is None and len(parts) > 0:
  171. command_name = parts.pop(0)
  172. command = config.commands.get(command_name)
  173. if command is None:
  174. print(_('No such command: $command_name'), file=results)
  175. else:
  176. status = command.process(
  177. mlist, msg, msgdata, parts, results)
  178. assert status in ContinueProcessing, (
  179. 'Invalid status: %s' % status)
  180. if status == ContinueProcessing.no:
  181. break
  182. # All done. Strip blank lines and send the response.
  183. lines = [line.strip() for line in finder.command_lines if line]
  184. if len(lines) > 0:
  185. print(_('\n- Unprocessed:'), file=results)
  186. for line in lines:
  187. print(line, file=results)
  188. lines = [line.strip() for line in finder.ignored_lines if line]
  189. if len(lines) > 0:
  190. print(_('\n- Ignored:'), file=results)
  191. for line in lines:
  192. print(line, file=results)
  193. print(_('\n- Done.'), file=results)
  194. # Send a reply, but do not attach the original message. This is a
  195. # compromise because the original message is often helpful in tracking
  196. # down problems, but it's also a vector for backscatter spam.
  197. language = getUtility(ILanguageManager)[msgdata['lang']]
  198. reply = UserNotification(msg.sender, mlist.bounces_address,
  199. _('The results of your email commands'),
  200. lang=language)
  201. cte = msg.get('content-transfer-encoding')
  202. if cte is not None:
  203. reply['Content-Transfer-Encoding'] = cte
  204. # Find a charset for the response body. Try the original message's
  205. # charset first, then ascii, then latin-1 and finally falling back to
  206. # utf-8.
  207. reply_body = str(results)
  208. for charset in (results.charset, 'us-ascii', 'latin-1'):
  209. try:
  210. reply_body.encode(charset)
  211. break
  212. except UnicodeError:
  213. pass
  214. else:
  215. charset = 'utf-8'
  216. reply.set_payload(reply_body, charset=charset)
  217. reply.send(mlist)