1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

# Copyright 2013 The Distro Tracker Developers 

# See the COPYRIGHT file at the top-level directory of this distribution and 

# at https://deb.li/DTAuthors 

# 

# This file is part of Distro Tracker. It is subject to the license terms 

# in the LICENSE file found in the top-level directory of this 

# distribution and at https://deb.li/DTLicense. No part of Distro Tracker, 

# including this file, may be copied, modified, propagated, or distributed 

# except according to the terms contained in the LICENSE file. 

 

""" 

Module for encoding and decoding Variable Envelope Return Path addresses. 

 

It is implemented following the recommendations laid out in 

`VERP <https://cr.yp.to/proto/verp.txt>`_ and 

`<https://www.courier-mta.org/draft-varshavchik-verp-smtpext.txt>`_ 

 

 

>>> from distro_tracker.core.utils import verp 

 

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

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

 

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

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

""" 

 

__all__ = ('encode', 'decode') 

 

_RETURN_ADDRESS_TEMPLATE = ( 

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

 

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

_ENCODE_MAPPINGS = { 

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

for char in _CHARACTERS 

} 

 

 

def encode(sender_address, recipient_address, separator='-'): 

""" 

Encodes ``sender_address``, ``recipient_address`` to a VERP compliant 

address to be used as the envelope-from (return-path) address. 

 

:param sender_address: The email address of the sender 

:type sender_address: string 

 

:param recipient_address: The email address of the recipient 

:type recipient_address: string 

 

:param separator: The separator to be used between the sender's local 

part and the encoded recipient's local part in the resulting 

VERP address. 

 

:rtype: string 

 

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

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

>>> str(encode('itny-out@domain.com', 'tom@old.example.com')) 

'itny-out-tom=old.example.com@domain.com' 

>>> str(encode('itny-out@domain.com', 'dave+priority@new.example.com')) 

'itny-out-dave+2Bpriority=new.example.com@domain.com' 

 

>>> str(encode('bounce@dom.com', 'user+!%-:@[]+@other.com')) 

'bounce-user+2B+21+25+2D+3A+40+5B+5D+2B=other.com@dom.com' 

""" 

# Split the addresses in two parts based on the last occurrence of '@' 

slocal, sdomain = sender_address.rsplit('@', 1) 

rlocal, rdomain = recipient_address.rsplit('@', 1) 

# Encode recipient parts by replacing relevant characters 

encoderlocal, encoderdomain = map(_encode_chars, (rlocal, rdomain)) 

# Putting it all together 

return _RETURN_ADDRESS_TEMPLATE.format(slocal=slocal, 

separator=separator, 

encoderlocal=encoderlocal, 

encoderdomain=encoderdomain, 

sdomain=sdomain) 

 

 

def decode(verp_address, separator='-'): 

""" 

Decodes the given VERP encoded from address and returns the original 

sender address and recipient address, returning them as a tuple. 

 

:param verp_address: The return path address 

:type sender_address: string 

 

:param separator: The separator to be expected between the sender's local 

part and the encoded recipient's local part in the given 

``verp_address`` 

 

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

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

True 

 

>>> map(str, decode('itny-out-dave+2Bpriority=new.example.com@domain.com')) 

['itny-out@domain.com', 'dave+priority@new.example.com'] 

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

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

>>> map(str, decode('bounce-addr+2B40=dom.com@asdf.com')) 

['bounce@asdf.com', 'addr+40@dom.com'] 

 

>>> s = 'bounce-user+2B+21+25+2D+3A+40+5B+5D+2B=other.com@dom.com' 

>>> str(decode(s)[1]) 

'user+!%-:@[]+@other.com' 

""" 

left_part, sdomain = verp_address.rsplit('@', 1) 

left_part, encodedrdomain = left_part.rsplit('=', 1) 

slocal, encodedrlocal = left_part.rsplit(separator, 1) 

rlocal, rdomain = map(_decode_chars, (encodedrlocal, encodedrdomain)) 

 

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

 

 

def _encode_chars(address): 

""" 

Helper function to replace the special characters in the recipient's 

address. 

""" 

return ''.join(_ENCODE_MAPPINGS.get(char, char) for char in address) 

 

 

def _decode_chars(address): 

""" 

Helper function to replace the encoded special characters with their 

regular character representation. 

""" 

for char in _CHARACTERS: 

address = address.replace(_ENCODE_MAPPINGS[char], char) 

address = address.replace(_ENCODE_MAPPINGS[char].lower(), char) 

return address 

 

 

if __name__ == '__main__': 

import doctest 

doctest.testmod()