A simple ticketing application written in Python/Django
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

get_email.py 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. """
  2. This file was derived from:
  3. https://github.com/rossp/django-helpdesk/blob/master/helpdesk/management/commands/get_email.py
  4. Copyright notice for that original file:
  5. Copyright (c) 2008, Ross Poulton (Trading as Jutda)
  6. All rights reserved.
  7. Redistribution and use in source and binary forms, with or without modification,
  8. are permitted provided that the following conditions are met:
  9. 1. Redistributions of source code must retain the above copyright
  10. notice, this list of conditions and the following disclaimer.
  11. 2. Redistributions in binary form must reproduce the above copyright
  12. notice, this list of conditions and the following disclaimer in the
  13. documentation and/or other materials provided with the distribution.
  14. 3. Neither the name of Ross Poulton, Jutda, nor the names of any
  15. of its contributors may be used to endorse or promote products
  16. derived from this software without specific prior written permission.
  17. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  18. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19. WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  21. FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  22. DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  23. SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  24. CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  25. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  26. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. """
  28. import email
  29. import imaplib
  30. import mimetypes
  31. import re
  32. import os
  33. from email.header import decode_header
  34. from email.utils import parseaddr, collapse_rfc2231_value
  35. from optparse import make_option
  36. from email_reply_parser import EmailReplyParser
  37. from django.core.files.base import ContentFile
  38. from django.core.management.base import BaseCommand
  39. from django.contrib.auth.models import User
  40. try:
  41. from django.utils import timezone
  42. except ImportError:
  43. from datetime import datetime as timezone
  44. from main.models import Ticket, Attachment, FollowUp
  45. class Command(BaseCommand):
  46. def __init__(self):
  47. BaseCommand.__init__(self)
  48. self.option_list += (
  49. make_option(
  50. '--quiet', '-q',
  51. default=False,
  52. action='store_true',
  53. help='Hide details about each message as they are processed.'),
  54. )
  55. help = 'Process email inbox and create tickets.'
  56. def handle(self, *args, **options):
  57. quiet = options.get('quiet', False)
  58. process_inbox(quiet=quiet)
  59. def process_inbox(quiet=False):
  60. """
  61. Process IMAP inbox
  62. """
  63. server = imaplib.IMAP4_SSL(os.environ["DJANGO_TICKET_INBOX_SERVER"], 993)
  64. server.login(os.environ["DJANGO_TICKET_INBOX_USER"], os.environ["DJANGO_TICKET_INBOX_PASSWORD"])
  65. server.select("INBOX")
  66. status, data = server.search(None, 'NOT', 'DELETED')
  67. if data:
  68. msgnums = data[0].split()
  69. for num in msgnums:
  70. status, data = server.fetch(num, '(RFC822)')
  71. ticket = ticket_from_message(message=data[0][1], quiet=quiet)
  72. if ticket:
  73. server.store(num, '+FLAGS', '\\Deleted')
  74. server.expunge()
  75. server.close()
  76. server.logout()
  77. def decodeUnknown(charset, string):
  78. if not charset:
  79. try:
  80. return string.decode('utf-8', 'ignore')
  81. except:
  82. return string.decode('iso8859-1', 'ignore')
  83. return unicode(string, charset)
  84. def decode_mail_headers(string):
  85. decoded = decode_header(string)
  86. return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded])
  87. def ticket_from_message(message, quiet):
  88. """
  89. Create a ticket or a followup (if ticket id in subject)
  90. """
  91. msg = message
  92. message = email.message_from_string(msg)
  93. subject = message.get('subject', 'Created from e-mail')
  94. subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject))
  95. sender = message.get('from', ('Unknown Sender'))
  96. sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender))
  97. sender_email = parseaddr(sender)[1]
  98. body_plain, body_html = '', ''
  99. matchobj = re.match(r".*\["+"-(?P<id>\d+)\]", subject)
  100. if matchobj:
  101. # This is a reply or forward.
  102. ticket = matchobj.group('id')
  103. else:
  104. ticket = None
  105. counter = 0
  106. files = []
  107. for part in message.walk():
  108. if part.get_content_maintype() == 'multipart':
  109. continue
  110. name = part.get_param("name")
  111. if name:
  112. name = collapse_rfc2231_value(name)
  113. if part.get_content_maintype() == 'text' and name == None:
  114. if part.get_content_subtype() == 'plain':
  115. body_plain = EmailReplyParser.parse_reply(decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)))
  116. else:
  117. body_html = part.get_payload(decode=True)
  118. else:
  119. if not name:
  120. ext = mimetypes.guess_extension(part.get_content_type())
  121. name = "part-%i%s" % (counter, ext)
  122. files.append({
  123. 'filename': name,
  124. 'content': part.get_payload(decode=True),
  125. 'type': part.get_content_type()},
  126. )
  127. counter += 1
  128. if body_plain:
  129. body = body_plain
  130. else:
  131. body = 'No plain-text email body available. Please see attachment email_html_body.html.'
  132. if body_html:
  133. files.append({
  134. 'filename': 'email_html_body.html',
  135. 'content': body_html,
  136. 'type': 'text/html',
  137. })
  138. now = timezone.now()
  139. if ticket:
  140. try:
  141. t = Ticket.objects.get(id=ticket)
  142. new = False
  143. except Ticket.DoesNotExist:
  144. ticket = None
  145. if ticket == None:
  146. # set owner depending on sender_email
  147. # list of all email addresses from the user model
  148. users = User.objects.all()
  149. email_addresses = []
  150. for user in users:
  151. email_addresses.append(user.email)
  152. ############################################################
  153. # if ticket id in subject => new followup instead of new ticket
  154. tickets = Ticket.objects.all()
  155. ticket_ids = []
  156. for ticket in tickets:
  157. ticket_ids.append(ticket.id)
  158. # extract id from subject
  159. subject_id = re.search(r'\[#(\d*)\]\s.*', subject)
  160. try:
  161. subject_id = subject_id.group(1)
  162. except:
  163. subject_id = "0000" # no valid id
  164. # if there was an ID in the subject, create followup
  165. if int(subject_id) in ticket_ids:
  166. if sender_email in email_addresses:
  167. f = FollowUp(
  168. title=subject,
  169. created=now,
  170. text=body,
  171. ticket=Ticket.objects.get(id=subject_id),
  172. user=User.objects.get(email=sender_email),
  173. )
  174. else:
  175. f = FollowUp(
  176. title=subject,
  177. created=now,
  178. text=body,
  179. ticket=Ticket.objects.get(id=subject_id),
  180. )
  181. f.save()
  182. # if no ID in the subject, create ticket
  183. else:
  184. # if known sender, set also the field owner
  185. if sender_email in email_addresses:
  186. t = Ticket(
  187. title=subject,
  188. status="TODO",
  189. created=now,
  190. description=body,
  191. owner=User.objects.get(email=sender_email),
  192. )
  193. # if unknown sender, skip the field owner
  194. else:
  195. t = Ticket(
  196. title=subject,
  197. status="TODO",
  198. created=now,
  199. description=body,
  200. )
  201. t.save()
  202. from django.core.mail import send_mail
  203. notification_subject = "[#" + str(t.id) + "] New ticket created"
  204. notification_body = "Hi,\n\na new ticket was created: http://localhost:8000/ticket/" \
  205. + str(t.id) + "/"
  206. send_mail(notification_subject, notification_body, os.environ["DJANGO_TICKET_EMAIL_NOTIFICATIONS_FROM"],
  207. [os.environ["DJANGO_TICKET_EMAIL_NOTIFICATIONS_TO"]], fail_silently=False)
  208. ############################################################
  209. new = True
  210. update = ''
  211. elif t.status == Ticket.CLOSED_STATUS:
  212. t.status = Ticket.REOPENED_STATUS
  213. t.save()
  214. # files of followups should be assigned to the corresponding ticket
  215. for file in files:
  216. if file['content']:
  217. filename = file['filename'].encode('ascii', 'replace').replace(' ', '_')
  218. filename = re.sub('[^a-zA-Z0-9._-]+', '', filename)
  219. # if followup
  220. if int(subject_id) in ticket_ids:
  221. a = Attachment(
  222. ticket=Ticket.objects.get(id=subject_id),
  223. filename=filename,
  224. #mime_type=file['type'],
  225. #size=len(file['content']),
  226. )
  227. # if new ticket
  228. else:
  229. a = Attachment(
  230. ticket=t,
  231. filename=filename,
  232. #mime_type=file['type'],
  233. #size=len(file['content']),
  234. )
  235. a.file.save(filename, ContentFile(file['content']), save=False)
  236. a.save()
  237. if not quiet:
  238. print " - %s" % filename
  239. if int(subject_id) in ticket_ids:
  240. return f
  241. else:
  242. return t
  243. if __name__ == '__main__':
  244. process_email()