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. 

10 

11""" 

12Module for encoding and decoding Variable Envelope Return Path addresses. 

13 

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>`_ 

17 

18 

19>>> from distro_tracker.core.utils import verp 

20 

21>>> str(verp.encode('itny-out@domain.com', 'node42!ann@old.example.com')) 

22'itny-out-node42+21ann=old.example.com@domain.com' 

23 

24>>> map(str, decode('itny-out-node42+21ann=old.example.com@domain.com')) 

25['itny-out@domain.com', 'node42!ann@old.example.com'] 

26""" 

27 

28__all__ = ('encode', 'decode') 

29 

30_RETURN_ADDRESS_TEMPLATE = ( 

31 '{slocal}{separator}{encoderlocal}={encoderdomain}@{sdomain}') 

32 

33_CHARACTERS = ('@', ':', '%', '!', '-', '[', ']', '+') 

34_ENCODE_MAPPINGS = { 

35 char: '+{val:0X}'.format(val=ord(char)) 

36 for char in _CHARACTERS 

37} 

38 

39 

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. 

44 

45 :param sender_address: The email address of the sender 

46 :type sender_address: string 

47 

48 :param recipient_address: The email address of the recipient 

49 :type recipient_address: string 

50 

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. 

54 

55 :rtype: string 

56 

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' 

63 

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) 

78 

79 

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. 

84 

85 :param verp_address: The return path address 

86 :type sender_address: string 

87 

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`` 

91 

92 >>> from_email, to_email = 'bounce@domain.com', 'user@other.com' 

93 >>> decode(encode(from_email, to_email)) == (from_email, to_email) 

94 True 

95 

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'] 

102 

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)) 

111 

112 return (slocal + '@' + sdomain, rlocal + '@' + rdomain) 

113 

114 

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) 

121 

122 

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 

132 

133 

134if __name__ == '__main__': 

135 import doctest 

136 doctest.testmod()