PageRenderTime 49ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/abusehelper/core/mailer.py

https://bitbucket.org/clarifiednetworks/abusehelper/
Python | 371 lines | 328 code | 42 blank | 1 comment | 40 complexity | f76bd0a5fd0b8a7dbdf242b2d6ecad96 MD5 | raw file
Possible License(s): MIT
  1. import time
  2. import socket
  3. import getpass
  4. import smtplib
  5. import collections
  6. from idiokit import threado, timer
  7. from abusehelper.core import events, taskfarm, services, templates, bot
  8. @threado.stream
  9. def wait(inner, amount):
  10. sleeper = timer.sleep(amount)
  11. while not sleeper.has_result():
  12. yield inner, sleeper
  13. def next_time(time_string):
  14. try:
  15. parsed = list(time.strptime(time_string, "%H:%M"))
  16. except (TypeError, ValueError):
  17. return float(time_string)
  18. now = time.localtime()
  19. current = list(now)
  20. current[3:6] = parsed[3:6]
  21. current_time = time.time()
  22. delta = time.mktime(current) - current_time
  23. if delta <= 0.0:
  24. current[2] += 1
  25. return time.mktime(current) - current_time
  26. return delta
  27. @threado.stream
  28. def alert(inner, *times):
  29. while True:
  30. if times:
  31. sleeper = timer.sleep(min(map(next_time, times)))
  32. else:
  33. sleeper = threado.Channel()
  34. while not sleeper.has_result():
  35. try:
  36. yield inner, sleeper
  37. except:
  38. sleeper.rethrow()
  39. raise
  40. inner.send()
  41. class ReportBot(bot.ServiceBot):
  42. REPORT_NOW = object()
  43. def __init__(self, *args, **keys):
  44. bot.ServiceBot.__init__(self, *args, **keys)
  45. self.rooms = taskfarm.TaskFarm(self.handle_room)
  46. self.collectors = dict()
  47. self.queue = collections.deque()
  48. @threado.stream
  49. def handle_room(inner, self, name):
  50. self.log.info("Joining room %r", name)
  51. room = yield inner.sub(self.xmpp.muc.join(name, self.bot_name))
  52. self.log.info("Joined room %r", name)
  53. try:
  54. yield inner.sub(room
  55. | events.stanzas_to_events()
  56. | self.distribute(name))
  57. finally:
  58. self.log.info("Left room %r", name)
  59. @threado.stream
  60. def distribute(inner, self, name):
  61. while True:
  62. event = yield inner
  63. collectors = self.collectors.get(name)
  64. for collector in collectors:
  65. collector.send(event)
  66. @threado.stream
  67. def main(inner, self, queue):
  68. if queue:
  69. self.queue.extendleft(queue)
  70. try:
  71. while True:
  72. while self.queue:
  73. item = self.queue.popleft()
  74. success = yield inner.sub(self.report(item))
  75. if not success:
  76. self.queue.append(item)
  77. yield inner.sub(wait(1.0))
  78. except services.Stop:
  79. inner.finish(self.queue)
  80. @threado.stream
  81. def session(inner, self, state, src_room, **keys):
  82. @threado.stream
  83. def _alert(inner):
  84. alert = self.alert(**keys)
  85. try:
  86. while True:
  87. source, item = yield threado.any(inner, alert)
  88. if inner is source:
  89. inner.send(item)
  90. else:
  91. inner.send(self.REPORT_NOW)
  92. except:
  93. alert.rethrow()
  94. @threado.stream
  95. def _collect(inner):
  96. while True:
  97. item = yield inner
  98. self.queue.append(item)
  99. collector = _alert() | self.collect(state, **keys) | _collect()
  100. self.collectors.setdefault(src_room, set()).add(collector)
  101. try:
  102. result = yield inner.sub(collector | self.rooms.inc(src_room))
  103. finally:
  104. collectors = self.collectors.get(src_room, set())
  105. collectors.discard(collector)
  106. if not collectors:
  107. self.collectors.pop(src_room, None)
  108. inner.finish(result)
  109. @threado.stream
  110. def alert(inner, self, times, **keys):
  111. yield inner.sub(alert(*times))
  112. @threado.stream
  113. def collect(inner, self, state, **keys):
  114. if state is None:
  115. state = events.EventCollector()
  116. try:
  117. while True:
  118. event = yield inner
  119. if event is self.REPORT_NOW:
  120. inner.send(state.purge())
  121. else:
  122. state.append(event)
  123. except services.Stop:
  124. inner.finish(state)
  125. @threado.stream
  126. def report(inner, self, collected):
  127. yield
  128. inner.finish(True)
  129. class MailTemplate(templates.Template):
  130. def format(self, events, encoding="utf-8"):
  131. from email import message_from_string
  132. from email.mime.multipart import MIMEMultipart
  133. from email.mime.text import MIMEText
  134. from email.charset import Charset, QP
  135. from email.utils import formatdate, make_msgid
  136. parts = list()
  137. data = templates.Template.format(self, parts, events)
  138. parsed = message_from_string(data.encode(encoding))
  139. charset = Charset(encoding)
  140. charset.header_encoding = QP
  141. msg = MIMEMultipart()
  142. msg.set_charset(charset)
  143. for key, value in msg.items():
  144. del parsed[key]
  145. for key, value in parsed.items():
  146. msg[key] = value
  147. for encoded in ["Subject", "Comment"]:
  148. if encoded not in msg:
  149. continue
  150. value = charset.header_encode(msg[encoded])
  151. del msg[encoded]
  152. msg[encoded] = value
  153. del msg['Content-Transfer-Encoding']
  154. msg['Content-Transfer-Encoding'] = '7bit'
  155. msg.attach(MIMEText(parsed.get_payload(), "plain", encoding))
  156. for part in parts:
  157. msg.attach(part)
  158. return msg
  159. def format_addresses(addrs):
  160. from email.utils import getaddresses, formataddr
  161. # FIXME: Use encoding after getaddresses
  162. return ", ".join(map(formataddr, getaddresses(addrs)))
  163. class MailerService(ReportBot):
  164. TOLERATED_EXCEPTIONS = (socket.error, smtplib.SMTPException)
  165. mail_sender = bot.Param("from whom it looks like the mails came from")
  166. smtp_host = bot.Param("hostname of the SMTP service used for sending mails")
  167. smtp_port = bot.IntParam("port of the SMTP service used for sending mails",
  168. default=25)
  169. smtp_auth_user = bot.Param("username for the authenticated SMTP service",
  170. default=None)
  171. smtp_auth_password = bot.Param("password for the authenticated SMTP "+
  172. "service", default=None)
  173. def __init__(self, **keys):
  174. super(MailerService, self).__init__(**keys)
  175. if self.smtp_auth_user and not self.smtp_auth_password:
  176. self.smtp_auth_password = getpass.getpass("SMTP password: ")
  177. self.server = None
  178. @threado.stream
  179. def build_mail(inner, self, events, template="", to=[], cc=[], **keys):
  180. """
  181. Return a mail object produced based on collected events and
  182. session parameters.
  183. """
  184. csv = templates.CSVFormatter()
  185. template = MailTemplate(template,
  186. csv=csv,
  187. attach_csv=templates.AttachUnicode(csv),
  188. attach_and_embed_csv=templates.AttachAndEmbedUnicode(csv),
  189. to=templates.Const(format_addresses(to)),
  190. cc=templates.Const(format_addresses(cc)))
  191. yield
  192. inner.finish(template.format(events))
  193. def collect(self, state, **keys):
  194. return ReportBot.collect(self, state, **keys) | self._collect(**keys)
  195. @threado.stream
  196. def _collect(inner, self, to=[], cc=[], **keys):
  197. from email.header import decode_header
  198. from email.utils import formatdate, make_msgid, getaddresses, formataddr
  199. # FIXME: Use encoding after getaddresses
  200. from_addr = getaddresses([self.mail_sender])[0]
  201. while True:
  202. events = yield inner
  203. if not events:
  204. continue
  205. msg = yield inner.sub(self.build_mail(events, to=to, cc=cc, **keys))
  206. if "To" not in msg:
  207. msg["To"] = format_addresses(to)
  208. if "Cc" not in msg:
  209. msg["Cc"] = format_addresses(cc)
  210. del msg["From"]
  211. msg["From"] = formataddr(from_addr)
  212. msg["Date"] = formatdate()
  213. msg["Message-ID"] = make_msgid()
  214. subject = msg.get("Subject", "")
  215. msg_data = msg.as_string()
  216. mail_to = msg.get_all("To", list()) + msg.get_all("Cc", list())
  217. mail_to = [addr for (name, addr) in getaddresses(mail_to)]
  218. mail_to = filter(None, [x.strip() for x in mail_to])
  219. for address in mail_to:
  220. inner.send(from_addr[1], address, subject, msg_data)
  221. @threado.stream
  222. def main(inner, self, state):
  223. try:
  224. result = yield inner.sub(ReportBot.main(self, state))
  225. finally:
  226. if self.server is not None:
  227. _, server = self.server
  228. self.server = None
  229. try:
  230. yield inner.thread(server.quit)
  231. except self.TOLERATED_EXCEPTIONS, exc:
  232. pass
  233. inner.finish(result)
  234. @threado.stream
  235. def _ensure_connection(inner, self):
  236. while self.server is None:
  237. host, port = self.smtp_host, self.smtp_port
  238. self.log.info("Connecting %r port %d", host, port)
  239. try:
  240. server = yield inner.thread(smtplib.SMTP, host, port)
  241. except self.TOLERATED_EXCEPTIONS, exc:
  242. self.log.error("Error connecting SMTP server: %r", exc)
  243. else:
  244. self.log.info("Connected to the SMTP server")
  245. self.server = False, server
  246. break
  247. self.log.info("Retrying SMTP connection in 10 seconds")
  248. yield inner.sub(wait(10.0))
  249. def _try_to_authenticate(self, server):
  250. if server.has_extn("starttls"):
  251. server.starttls()
  252. server.ehlo()
  253. if (self.smtp_auth_user is not None and
  254. self.smtp_auth_password is not None and
  255. server.has_extn("auth")):
  256. server.login(self.smtp_auth_user, self.smtp_auth_password)
  257. @threado.stream
  258. def _try_to_send(inner, self, item):
  259. from_addr, to_addr, subject, msg = item
  260. yield inner.sub(self._ensure_connection())
  261. ehlo_done, server = self.server
  262. self.log.info("Sending message %r to %r", subject, to_addr)
  263. try:
  264. if not ehlo_done:
  265. yield inner.thread(server.ehlo)
  266. self.server = True, server
  267. try:
  268. yield inner.thread(server.sendmail, from_addr, to_addr, msg)
  269. except smtplib.SMTPSenderRefused, refused:
  270. if refused.smtp_code != 530:
  271. raise
  272. yield inner.thread(self._try_to_authenticate, server)
  273. yield inner.thread(server.sendmail, from_addr, to_addr, msg)
  274. except smtplib.SMTPDataError, data_error:
  275. self.log.error("Could not send message to %r: %r. "+
  276. "Dropping message from queue.",
  277. to_addr, data_error)
  278. inner.finish(True)
  279. except smtplib.SMTPRecipientsRefused, refused:
  280. for recipient, reason in refused.recipients.iteritems():
  281. self.log.error("Could not send message to %r: %r. "+
  282. "Dropping message from queue.",
  283. recipient, reason)
  284. inner.finish(True)
  285. except self.TOLERATED_EXCEPTIONS, exc:
  286. self.log.error("Could not send message to %r: %r", to_addr, exc)
  287. self.server = None
  288. try:
  289. yield inner.thread(server.quit)
  290. except self.TOLERATED_EXCEPTIONS:
  291. pass
  292. inner.finish(False)
  293. self.log.info("Sent message to %r", to_addr)
  294. inner.finish(True)
  295. @threado.stream
  296. def report(inner, self, item):
  297. while True:
  298. result = yield inner.sub(self._try_to_send(item))
  299. if result:
  300. inner.finish(True)
  301. self.log.info("Retrying sending in 10 seconds")
  302. yield inner.sub(wait(10.0))
  303. if __name__ == "__main__":
  304. MailerService.from_command_line().execute()