bounce.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. # -*- test-case-name: twisted.mail.test.test_bounce -*-
  2. #
  3. # Copyright (c) Twisted Matrix Laboratories.
  4. # See LICENSE for details.
  5. """
  6. Support for bounce message generation.
  7. """
  8. import email.utils
  9. import time
  10. import os
  11. from io import StringIO, SEEK_SET, SEEK_END
  12. from twisted.mail import smtp
  13. BOUNCE_FORMAT = u"""\
  14. From: postmaster@{failedDomain}
  15. To: {failedFrom}
  16. Subject: Returned Mail: see transcript for details
  17. Message-ID: {messageID}
  18. Content-Type: multipart/report; report-type=delivery-status;
  19. boundary="{boundary}"
  20. --{boundary}
  21. {transcript}
  22. --{boundary}
  23. Content-Type: message/delivery-status
  24. Arrival-Date: {ctime}
  25. Final-Recipient: RFC822; {failedTo}
  26. """
  27. def generateBounce(message, failedFrom, failedTo, transcript='',
  28. encoding='utf-8'):
  29. """
  30. Generate a bounce message for an undeliverable email message.
  31. @type message: a file-like object
  32. @param message: The undeliverable message.
  33. @type failedFrom: L{bytes} or L{unicode}
  34. @param failedFrom: The originator of the undeliverable message.
  35. @type failedTo: L{bytes} or L{unicode}
  36. @param failedTo: The destination of the undeliverable message.
  37. @type transcript: L{bytes} or L{unicode}
  38. @param transcript: An error message to include in the bounce message.
  39. @type encoding: L{str} or L{unicode}
  40. @param encoding: Encoding to use, default: utf-8
  41. @rtype: 3-L{tuple} of (E{1}) L{bytes}, (E{2}) L{bytes}, (E{3}) L{bytes}
  42. @return: The originator, the destination and the contents of the bounce
  43. message. The destination of the bounce message is the originator of
  44. the undeliverable message.
  45. """
  46. if isinstance(failedFrom, bytes):
  47. failedFrom = failedFrom.decode(encoding)
  48. if isinstance(failedTo, bytes):
  49. failedTo = failedTo.decode(encoding)
  50. if not transcript:
  51. transcript = u'''\
  52. I'm sorry, the following address has permanent errors: {failedTo}.
  53. I've given up, and I will not retry the message again.
  54. '''.format(failedTo=failedTo)
  55. failedAddress = email.utils.parseaddr(failedTo)[1]
  56. data = {
  57. 'boundary': "{}_{}_{}".format(time.time(), os.getpid(), 'XXXXX'),
  58. 'ctime': time.ctime(time.time()),
  59. 'failedAddress': failedAddress,
  60. 'failedDomain': failedAddress.split('@', 1)[1],
  61. 'failedFrom': failedFrom,
  62. 'failedTo': failedTo,
  63. 'messageID': smtp.messageid(uniq='bounce'),
  64. 'message': message,
  65. 'transcript': transcript,
  66. }
  67. fp = StringIO()
  68. fp.write(BOUNCE_FORMAT.format(**data))
  69. orig = message.tell()
  70. message.seek(0, SEEK_END)
  71. sz = message.tell()
  72. message.seek(orig, SEEK_SET)
  73. if sz > 10000:
  74. while 1:
  75. line = message.readline()
  76. if isinstance(line, bytes):
  77. line = line.decode(encoding)
  78. if len(line) <= 0:
  79. break
  80. fp.write(line)
  81. else:
  82. messageContent = message.read()
  83. if isinstance(messageContent, bytes):
  84. messageContent = messageContent.decode(encoding)
  85. fp.write(messageContent)
  86. return b'', failedFrom.encode(encoding), fp.getvalue().encode(encoding)