verp.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. # Copyright 2013 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://deb.li/DTAuthors
  4. #
  5. # This file is part of Distro Tracker. It is subject to the license terms
  6. # in the LICENSE file found in the top-level directory of this
  7. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  8. # including this file, may be copied, modified, propagated, or distributed
  9. # except according to the terms contained in the LICENSE file.
  10. """
  11. Module for encoding and decoding Variable Envelope Return Path addresses.
  12. It is implemented following the recommendations laid out in
  13. `VERP <http://cr.yp.to/proto/verp.txt>`_ and
  14. `<http://www.courier-mta.org/draft-varshavchik-verp-smtpext.txt>`_
  15. >>> from distro_tracker.core.utils import verp
  16. >>> str(verp.encode('itny-out@domain.com', 'node42!ann@old.example.com'))
  17. 'itny-out-node42+21ann=old.example.com@domain.com'
  18. >>> map(str, decode('itny-out-node42+21ann=old.example.com@domain.com'))
  19. ['itny-out@domain.com', 'node42!ann@old.example.com']
  20. """
  21. from __future__ import unicode_literals
  22. __all__ = ('encode', 'decode')
  23. _RETURN_ADDRESS_TEMPLATE = (
  24. '{slocal}{separator}{encoderlocal}={encoderdomain}@{sdomain}')
  25. _CHARACTERS = ('@', ':', '%', '!', '-', '[', ']', '+')
  26. _ENCODE_MAPPINGS = {
  27. char: '+{val:0X}'.format(val=ord(char))
  28. for char in _CHARACTERS
  29. }
  30. def encode(sender_address, recipient_address, separator='-'):
  31. """
  32. Encodes ``sender_address``, ``recipient_address`` to a VERP compliant
  33. address to be used as the envelope-from (return-path) address.
  34. :param sender_address: The email address of the sender
  35. :type sender_address: string
  36. :param recipient_address: The email address of the recipient
  37. :type recipient_address: string
  38. :param separator: The separator to be used between the sender's local
  39. part and the encoded recipient's local part in the resulting
  40. VERP address.
  41. :rtype: string
  42. >>> str(encode('itny-out@domain.com', 'node42!ann@old.example.com'))
  43. 'itny-out-node42+21ann=old.example.com@domain.com'
  44. >>> str(encode('itny-out@domain.com', 'tom@old.example.com'))
  45. 'itny-out-tom=old.example.com@domain.com'
  46. >>> str(encode('itny-out@domain.com', 'dave+priority@new.example.com'))
  47. 'itny-out-dave+2Bpriority=new.example.com@domain.com'
  48. >>> str(encode('bounce@dom.com', 'user+!%-:@[]+@other.com'))
  49. 'bounce-user+2B+21+25+2D+3A+40+5B+5D+2B=other.com@dom.com'
  50. """
  51. # Split the addresses in two parts based on the last occurrence of '@'
  52. slocal, sdomain = sender_address.rsplit('@', 1)
  53. rlocal, rdomain = recipient_address.rsplit('@', 1)
  54. # Encode recipient parts by replacing relevant characters
  55. encoderlocal, encoderdomain = map(_encode_chars, (rlocal, rdomain))
  56. # Putting it all together
  57. return _RETURN_ADDRESS_TEMPLATE.format(slocal=slocal,
  58. separator=separator,
  59. encoderlocal=encoderlocal,
  60. encoderdomain=encoderdomain,
  61. sdomain=sdomain)
  62. def decode(verp_address, separator='-'):
  63. """
  64. Decodes the given VERP encoded from address and returns the original
  65. sender address and recipient address, returning them as a tuple.
  66. :param verp_address: The return path address
  67. :type sender_address: string
  68. :param separator: The separator to be expected between the sender's local
  69. part and the encoded recipient's local part in the given
  70. ``verp_address``
  71. >>> from_email, to_email = 'bounce@domain.com', 'user@other.com'
  72. >>> decode(encode(from_email, to_email)) == (from_email, to_email)
  73. True
  74. >>> map(str, decode('itny-out-dave+2Bpriority=new.example.com@domain.com'))
  75. ['itny-out@domain.com', 'dave+priority@new.example.com']
  76. >>> map(str, decode('itny-out-node42+21ann=old.example.com@domain.com'))
  77. ['itny-out@domain.com', 'node42!ann@old.example.com']
  78. >>> map(str, decode('bounce-addr+2B40=dom.com@asdf.com'))
  79. ['bounce@asdf.com', 'addr+40@dom.com']
  80. >>> s = 'bounce-user+2B+21+25+2D+3A+40+5B+5D+2B=other.com@dom.com'
  81. >>> str(decode(s)[1])
  82. 'user+!%-:@[]+@other.com'
  83. """
  84. left_part, sdomain = verp_address.rsplit('@', 1)
  85. left_part, encodedrdomain = left_part.rsplit('=', 1)
  86. slocal, encodedrlocal = left_part.rsplit(separator, 1)
  87. rlocal, rdomain = map(_decode_chars, (encodedrlocal, encodedrdomain))
  88. return (slocal + '@' + sdomain, rlocal + '@' + rdomain)
  89. def _encode_chars(address):
  90. """
  91. Helper function to replace the special characters in the recipient's
  92. address.
  93. """
  94. return ''.join(_ENCODE_MAPPINGS.get(char, char) for char in address)
  95. def _decode_chars(address):
  96. """
  97. Helper function to replace the encoded special characters with their
  98. regular character representation.
  99. """
  100. for char in _CHARACTERS:
  101. address = address.replace(_ENCODE_MAPPINGS[char], char)
  102. address = address.replace(_ENCODE_MAPPINGS[char].lower(), char)
  103. return address
  104. if __name__ == '__main__':
  105. import doctest
  106. doctest.testmod()