1
0

comment_email_milter.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. # Milter calls methods of your class at milter events.
  4. # Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
  5. # You can also add/del recipients, replacebody, add/del headers, etc.
  6. import base64
  7. import email
  8. import hashlib
  9. import os
  10. import urlparse
  11. import StringIO
  12. import sys
  13. import time
  14. from socket import AF_INET, AF_INET6
  15. from multiprocessing import Process as Thread, Queue
  16. import Milter
  17. import requests
  18. from Milter.utils import parse_addr
  19. logq = Queue(maxsize=4)
  20. if 'PAGURE_CONFIG' not in os.environ \
  21. and os.path.exists('/etc/pagure/pagure.cfg'):
  22. os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
  23. import pagure
  24. def get_email_body(emailobj):
  25. ''' Return the body of the email, preferably in text.
  26. '''
  27. body = None
  28. if emailobj.is_multipart():
  29. for payload in emailobj.get_payload():
  30. body = payload.get_payload()
  31. if payload.get_content_type() == 'text/plain':
  32. break
  33. else:
  34. body = emailobj.get_payload()
  35. enc = emailobj['Content-Transfer-Encoding']
  36. if enc == 'base64':
  37. body = base64.decodestring(body)
  38. return body
  39. def clean_item(item):
  40. ''' For an item provided as <item> return the content, if there are no
  41. <> then return the string.
  42. '''
  43. if '<' in item:
  44. item = item.split('<')[1]
  45. if '>' in item:
  46. item = item.split('>')[0]
  47. return item
  48. class PagureMilter(Milter.Base):
  49. def __init__(self): # A new instance with each new connection.
  50. self.id = Milter.uniqueID() # Integer incremented with each call.
  51. self.fp = None
  52. def log(self, message):
  53. print(message)
  54. sys.stdout.flush()
  55. def envfrom(self, mailfrom, *str):
  56. self.log("mail from: %s - %s" % (mailfrom, str))
  57. self.fromparms = Milter.dictfromlist(str)
  58. # NOTE: self.fp is only an *internal* copy of message data. You
  59. # must use addheader, chgheader, replacebody to change the message
  60. # on the MTA.
  61. self.fp = StringIO.StringIO()
  62. self.canon_from = '@'.join(parse_addr(mailfrom))
  63. self.fp.write('From %s %s\n' % (self.canon_from, time.ctime()))
  64. return Milter.CONTINUE
  65. @Milter.noreply
  66. def header(self, name, hval):
  67. ''' Headers '''
  68. # add header to buffer
  69. self.fp.write("%s: %s\n" % (name, hval))
  70. return Milter.CONTINUE
  71. @Milter.noreply
  72. def eoh(self):
  73. ''' End of Headers '''
  74. self.fp.write("\n")
  75. return Milter.CONTINUE
  76. @Milter.noreply
  77. def body(self, chunk):
  78. ''' Body '''
  79. self.fp.write(chunk)
  80. return Milter.CONTINUE
  81. @Milter.noreply
  82. def envrcpt(self, to, *str):
  83. rcptinfo = to, Milter.dictfromlist(str)
  84. print rcptinfo
  85. return Milter.CONTINUE
  86. def eom(self):
  87. ''' End of Message '''
  88. self.fp.seek(0)
  89. msg = email.message_from_file(self.fp)
  90. msg_id = msg.get('In-Reply-To', None)
  91. if msg_id is None:
  92. self.log('No In-Reply-To, keep going')
  93. return Milter.CONTINUE
  94. # Ensure we don't get extra lines in the message-id
  95. msg_id = msg_id.split('\n')[0].strip()
  96. self.log('msg-ig %s' % msg_id)
  97. self.log('To %s' % msg['to'])
  98. self.log('Cc %s' % msg.get('cc'))
  99. self.log('From %s' % msg['From'])
  100. # Ensure the user replied to his/her own notification, not that
  101. # they are trying to forge their ID into someone else's
  102. salt = pagure.APP.config.get('SALT_EMAIL')
  103. m = hashlib.sha512('%s%s%s' % (msg_id, salt, clean_item(msg['From'])))
  104. email_address = msg['to']
  105. if 'reply+' in msg.get('cc', ''):
  106. email_address = msg['cc']
  107. if not 'reply+' in email_address:
  108. self.log(
  109. 'No valid recipient email found in To/Cc: %s'
  110. % email_address)
  111. tohash = email_address.split('@')[0].split('+')[-1]
  112. if m.hexdigest() != tohash:
  113. self.log('hash: %s' % m.hexdigest())
  114. self.log('tohash: %s' % tohash)
  115. self.log('Hash does not correspond to the destination')
  116. return Milter.CONTINUE
  117. if msg['From'] and msg['From'] == pagure.APP.config.get('FROM_EMAIL'):
  118. self.log("Let's not process the email we send")
  119. return Milter.CONTINUE
  120. msg_id = clean_item(msg_id)
  121. if msg_id and '-ticket-' in msg_id:
  122. self.log('Processing issue')
  123. return self.handle_ticket_email(msg, msg_id)
  124. elif msg_id and '-pull-request-' in msg_id:
  125. self.log('Processing pull-request')
  126. return self.handle_request_email(msg, msg_id)
  127. else:
  128. self.log('Not a pagure ticket or pull-request email, let it go')
  129. return Milter.CONTINUE
  130. def handle_ticket_email(self, emailobj, msg_id):
  131. ''' Add the email as a comment on a ticket. '''
  132. uid = msg_id.split('-ticket-')[-1].split('@')[0]
  133. parent_id = None
  134. if '-' in uid:
  135. uid, parent_id = uid.rsplit('-', 1)
  136. if '/' in uid:
  137. uid = uid.split('/')[0]
  138. self.log('uid %s' % uid)
  139. self.log('parent_id %s' % parent_id)
  140. data = {
  141. 'objid': uid,
  142. 'comment': get_email_body(emailobj),
  143. 'useremail': clean_item(emailobj['From']),
  144. }
  145. url = pagure.APP.config.get('APP_URL')
  146. if url.endswith('/'):
  147. url = url[:-1]
  148. url = '%s/pv/ticket/comment/' % url
  149. req = requests.put(url, data=data)
  150. if req.status_code == 200:
  151. self.log('Comment added')
  152. return Milter.ACCEPT
  153. self.log('Could not add the comment to pagure')
  154. return Milter.CONTINUE
  155. def handle_request_email(self, emailobj, msg_id):
  156. ''' Add the email as a comment on a request. '''
  157. uid = msg_id.split('-pull-request-')[-1].split('@')[0]
  158. parent_id = None
  159. if '-' in uid:
  160. uid, parent_id = uid.rsplit('-', 1)
  161. if '/' in uid:
  162. uid = uid.split('/')[0]
  163. self.log('uid %s' % uid)
  164. self.log('parent_id %s' % parent_id)
  165. data = {
  166. 'objid': uid,
  167. 'comment': get_email_body(emailobj),
  168. 'useremail': clean_item(emailobj['From']),
  169. }
  170. url = pagure.APP.config.get('APP_URL')
  171. if url.endswith('/'):
  172. url = url[:-1]
  173. url = '%s/pv/pull-request/comment/' % url
  174. req = requests.put(url, data=data)
  175. return Milter.ACCEPT
  176. def background():
  177. while True:
  178. t = logq.get()
  179. if not t: break
  180. msg,id,ts = t
  181. print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
  182. # 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
  183. for i in msg: print i,
  184. print
  185. def main():
  186. bt = Thread(target=background)
  187. bt.start()
  188. socketname = "/var/run/pagure/paguresock"
  189. timeout = 600
  190. # Register to have the Milter factory create instances of your class:
  191. Milter.factory = PagureMilter
  192. print "%s pagure milter startup" % time.strftime('%Y%b%d %H:%M:%S')
  193. sys.stdout.flush()
  194. Milter.runmilter("paguremilter", socketname, timeout)
  195. logq.put(None)
  196. bt.join()
  197. print "%s pagure milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
  198. if __name__ == "__main__":
  199. main()