1# Copyright 2013 The Distro Tracker Developers
2# See the COPYRIGHT file at the top-level directory of this distribution and
3# at https://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 https://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.
11"""
12Module for encoding and decoding Variable Envelope Return Path addresses.
14It is implemented following the recommendations laid out in
15`VERP <https://cr.yp.to/proto/verp.txt>`_ and
16`<https://www.courier-mta.org/draft-varshavchik-verp-smtpext.txt>`_
19>>> from distro_tracker.core.utils import verp
21>>> str(verp.encode('itny-out@domain.com', 'node42!ann@old.example.com'))
22'itny-out-node42+21ann=old.example.com@domain.com'
24>>> map(str, decode('itny-out-node42+21ann=old.example.com@domain.com'))
25['itny-out@domain.com', 'node42!ann@old.example.com']
26"""
28__all__ = ('encode', 'decode')
30_RETURN_ADDRESS_TEMPLATE = (
31 '{slocal}{separator}{encoderlocal}={encoderdomain}@{sdomain}')
33_CHARACTERS = ('@', ':', '%', '!', '-', '[', ']', '+')
34_ENCODE_MAPPINGS = {
35 char: '+{val:0X}'.format(val=ord(char))
36 for char in _CHARACTERS
37}
40def encode(sender_address, recipient_address, separator='-'):
41 """
42 Encodes ``sender_address``, ``recipient_address`` to a VERP compliant
43 address to be used as the envelope-from (return-path) address.
45 :param sender_address: The email address of the sender
46 :type sender_address: string
48 :param recipient_address: The email address of the recipient
49 :type recipient_address: string
51 :param separator: The separator to be used between the sender's local
52 part and the encoded recipient's local part in the resulting
53 VERP address.
55 :rtype: string
57 >>> str(encode('itny-out@domain.com', 'node42!ann@old.example.com'))
58 'itny-out-node42+21ann=old.example.com@domain.com'
59 >>> str(encode('itny-out@domain.com', 'tom@old.example.com'))
60 'itny-out-tom=old.example.com@domain.com'
61 >>> str(encode('itny-out@domain.com', 'dave+priority@new.example.com'))
62 'itny-out-dave+2Bpriority=new.example.com@domain.com'
64 >>> str(encode('bounce@dom.com', 'user+!%-:@[]+@other.com'))
65 'bounce-user+2B+21+25+2D+3A+40+5B+5D+2B=other.com@dom.com'
66 """
67 # Split the addresses in two parts based on the last occurrence of '@'
68 slocal, sdomain = sender_address.rsplit('@', 1)
69 rlocal, rdomain = recipient_address.rsplit('@', 1)
70 # Encode recipient parts by replacing relevant characters
71 encoderlocal, encoderdomain = map(_encode_chars, (rlocal, rdomain))
72 # Putting it all together
73 return _RETURN_ADDRESS_TEMPLATE.format(slocal=slocal,
74 separator=separator,
75 encoderlocal=encoderlocal,
76 encoderdomain=encoderdomain,
77 sdomain=sdomain)
80def decode(verp_address, separator='-'):
81 """
82 Decodes the given VERP encoded from address and returns the original
83 sender address and recipient address, returning them as a tuple.
85 :param verp_address: The return path address
86 :type sender_address: string
88 :param separator: The separator to be expected between the sender's local
89 part and the encoded recipient's local part in the given
90 ``verp_address``
92 >>> from_email, to_email = 'bounce@domain.com', 'user@other.com'
93 >>> decode(encode(from_email, to_email)) == (from_email, to_email)
94 True
96 >>> map(str, decode('itny-out-dave+2Bpriority=new.example.com@domain.com'))
97 ['itny-out@domain.com', 'dave+priority@new.example.com']
98 >>> map(str, decode('itny-out-node42+21ann=old.example.com@domain.com'))
99 ['itny-out@domain.com', 'node42!ann@old.example.com']
100 >>> map(str, decode('bounce-addr+2B40=dom.com@asdf.com'))
101 ['bounce@asdf.com', 'addr+40@dom.com']
103 >>> s = 'bounce-user+2B+21+25+2D+3A+40+5B+5D+2B=other.com@dom.com'
104 >>> str(decode(s)[1])
105 'user+!%-:@[]+@other.com'
106 """
107 left_part, sdomain = verp_address.rsplit('@', 1)
108 left_part, encodedrdomain = left_part.rsplit('=', 1)
109 slocal, encodedrlocal = left_part.rsplit(separator, 1)
110 rlocal, rdomain = map(_decode_chars, (encodedrlocal, encodedrdomain))
112 return (slocal + '@' + sdomain, rlocal + '@' + rdomain)
115def _encode_chars(address):
116 """
117 Helper function to replace the special characters in the recipient's
118 address.
119 """
120 return ''.join(_ENCODE_MAPPINGS.get(char, char) for char in address)
123def _decode_chars(address):
124 """
125 Helper function to replace the encoded special characters with their
126 regular character representation.
127 """
128 for char in _CHARACTERS:
129 address = address.replace(_ENCODE_MAPPINGS[char], char)
130 address = address.replace(_ENCODE_MAPPINGS[char].lower(), char)
131 return address
134if __name__ == '__main__':
135 import doctest
136 doctest.testmod()