[Amavisd-new-commits] [pkg-amavisd-new] 10/16: Imported Upstream version 2.10.0
Alexander Wirt
formorer at debian.org
Sun Oct 26 07:38:35 UTC 2014
This is an automated email from the git hooks/post-receive script.
formorer pushed a commit to branch master
in repository pkg-amavisd-new.
commit ab9fdf05f077db32bef5f498e0ebc32c2d40aec7
Author: Alexander Wirt <formorer at debian.org>
Date: Wed Oct 22 20:50:48 2014 +0200
Imported Upstream version 2.10.0
---
LDAP.ldif | 337 ++--
LDAP.schema | 2 +-
README_FILES/README.customize | 80 +-
RELEASE_NOTES | 203 +++
amavisd | 3731 +++++++++++++++++++++++++++--------------
amavisd-new-courier.patch | 22 +-
amavisd-new-qmqpqq.patch | 35 +-
amavisd.conf | 3 +-
amavisd.conf-default | 29 +-
9 files changed, 2996 insertions(+), 1446 deletions(-)
diff --git a/LDAP.ldif b/LDAP.ldif
index a0b89a4..c3ccaa6 100644
--- a/LDAP.ldif
+++ b/LDAP.ldif
@@ -29,182 +29,185 @@
# Attribute Types
#-----------------
#
-# DO NOT EDIT!! Use ldapmodify.
+# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
dn: cn=amavisd,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: amavisd
-olcAttributeTypes: {0}( 1.3.6.1.4.1.15312.2.2.1.1 NAME 'amavisVirusLover' DESC
- 'Virus Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SING
- LE-VALUE )
-olcAttributeTypes: {1}( 1.3.6.1.4.1.15312.2.2.1.2 NAME 'amavisBannedFilesLover
- ' DESC 'Banned Files Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115
- .121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {2}( 1.3.6.1.4.1.15312.2.2.1.3 NAME 'amavisBypassVirusCheck
- s' DESC 'Bypass Virus Check' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.11
- 5.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {3}( 1.3.6.1.4.1.15312.2.2.1.4 NAME 'amavisBypassSpamChecks
- ' DESC 'Bypass Spam Check' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.
- 121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {4}( 1.3.6.1.4.1.15312.2.2.1.5 NAME 'amavisSpamTagLevel' DE
- SC 'Spam Tag Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Substring
- sMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {5}( 1.3.6.1.4.1.15312.2.2.1.6 NAME 'amavisSpamTag2Level' D
- ESC 'Spam Tag2 Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Substri
- ngsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {6}( 1.3.6.1.4.1.15312.2.2.1.7 NAME 'amavisSpamKillLevel' D
- ESC 'Spam Kill Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Substri
- ngsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {7}( 1.3.6.1.4.1.15312.2.2.1.8 NAME 'amavisSpamModifiesSubj
- ' DESC 'Modifies Subject on spam - no longer in use since 2.7.0' EQUALITY boo
- leanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {8}( 1.3.6.1.4.1.15312.2.2.1.9 NAME 'amavisWhitelistSender'
- DESC 'White List Sender' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Sub
- stringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
-olcAttributeTypes: {9}( 1.3.6.1.4.1.15312.2.2.1.10 NAME 'amavisBlacklistSender
- ' DESC 'Black List Sender' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Su
- bstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
-olcAttributeTypes: {10}( 1.3.6.1.4.1.15312.2.2.1.11 NAME 'amavisSpamQuarantine
- To' DESC 'Spam Quarantine to' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA
- 5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {11}( 1.3.6.1.4.1.15312.2.2.1.12 NAME 'amavisSpamLover' DES
- C 'Spam Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SING
- LE-VALUE )
-olcAttributeTypes: {12}( 1.3.6.1.4.1.15312.2.2.1.13 NAME 'amavisBadHeaderLover
- ' DESC 'Bad Header Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.1
- 21.1.7 SINGLE-VALUE )
-olcAttributeTypes: {13}( 1.3.6.1.4.1.15312.2.2.1.14 NAME 'amavisBypassBannedCh
- ecks' DESC 'Bypass Banned Files Check' EQUALITY booleanMatch SYNTAX 1.3.6.1.4
- .1.1466.115.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {14}( 1.3.6.1.4.1.15312.2.2.1.15 NAME 'amavisBypassHeaderCh
- ecks' DESC 'Bypass Header Check' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.146
+olcAttributeTypes: {0}( 1.3.6.1.4.1.15312.2.2.1.1 NAME 'amavisVirusLover' DE
+ SC 'Virus Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+ SINGLE-VALUE )
+olcAttributeTypes: {1}( 1.3.6.1.4.1.15312.2.2.1.2 NAME 'amavisBannedFilesLov
+ er' DESC 'Banned Files Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466
+ .115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {2}( 1.3.6.1.4.1.15312.2.2.1.3 NAME 'amavisBypassVirusChe
+ cks' DESC 'Bypass Virus Check' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.146
6.115.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {15}( 1.3.6.1.4.1.15312.2.2.1.16 NAME 'amavisVirusQuarantin
- eTo' DESC 'Virus quarantine location' EQUALITY caseIgnoreIA5Match SUBSTR case
- IgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VAL
- UE )
-olcAttributeTypes: {16}( 1.3.6.1.4.1.15312.2.2.1.17 NAME 'amavisBannedQuaranti
- neTo' DESC 'Banned Files quarantine location' EQUALITY caseIgnoreIA5Match SUB
- STR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SI
- NGLE-VALUE )
-olcAttributeTypes: {17}( 1.3.6.1.4.1.15312.2.2.1.18 NAME 'amavisBadHeaderQuara
- ntineTo' DESC 'Bad Header quarantine location' EQUALITY caseIgnoreIA5Match SU
- BSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} S
- INGLE-VALUE )
-olcAttributeTypes: {18}( 1.3.6.1.4.1.15312.2.2.1.19 NAME 'amavisLocal' DESC 'I
- s user considered local' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.12
- 1.1.7 SINGLE-VALUE )
-olcAttributeTypes: {19}( 1.3.6.1.4.1.15312.2.2.1.20 NAME 'amavisMessageSizeLim
- it' DESC 'Message size limit' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA
- 5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {20}( 1.3.6.1.4.1.15312.2.2.1.21 NAME 'amavisWarnVirusRecip
- ' DESC 'Notify virus recipients' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.146
- 6.115.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {21}( 1.3.6.1.4.1.15312.2.2.1.22 NAME 'amavisWarnBannedReci
- p' DESC 'Notify banned file recipients' EQUALITY booleanMatch SYNTAX 1.3.6.1.
- 4.1.1466.115.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {22}( 1.3.6.1.4.1.15312.2.2.1.23 NAME 'amavisWarnBadHeaderR
- ecip' DESC 'Notify bad header recipients' EQUALITY booleanMatch SYNTAX 1.3.6.
- 1.4.1.1466.115.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {23}( 1.3.6.1.4.1.15312.2.2.1.24 NAME 'amavisVirusAdmin' DE
- SC 'Virus admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMa
- tch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {24}( 1.3.6.1.4.1.15312.2.2.1.25 NAME 'amavisNewVirusAdmin'
- DESC 'New virus admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Subst
- ringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {25}( 1.3.6.1.4.1.15312.2.2.1.26 NAME 'amavisSpamAdmin' DES
- C 'Spam admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatc
- h SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {26}( 1.3.6.1.4.1.15312.2.2.1.27 NAME 'amavisBannedAdmin' D
- ESC 'Banned file admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Subst
+olcAttributeTypes: {3}( 1.3.6.1.4.1.15312.2.2.1.4 NAME 'amavisBypassSpamChec
+ ks' DESC 'Bypass Spam Check' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.
+ 115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {4}( 1.3.6.1.4.1.15312.2.2.1.5 NAME 'amavisSpamTagLevel'
+ DESC 'Spam Tag Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Subst
ringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {27}( 1.3.6.1.4.1.15312.2.2.1.28 NAME 'amavisBadHeaderAdmin
- ' DESC 'Bad header admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Sub
+olcAttributeTypes: {5}( 1.3.6.1.4.1.15312.2.2.1.6 NAME 'amavisSpamTag2Level'
+ DESC 'Spam Tag2 Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Sub
stringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {28}( 1.3.6.1.4.1.15312.2.2.1.29 NAME 'amavisBannedRuleName
- s' DESC 'Banned rule names' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5S
- ubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {29}( 1.3.6.1.4.1.15312.2.2.1.30 NAME 'amavisSpamDsnCutoffL
- evel' DESC 'Spam DSN Cutoff Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgn
- oreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE
- )
-olcAttributeTypes: {30}( 1.3.6.1.4.1.15312.2.2.1.31 NAME 'amavisSpamQuarantine
- CutoffLevel' DESC 'Spam Quarantine Cutoff Level' EQUALITY caseIgnoreIA5Match
- SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}
- SINGLE-VALUE )
-olcAttributeTypes: {31}( 1.3.6.1.4.1.15312.2.2.1.32 NAME 'amavisSpamSubjectTag
- ' DESC 'Spam Subject Tag' EQUALITY caseExactIA5Match SUBSTR caseExactSubstrin
- gsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {32}( 1.3.6.1.4.1.15312.2.2.1.33 NAME 'amavisSpamSubjectTag
- 2' DESC 'Spam Subject Tag2' EQUALITY caseExactIA5Match SUBSTR caseExactSubstr
- ingsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {33}( 1.3.6.1.4.1.15312.2.2.1.34 NAME 'amavisArchiveQuarant
- ineTo' DESC 'Archive quarantine location' EQUALITY caseIgnoreIA5Match SUBSTR
- caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE
- -VALUE )
-olcAttributeTypes: {34}( 1.3.6.1.4.1.15312.2.2.1.35 NAME 'amavisAddrExtensionV
- irus' DESC 'Address Extension for Virus' EQUALITY caseExactIA5Match SUBSTR ca
- seExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE
+olcAttributeTypes: {6}( 1.3.6.1.4.1.15312.2.2.1.7 NAME 'amavisSpamKillLevel'
+ DESC 'Spam Kill Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Sub
+ stringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {7}( 1.3.6.1.4.1.15312.2.2.1.8 NAME 'amavisSpamModifiesSu
+ bj' DESC 'Modifies Subject on spam - no longer in use since 2.7.0' EQUALITY
+ booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {8}( 1.3.6.1.4.1.15312.2.2.1.9 NAME 'amavisWhitelistSende
+ r' DESC 'White List Sender' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA
+ 5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
+olcAttributeTypes: {9}( 1.3.6.1.4.1.15312.2.2.1.10 NAME 'amavisBlacklistSend
+ er' DESC 'Black List Sender' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreI
+ A5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
+olcAttributeTypes: {10}( 1.3.6.1.4.1.15312.2.2.1.11 NAME 'amavisSpamQuaranti
+ neTo' DESC 'Spam Quarantine to' EQUALITY caseIgnoreIA5Match SUBSTR caseIgno
+ reIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE
)
-olcAttributeTypes: {35}( 1.3.6.1.4.1.15312.2.2.1.36 NAME 'amavisAddrExtensionS
- pam' DESC 'Address Extension for Spam' EQUALITY caseExactIA5Match SUBSTR case
- ExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {36}( 1.3.6.1.4.1.15312.2.2.1.37 NAME 'amavisAddrExtensionB
- anned' DESC 'Address Extension for Banned' EQUALITY caseExactIA5Match SUBSTR
- caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VAL
- UE )
-olcAttributeTypes: {37}( 1.3.6.1.4.1.15312.2.2.1.38 NAME 'amavisAddrExtensionB
- adHeader' DESC 'Address Extension for Bad Header' EQUALITY caseExactIA5Match
- SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SIN
- GLE-VALUE )
-olcAttributeTypes: {38}( 1.3.6.1.4.1.15312.2.2.1.39 NAME 'amavisSpamTag3Level'
- DESC 'Spam Tag3 Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Subst
- ringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {39}( 1.3.6.1.4.1.15312.2.2.1.40 NAME 'amavisSpamSubjectTag
- 3' DESC 'Spam Subject Tag3' EQUALITY caseExactIA5Match SUBSTR caseExactSubstr
- ingsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
-olcAttributeTypes: {40}( 1.3.6.1.4.1.15312.2.2.1.41 NAME 'amavisUncheckedQuara
- ntineTo' DESC 'Virus quarantine location' EQUALITY caseIgnoreIA5Match SUBSTR
- caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE
- -VALUE )
-olcAttributeTypes: {41}( 1.3.6.1.4.1.15312.2.2.1.42 NAME 'amavisCleanQuarantin
- eTo' DESC 'Clean quarantine location' EQUALITY caseIgnoreIA5Match SUBSTR case
- IgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VAL
- UE )
-olcAttributeTypes: {42}( 1.3.6.1.4.1.15312.2.2.1.43 NAME 'amavisUncheckedLover
- ' DESC 'Unchecked Files Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.
- 115.121.1.7 SINGLE-VALUE )
-olcAttributeTypes: {43}( 1.3.6.1.4.1.15312.2.2.1.44 NAME 'amavisForwardMethod'
- DESC 'Forward / next hop destination' EQUALITY caseIgnoreIA5Match SUBSTR cas
- eIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
-olcAttributeTypes: {44}( 1.3.6.1.4.1.15312.2.2.1.45 NAME 'amavisSaUserConf' DE
- SC 'SpamAssassin user preferences configuration filename' EQUALITY caseExactI
- A5Match SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{
+olcAttributeTypes: {11}( 1.3.6.1.4.1.15312.2.2.1.12 NAME 'amavisSpamLover' D
+ ESC 'Spam Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+ SINGLE-VALUE )
+olcAttributeTypes: {12}( 1.3.6.1.4.1.15312.2.2.1.13 NAME 'amavisBadHeaderLov
+ er' DESC 'Bad Header Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.1
+ 15.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {13}( 1.3.6.1.4.1.15312.2.2.1.14 NAME 'amavisBypassBanned
+ Checks' DESC 'Bypass Banned Files Check' EQUALITY booleanMatch SYNTAX 1.3.6
+ .1.4.1.1466.115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {14}( 1.3.6.1.4.1.15312.2.2.1.15 NAME 'amavisBypassHeader
+ Checks' DESC 'Bypass Header Check' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1
+ .1466.115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {15}( 1.3.6.1.4.1.15312.2.2.1.16 NAME 'amavisVirusQuarant
+ ineTo' DESC 'Virus quarantine location' EQUALITY caseIgnoreIA5Match SUBSTR
+ caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SING
+ LE-VALUE )
+olcAttributeTypes: {16}( 1.3.6.1.4.1.15312.2.2.1.17 NAME 'amavisBannedQuaran
+ tineTo' DESC 'Banned Files quarantine location' EQUALITY caseIgnoreIA5Match
+ SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{2
+ 56} SINGLE-VALUE )
+olcAttributeTypes: {17}( 1.3.6.1.4.1.15312.2.2.1.18 NAME 'amavisBadHeaderQua
+ rantineTo' DESC 'Bad Header quarantine location' EQUALITY caseIgnoreIA5Matc
+ h SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{
256} SINGLE-VALUE )
-olcAttributeTypes: {45}( 1.3.6.1.4.1.15312.2.2.1.46 NAME 'amavisSaUserName' DE
- SC 'SpamAssassin username (for Bayes and AWL lookups)' EQUALITY caseExactIA5M
- atch SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256
- } SINGLE-VALUE )
-olcAttributeTypes: {46}( 1.3.6.1.4.1.15312.2.2.1.47 NAME 'amavisDisclaimerOpti
- ons' DESC 'Altermime disclaimer map data' EQUALITY caseIgnoreIA5Match SUBSTR
- caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE
+olcAttributeTypes: {18}( 1.3.6.1.4.1.15312.2.2.1.19 NAME 'amavisLocal' DESC
+ 'Is user considered local' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.11
+ 5.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {19}( 1.3.6.1.4.1.15312.2.2.1.20 NAME 'amavisMessageSizeL
+ imit' DESC 'Message size limit' EQUALITY caseIgnoreIA5Match SUBSTR caseIgno
+ reIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE
+ )
+olcAttributeTypes: {20}( 1.3.6.1.4.1.15312.2.2.1.21 NAME 'amavisWarnVirusRec
+ ip' DESC 'Notify virus recipients' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1
+ .1466.115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {21}( 1.3.6.1.4.1.15312.2.2.1.22 NAME 'amavisWarnBannedRe
+ cip' DESC 'Notify banned file recipients' EQUALITY booleanMatch SYNTAX 1.3.
+ 6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {22}( 1.3.6.1.4.1.15312.2.2.1.23 NAME 'amavisWarnBadHeade
+ rRecip' DESC 'Notify bad header recipients' EQUALITY booleanMatch SYNTAX 1.
+ 3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {23}( 1.3.6.1.4.1.15312.2.2.1.24 NAME 'amavisVirusAdmin'
+ DESC 'Virus admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Substrin
+ gsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {24}( 1.3.6.1.4.1.15312.2.2.1.25 NAME 'amavisNewVirusAdmi
+ n' DESC 'New virus admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5S
+ ubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {25}( 1.3.6.1.4.1.15312.2.2.1.26 NAME 'amavisSpamAdmin' D
+ ESC 'Spam admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Substrings
+ Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {26}( 1.3.6.1.4.1.15312.2.2.1.27 NAME 'amavisBannedAdmin'
+ DESC 'Banned file admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5S
+ ubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {27}( 1.3.6.1.4.1.15312.2.2.1.28 NAME 'amavisBadHeaderAdm
+ in' DESC 'Bad header admin' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA
+ 5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {28}( 1.3.6.1.4.1.15312.2.2.1.29 NAME 'amavisBannedRuleNa
+ mes' DESC 'Banned rule names' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnore
+ IA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {29}( 1.3.6.1.4.1.15312.2.2.1.30 NAME 'amavisSpamDsnCutof
+ fLevel' DESC 'Spam DSN Cutoff Level' EQUALITY caseIgnoreIA5Match SUBSTR cas
+ eIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-
+ VALUE )
+olcAttributeTypes: {30}( 1.3.6.1.4.1.15312.2.2.1.31 NAME 'amavisSpamQuaranti
+ neCutoffLevel' DESC 'Spam Quarantine Cutoff Level' EQUALITY caseIgnoreIA5Ma
+ tch SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.2
+ 6{256} SINGLE-VALUE )
+olcAttributeTypes: {31}( 1.3.6.1.4.1.15312.2.2.1.32 NAME 'amavisSpamSubjectT
+ ag' DESC 'Spam Subject Tag' EQUALITY caseExactIA5Match SUBSTR caseExactSubs
+ tringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {32}( 1.3.6.1.4.1.15312.2.2.1.33 NAME 'amavisSpamSubjectT
+ ag2' DESC 'Spam Subject Tag2' EQUALITY caseExactIA5Match SUBSTR caseExactSu
+ bstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {33}( 1.3.6.1.4.1.15312.2.2.1.34 NAME 'amavisArchiveQuara
+ ntineTo' DESC 'Archive quarantine location' EQUALITY caseIgnoreIA5Match SUB
+ STR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}
+ SINGLE-VALUE )
+olcAttributeTypes: {34}( 1.3.6.1.4.1.15312.2.2.1.35 NAME 'amavisAddrExtensio
+ nVirus' DESC 'Address Extension for Virus' EQUALITY caseExactIA5Match SUBST
+ R caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE
-VALUE )
-olcObjectClasses: {0}( 1.3.6.1.4.1.15312.2.2.2.1 NAME 'amavisAccount' DESC 'Am
- avisd Account' SUP top AUXILIARY MAY ( amavisVirusLover $ amavisBypassVirusCh
- ecks $ amavisSpamLover $ amavisBypassSpamChecks $ amavisBannedFilesLover $ am
- avisBypassBannedChecks $ amavisBadHeaderLover $ amavisBypassHeaderChecks $ am
- avisSpamTagLevel $ amavisSpamTag2Level $ amavisSpamKillLevel $ amavisWhitelis
- tSender $ amavisBlacklistSender $ amavisSpamQuarantineTo $ amavisVirusQuarant
- ineTo $ amavisBannedQuarantineTo $ amavisBadHeaderQuarantineTo $ amavisArchiv
- eQuarantineTo $ amavisSpamModifiesSubj $ amavisLocal $ amavisMessageSizeLimit
- $ amavisWarnVirusRecip $ amavisWarnBannedRecip $ amavisWarnBadHeaderRecip $
- amavisVirusAdmin $ amavisNewVirusAdmin $ amavisSpamAdmin $ amavisBannedAdmin
- $ amavisBadHeaderAdmin $ amavisBannedRuleNames $ amavisSpamDsnCutoffLevel $ a
- mavisSpamQuarantineCutoffLevel $ amavisSpamSubjectTag $ amavisSpamSubjectTag2
- $ amavisAddrExtensionVirus $ amavisAddrExtensionSpam $ amavisAddrExtensionBa
- nned $ amavisAddrExtensionBadHeader $ amavisSpamTag3Level $ amavisSpamSubject
- Tag3 $ amavisUncheckedQuarantineTo $ amavisCleanQuarantineTo $ amavisUnchecke
- dLover $ amavisForwardMethod $ amavisSaUserConf $ amavisSaUserName $ cn $ des
- cription ) )
+olcAttributeTypes: {35}( 1.3.6.1.4.1.15312.2.2.1.36 NAME 'amavisAddrExtensio
+ nSpam' DESC 'Address Extension for Spam' EQUALITY caseExactIA5Match SUBSTR
+ caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-V
+ ALUE )
+olcAttributeTypes: {36}( 1.3.6.1.4.1.15312.2.2.1.37 NAME 'amavisAddrExtensio
+ nBanned' DESC 'Address Extension for Banned' EQUALITY caseExactIA5Match SUB
+ STR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SING
+ LE-VALUE )
+olcAttributeTypes: {37}( 1.3.6.1.4.1.15312.2.2.1.38 NAME 'amavisAddrExtensio
+ nBadHeader' DESC 'Address Extension for Bad Header' EQUALITY caseExactIA5Ma
+ tch SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{25
+ 6} SINGLE-VALUE )
+olcAttributeTypes: {38}( 1.3.6.1.4.1.15312.2.2.1.39 NAME 'amavisSpamTag3Leve
+ l' DESC 'Spam Tag3 Level' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5S
+ ubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {39}( 1.3.6.1.4.1.15312.2.2.1.40 NAME 'amavisSpamSubjectT
+ ag3' DESC 'Spam Subject Tag3' EQUALITY caseExactIA5Match SUBSTR caseExactSu
+ bstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {40}( 1.3.6.1.4.1.15312.2.2.1.41 NAME 'amavisUncheckedQua
+ rantineTo' DESC 'Virus quarantine location' EQUALITY caseIgnoreIA5Match SUB
+ STR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}
+ SINGLE-VALUE )
+olcAttributeTypes: {41}( 1.3.6.1.4.1.15312.2.2.1.42 NAME 'amavisCleanQuarant
+ ineTo' DESC 'Clean quarantine location' EQUALITY caseIgnoreIA5Match SUBSTR
+ caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SING
+ LE-VALUE )
+olcAttributeTypes: {42}( 1.3.6.1.4.1.15312.2.2.1.43 NAME 'amavisUncheckedLov
+ er' DESC 'Unchecked Files Lover' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1
+ 466.115.121.1.7 SINGLE-VALUE )
+olcAttributeTypes: {43}( 1.3.6.1.4.1.15312.2.2.1.44 NAME 'amavisForwardMetho
+ d' DESC 'Forward / next hop destination' EQUALITY caseIgnoreIA5Match SUBSTR
+ caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
+olcAttributeTypes: {44}( 1.3.6.1.4.1.15312.2.2.1.45 NAME 'amavisSaUserConf'
+ DESC 'SpamAssassin user preferences configuration filename' EQUALITY caseEx
+ actIA5Match SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121
+ .1.26{256} SINGLE-VALUE )
+olcAttributeTypes: {45}( 1.3.6.1.4.1.15312.2.2.1.46 NAME 'amavisSaUserName'
+ DESC 'SpamAssassin username (for Bayes and AWL lookups)' EQUALITY caseExact
+ IA5Match SUBSTR caseExactSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.
+ 26{256} SINGLE-VALUE )
+olcAttributeTypes: {46}( 1.3.6.1.4.1.15312.2.2.1.47 NAME 'amavisDisclaimerOp
+ tions' DESC 'Altermime disclaimer map data' EQUALITY caseIgnoreIA5Match SUB
+ STR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}
+ SINGLE-VALUE )
+olcObjectClasses: {0}( 1.3.6.1.4.1.15312.2.2.2.1 NAME 'amavisAccount' DESC '
+ Amavisd Account' SUP top AUXILIARY MAY ( amavisVirusLover $ amavisBypassVir
+ usChecks $ amavisSpamLover $ amavisBypassSpamChecks $ amavisBannedFilesLove
+ r $ amavisBypassBannedChecks $ amavisBadHeaderLover $ amavisBypassHeaderChe
+ cks $ amavisSpamTagLevel $ amavisSpamTag2Level $ amavisSpamKillLevel $ amav
+ isWhitelistSender $ amavisBlacklistSender $ amavisSpamQuarantineTo $ amavis
+ VirusQuarantineTo $ amavisBannedQuarantineTo $ amavisBadHeaderQuarantineTo
+ $ amavisArchiveQuarantineTo $ amavisSpamModifiesSubj $ amavisLocal $ amavis
+ MessageSizeLimit $ amavisWarnVirusRecip $ amavisWarnBannedRecip $ amavisWar
+ nBadHeaderRecip $ amavisVirusAdmin $ amavisNewVirusAdmin $ amavisSpamAdmin
+ $ amavisBannedAdmin $ amavisBadHeaderAdmin $ amavisBannedRuleNames $ amavis
+ SpamDsnCutoffLevel $ amavisSpamQuarantineCutoffLevel $ amavisSpamSubjectTag
+ $ amavisSpamSubjectTag2 $ amavisAddrExtensionVirus $ amavisAddrExtensionSp
+ am $ amavisAddrExtensionBanned $ amavisAddrExtensionBadHeader $ amavisSpamT
+ ag3Level $ amavisSpamSubjectTag3 $ amavisUncheckedQuarantineTo $ amavisClea
+ nQuarantineTo $ amavisUncheckedLover $ amavisForwardMethod $ amavisSaUserCo
+ nf $ amavisSaUserName $ amavisDisclaimerOptions $ cn $ description ) )
#--------------------------------------------------------------------------
#
# GNU Free Documentation License
diff --git a/LDAP.schema b/LDAP.schema
index a0f0f91..d34b152 100644
--- a/LDAP.schema
+++ b/LDAP.schema
@@ -568,7 +568,7 @@ objectclass ( 1.3.6.1.4.1.15312.2.2.2.1
amavisSpamTag3Level $ amavisSpamSubjectTag3 $
amavisUncheckedQuarantineTo $ amavisCleanQuarantineTo $
amavisUncheckedLover $ amavisForwardMethod $
- amavisSaUserConf $ amavisSaUserName $
+ amavisSaUserConf $ amavisSaUserName $ amavisDisclaimerOptions $
cn $ description ) )
#--------------------------------------------------------------------------
diff --git a/README_FILES/README.customize b/README_FILES/README.customize
index 17b218d..e9419d8 100644
--- a/README_FILES/README.customize
+++ b/README_FILES/README.customize
@@ -1,7 +1,6 @@
Customization of notification messages and log entries
======================================================
- Mark Martinec <Mark.Martinec at ijs.si>,
- 2002, 2004, 2006, 2007, 2008, 2010, 2011, 2012
+ Mark Martinec <Mark.Martinec at ijs.si>
Since March 2002 amavisd-new provides a way to customize e-mail notification
messages that are sent in response to a virus (and spam) detection,
@@ -165,6 +164,43 @@ The substitution text for the following simple macros is built-in:
client_addr_port combines addr and port, similar to: \[%a\]:[:client_port]
+ protocol
+ a protocol name by which a message was received by amavisd,
+ according to RFC 3848 ("Transmission Types Registration") and
+ "Mail Transmission Types" / "WITH protocol types" IANA registration
+ http://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
+ e.g.:
+ SMTP, ESMTP, ESMTPA, ESMTPS, ESMTPSA,
+ LMTP, LMTPA, LMTPS, LMTPSA,
+ UTF8SMTP, UTF8SMTPA, UTF8SMTPS, UTF8SMTPSA,
+ UTF8LMTP, UTF8LMTPA, UTF8LMTPS, UTF8LMTPSA, ...
+
+ client_protocol
+ a protocol name by which a message was received from a client by MTA;
+ the information is passed from MTA to amavisd through XFORWARD PROTO
+ SMTP protocol extension or through AM.PDP (milter);
+
+ ip_trace_all
+ a list of IP addresses from a Received header trace;
+
+ ip_trace_public
+ like ip_trace_all, except that non-public IP addresses are excluded
+ from the list;
+
+ ip_proto_trace_all
+ a list of information items from a Received header trace; each item
+ consists of a protocol name (the WITH clause) and an IP address,
+ optionally followed by a source port number if known;
+ Example:
+ ESMTP://[2001:db8::143:1]:39141 < ESMTP://2001:db8::25 <
+ esmtps://203.0.113.172 < ESMTPSA://192.168.9.9
+ or:
+ UTF8SMTP://[203.0.113.172]:51208 < UTF8SMTPSA://192.168.9.9
+
+ ip_proto_trace_public
+ like ip_proto_trace_all, except that entries with non-public
+ IP address are excluded from the list;
+
l (letter ell, suggesting 'local') is true if a variable 'originating' is
true, and is an empty string otherwise; the boolean variable 'originating'
is under policy bank control, and usually corresponds to a sending host
@@ -190,14 +226,22 @@ The substitution text for the following simple macros is built-in:
j 'Subject' header field body
m 'Message-ID' header field body
r first 'Resent-Message-ID' header field body
+
header_field field body of the header field specified in the argument;
the first argument is a header field name (case insensitive);
optional second argument truncates the result to n characters;
optional third argument j helps choosing the header field in case of
multiple fields of the same name: returns a j-th header field with a
given field name; search proceeds top-down if j >= 0, or bottom up for
- negative values (-1=last, -2=next-to-last, ...); unspecified j is
- equivalent to -1, i.e. the last header field of the specified name;
+ negative values (-1=last, -2=next-to-last, ...). Unspecified j is
+ equivalent to -1, i.e. the last header field of the specified name.
+ The result is a string of logical characters (Unicode), suitable for
+ notification templates.
+
+ header_field_octets
+ like header_field, except that a result is a string of octets
+ in UTF-8 encoding, suitable for a log template
+
useragent returns 'User-Agent: ...' or 'X-Mailer: ...' header field
(whichever is present); an optional argument specifies whether
an entire field is to be returned (empty or unrecognized argument),
@@ -364,7 +408,7 @@ The substitution text for the following simple macros is built-in:
b64urlenc encodes its arguments as base64 strings [A-Za-z0-9-_]
according to RFC 4648, removing the final null padding '=' characters;
- mime2utf8 takes a string as its first argument, and an optional truncation
+ mime_decode takes a string as its first argument, and an optional truncation
length as the second. The string is decoded as a MIME-Header string
(understands Q or B character set encodings like =?iso-8859-2?Q?...?=,
=?koi8-r?B?...?=) and converted to UTF-8, optionally truncated to the
@@ -372,6 +416,26 @@ The substitution text for the following simple macros is built-in:
The macro can be useful to decode Subject or From header fields, e.g.:
[? [:header_field|Subject]||,\
Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100]]]#
+ The result is a string of logical characters (Unicode), suitable for
+ notification templates;
+
+ mime2utf8
+ like mime_decode, except that the result is a string of octets
+ in UTF-8 encoding, suitable for log templates;
+
+ mail_addr_decode
+ takes an e-mail address as a string of octets, where a local part
+ may be encoded as UTF-8, and the domain part may be an international
+ domain name (IDN) consisting either of U-labels or A-labels or NR-LDH
+ labels. Decodes A-labels to U-labels in domain name. Returns a string
+ of logical characters (Unicode), suitable for notification templates.
+ If the mail address is not a valid UTF-8 string, it is interpreted as
+ ISO-8859-1 (Latin-1).
+
+ mail_addr_decode_octets
+ like mail_addr_decode, except that the result is a string of octets,
+ only valid as UTF-8 if the provided address was a valid UTF-8
+ (garbage-in/garbage-out);
supplementary_info gives access to some additional information provided by
content scanners, such as a provided by SpamAssassin API routine get_tag.
@@ -574,9 +638,9 @@ syntax:
[? arg1 | arg2 | ... ] a selector
[~ arg1 | arg2 | ... ] a regexp selector
[ arg1 | arg2 | ... ] an iterator
-where [, [?, [~, | and ] are required tokens. Arguments are arbitrary text,
-possibly multiline, whitespace counts. Nested macro calls are permitted,
-proper bracket nesting must be observed.
+where '[', '[?', '[~', '|', and ']' are required tokens. Arguments are
+arbitrary text, possibly multiline, whitespace matters. Nested macro calls
+are permitted, proper bracket nesting must be observed.
SELECTOR lets its first argument be evaluated immediately, and implicitly
quotes remaining arguments. The evaluated first argument chooses which
diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 24a69bb..d2487b9 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -1,4 +1,207 @@
---------------------------------------------------------------------------
+ October 22, 2014
+amavisd-new-2.10.0 release notes
+
+Contents:
+ COMPATIBILITY
+ BUG FIXES
+ NEW FEATURES
+
+
+COMPATIBILITY
+
+- New requirement: perl module Net::LibIDN needs to be installed.
+
+- Uses a perl module File::LibMagic if installed, instead of spawning
+ a file(1) utility.
+
+- Support for international email relies heavily on perl to do the
+ right thing in its support of Unicode, so using a reasonably recent
+ version of perl is recommended. Amavisd was tested with perl 5.18
+ and 5.20.1. Versions of perl older than 5.12 may cause problems
+ with handling, encoding, and decoding of Unicode characters.
+ It is reasonable to expect that versions 5.14 and 5.16 are fine too,
+ but have not been tested extensively.
+
+- Default log templates and notification templates have changed
+ in details (like in decoding of international e-mail addresses), so
+ if locally customized templates are in use these will benefit from
+ updating - otherwise expect some mojibake in log and notifications.
+
+- International domain names (IDN) encoded in ASCII-compatible encoding
+ found in e-mail addresses and in Message-ID header field will be decoded
+ to Unicode for presentation purposes (syslog, JSON structured log,
+ notifications). This decoding does not affect a mail message itself.
+
+- Logging via syslog expects that syslogd (or equivalent) will not
+ clobber UTF-8 octets. It may be necessary to tell syslogd to accept
+ C1 control characters unchanged, e.g. by adding a command line option
+ "-8" to syslogd. Failing to do so may leave logged entries (like
+ sender and recipient address, From, Subject) in international mail
+ garbled or poorly readable in syslog.
+ On FreeBSD one should add: syslogd_flags="-8" to /etc/rc.conf.
+
+- Third party log parsers may need updating to accept logs with Unicode
+ characters in UTF-8 encoding.
+
+- A SMTP response to an EHLO command will now announce SMTPUTF8 capability
+ by default.
+
+
+BUG FIXES
+
+- releasing a message from an SQL quarantine was broken in version 2.9.1
+ due to introduction of parent_mail_id(); patches provided by Stef Simoens
+ and Gionatan Danti;
+
+- if checking of a message was aborted prematurely (like due to a timeout
+ or some fatal error), JSON log could receive a copy of a previous
+ log entry;
+
+- prevent non-ASCII non-UTF-8 octets from reaching a JSON log/report
+ (which produced an invalid JSON object and Elasticsearch complaining);
+
+- allow SMTP commands MAIL FROM and RCPT TO to accept options without
+ values, as allowed by the RFC 5321 syntax;
+
+- in delivery status notification (DSN) the field Received-From-MTA
+ specified 'smtp' as mta-name-type, instead of a 'dns' as prescribed
+ in RFC 3464;
+
+- releasing from a quarantine leaves envelope sender address as '<>'
+ instead of using the address found in a Return-Path header field
+ of a quarantined message, while also logging a warning:
+ Quarantine release $QID: missing X-Envelope-From or Return-Path
+ reported by Pascal Volk;
+
+- avoid failure of os_fingerprint in certain cases where the
+ $os_fingerprint_method uses an asterisk in place of a host IP address
+ or port number; the reported error was:
+ os_fingerprint FAILED: Insecure dependency in socket
+ while running with -T switch
+ at /usr/lib/perl/5.18/IO/Socket.pm line 80
+ reported by -ben;
+
+- files LDAP.ldif and LDAP.schema: added a missing attribute
+ amavisDisclaimerOptions to objectClass; reported by Quanah Gibson-Mount;
+
+
+NEW FEATURES
+
+- added support for Internationalized Email:
+ * RFC 6530 - Overview and Framework for Internationalized Email
+ * RFC 6531 - SMTP Extension for Internationalized Email (SMTPUTF8)
+ * RFC 6532 - Internationalized Email Headers
+ * RFC 6533 - Internationalized Delivery Status Notifications
+
+ This supports UTF-8 (EAI) in SMTP/LMTP sender addresses, recipient
+ addresses, and message header section. Feature parity with Postfix
+ version 2.12 (support introduced in development snapshot 20140715).
+
+ The SMTPUTF8 extension is supported by Gmail since 2014-08-05:
+ http://googleblog.blogspot.com/2014/08/a-first-step-toward-more-global-email.html
+
+
+- added support for Internationalized Domain Names (IDN) according
+ to IDNA (RFC 5890, RFC 5891; RFC 3490);
+
+ * A-labels in ASCII-compatible encoding of domain names are converted
+ to U-labels for presentation/logging purposed;
+
+ * U-labels are converted to A-labels when feeding a mail message
+ to an MTA which does not announce support for SMTPUTF8 extension
+ (instead of rejecting them as invalid mail address);
+
+ * For lookup purposes an international domain name is converted to
+ ASCII-compatible encoding when used as a query key in DNS lookups
+ and in lookups into hash, list, SQL and LDAP lookup tables (but not
+ in regexp table lookups). These tables are expected to contain domain
+ names in their ASCII representation (ACE). For convenience of config
+ files subroutines idn_to_ascii() and mail_idn_to_ascii() are available,
+ which encode a Unicode domain name to ACE (like ToASCII in RFC 3490);
+
+ * Many configuration settings may have their domain names in UTF-8.
+ These will be converted to ACE automatically where necessary
+ (e.g. when creating a Received and Authentication-Results header
+ fields, DKIM signatures, mail addresses in notifications, ...).
+ These settings include:
+
+ $myhostname, $localhost_name, $myauthservid, $mydomain,
+ notification sender and recipient mail addresses
+ ($mailfrom_notify_*, $hdrfrom_notify_*, @*_admin_maps),
+ domain names and selectors in DKIM signing keys (in calls
+ to dkim_key() );
+
+
+- delivery notifications and admin notifications now show the following
+ information encoded as UTF-8 (which is a default $bdy_encoding) in the
+ plain text part of the message: IDN domain names in sender and recipient
+ mail addresses and Message-ID are first decoded to Unicode, Subject and
+ author display names are MIME-decoded;
+
+- 'amavisd showkeys' and 'amavisd testkeys' can now deal with IDN
+ (international domain names): domain names in DNS zone comments
+ end up as UTF-8, DNS labels are in ASCII (A-labels); domain names in
+ calls to dkim_key() may be specified either as UTF-8 or in ASCII (ACE);
+
+- Received trace information in $log_verbose_templ and in notifications
+ now include protocol information (the WITH clause from Received header
+ fields);
+
+- added a macro 'protocol' to the default $log_verbose_templ template.
+ It evaluates to a protocol name by which a message was received by
+ amavisd, according to RFC 3848 ("Transmission Types Registration") and
+ "Mail Transmission Types" / "WITH protocol types" IANA registration
+ http://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
+ e.g.: SMTP, ESMTP, ESMTPA, ESMTPS, ESMTPSA, LMTP, LMTPA, LMTPS, LMTPSA,
+ UTF8SMTP, UTF8SMTPA, UTF8SMTPS, UTF8SMTPSA,
+ UTF8LMTP, UTF8LMTPA, UTF8LMTPS, UTF8LMTPSA, ...
+
+- new macros: mail_addr_decode, mail_addr_decode_octets, mime_decode,
+ header_field_octets, ip_trace_all, ip_trace_public, protocol,
+ ip_proto_trace_all, ip_proto_trace_public, client_protocol;
+ (documented in README_FILES/README.customize);
+
+- use a perl module File::LibMagic when available, instead of spawning
+ a file(1) utility for classifying contents of mail parts.
+ By using a direct interface to libmagic library the startup cost
+ of spawning an external process is avoided. Benchmarking shows that
+ using libmagic is significantly faster especially for checking a small
+ number of files - takes 4 ms for checking one file with libmagic
+ vs. 27 ms with a spawned file(1); based on a patch by Markus Benning;
+
+
+OTHER
+
+- RFC 6533: recognize a MIME type 'message/global' as similar
+ to 'message/rfc822', and 'message/global-headers' as similar
+ to 'text/rfc822-headers' where appropriate (e.g. in bounce killer);
+
+- header validity check now distinguishes 'non-ASCII and invalid UTF-8'
+ from 'non-ASCII but valid UTF-8' characters in a mail header section.
+ By default valid UTF-8 strings in a mail header section are not treated
+ as error even if mail is not flagged as international mail (SMTPUTF8),
+ as these are quite common in practice. To treat non- MIME-encoded UTF-8
+ in a header section as error the test can be enabled by:
+ $allowed_header_tests{'utf8'} = 1;
+
+- ORCPT attribute in SMTP 'RCPT TO' command now accepts the original
+ recipient mail address in any of these encodings: utf-8-address,
+ utf-8-addr-unitext, utf-8-addr-xtext, or as a legacy xtext,
+ as required by RFC 6533;
+
+- updated do_cabextract (extraction of Microsoft cabinet .cab archives)
+ to recognize a slightly changed output of cabextract version 1.2;
+ patch by Thomas Jarosch;
+
+- adjusted some timeouts to leave more reserve for later stages of
+ mail processing and forwarding;
+
+- prefer sanitizing/protecting control characters as hex code (like \x7F)
+ instead of octal (like \177) (e.g. in logging and DSN);
+
+
+---------------------------------------------------------------------------
June 27, 2014
amavisd-new-2.9.1 release notes
diff --git a/amavisd b/amavisd
index 8d2a2e1..278f0dd 100755
--- a/amavisd
+++ b/amavisd
@@ -132,7 +132,7 @@
# Amavis::Tools
#------------------------------------------------------------------------------
-use sigtrap qw(stack-trace BUS SEGV EMT FPE ILL SYS TRAP ABRT);
+use sigtrap qw(stack-trace BUS SEGV EMT FPE ILL SYS TRAP); # ABRT
use strict;
use re 'taint';
@@ -211,7 +211,7 @@ sub fetch_modules($$@) {
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
push(@missing,$m);
- $eval_stat =~ s/^/ /mgs; # indent
+ $eval_stat =~ s/^/ /gms; # indent
printf STDERR ("fetch_modules: error loading %s module %s:\n%s\n",
$required ? 'required' : 'optional', $_, $eval_stat)
if $eval_stat !~ /\bCan't locate \Q$_\E in \@INC\b/;
@@ -235,7 +235,7 @@ BEGIN {
MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder
MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint
MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64
- Net::Server Net::Server::PreFork
+ Net::LibIDN Net::Server Net::Server::PreFork
));
# with earlier versions of Perl one may need to add additional modules
# to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ...
@@ -284,7 +284,7 @@ use constant CC_VIRUS => 9;
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
%EXPORT_TAGS = (
'dynamic_confvars' => # per- policy bank settings
@@ -415,7 +415,7 @@ BEGIN {
$dspam $sa_spawned
)],
'platform' => [qw(
- $profiling $can_truncate $unicode_aware $my_pid
+ $profiling $can_truncate $my_pid
$AF_INET6 $have_inet4 $have_inet6 $io_socket_module_name
&D_TEMPFAIL &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS
&CC_CATCHALL &CC_CLEAN &CC_MTA &CC_OVERSIZED &CC_BADH
@@ -614,7 +614,7 @@ EOD
# signatures which have no corresponding public key published in DNS.
# The proper way is to have one dkim_key entry for each published DNS RR.
# Optional arguments can provide additional information about the resource
-# record (RR) of a public key, i.e. its options according to RFC 4871.
+# record (RR) of a public key, i.e. its options according to RFC 6376.
# The subroutine is typically called from a configuration file, once for
# each signing key available.
#
@@ -654,8 +654,9 @@ sub dkim_key($$$;@) {
$pem_fh->close or die "Error closing file $fname: $!";
$key_options{k} = 'rsa' if defined $key_options{k}; # force RSA
}
- $domain = lc($domain) if !ref($domain); # possibly a regexp
- $selector = lc($selector);
+ # possibly the $domain is a regexp
+ $domain = Amavis::Util::idn_to_ascii($domain) if !ref $domain;
+ $selector = Amavis::Util::idn_to_ascii($selector);
$key_options{domain} = $domain; $key_options{selector} = $selector;
$key_options{key_storage_ind} = $key_storage_ind;
if (@dkim_signing_keys_list > 100) {
@@ -672,11 +673,11 @@ sub dkim_key($$$;@) {
# essential initializations, right at the program start time, may run as root!
#
use vars qw($read_config_files_depth @actual_config_files);
-BEGIN { # init_primary: version, $unicode_aware, base policy bank
+BEGIN { # init_primary: version, base policy bank
$myprogram_name = $0; # typically 'amavisd'
local $1; $myprogram_name =~ s{([^/]*)\z}{$1}s;
$myproduct_name = 'amavisd-new';
- $myversion_id = '2.9.1'; $myversion_date = '20140627';
+ $myversion_id = '2.10.0'; $myversion_date = '20141022';
$myversion = "$myproduct_name-$myversion_id ($myversion_date)";
$myversion_id_numeric = # x.yyyzzz, allows numerical compare, like Perl $]
@@ -684,8 +685,6 @@ BEGIN { # init_primary: version, $unicode_aware, base policy bank
if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/s;
$sql_schema_version = $myversion_id_numeric;
- $unicode_aware =
- $] >= 5.008 && length("\x{263a}")==1 && eval { require Encode };
$read_config_files_depth = 0;
# initialize policy bank hash to contain dynamic config settings
for my $tag (@EXPORT_TAGS{'dynamic_confvars', 'legacy_dynamic_confvars'}) {
@@ -971,14 +970,12 @@ BEGIN {
# encoding (charset in MIME terminology)
# to be used in RFC 2047-encoded ...
-# $hdr_encoding = 'iso-8859-1'; # ... header field bodies
-# $bdy_encoding = 'iso-8859-1'; # ... notification body text
$hdr_encoding = 'UTF-8'; # ... header field bodies
$bdy_encoding = 'UTF-8'; # ... notification body text
# encoding (encoding in MIME terminology)
$hdr_encoding_qb = 'Q'; # quoted-printable (default)
-# $hdr_encoding_qb = 'B'; # base64 (usual for far east charsets)
+# $hdr_encoding_qb = 'B'; # base64
$smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit
@@ -994,7 +991,7 @@ BEGIN {
$enforce_smtpd_message_size_limit_64kb_min = 1;
# $localhost_name is the name of THIS host running amavisd
- # (typically 'localhost'). It is used in HELO SMTP command
+ # (often just 'localhost'). It is used in HELO SMTP command
# when reinjecting mail back to MTA via SMTP for final delivery,
# and in inserted Received header field
$localhost_name = 'localhost';
@@ -1261,9 +1258,10 @@ BEGIN {
# controls which header section tests are performed in check_header_validity,
# keys correspond to minor contents categories for CC_BADH
$allowed_header_tests{lc($_)} = 1 for qw(
- other mime 8bit control empty long syntax missing multiple);
+ other mime syntax empty long control 8bit utf8 missing multiple);
+ $allowed_header_tests{'utf8'} = 0; # turn this test off by default
- # RFC 4871 standard set of header fields to be signed:
+ # RFC 6376 standard set of header fields to be signed:
my(@sign_headers) = qw(From Sender Reply-To Subject Date Message-ID To Cc
In-Reply-To References MIME-Version Content-Type Content-Transfer-Encoding
Content-ID Content-Description Resent-Date Resent-From Resent-Sender
@@ -1277,7 +1275,7 @@ BEGIN {
Content-Location Content-Features Content-Disposition Content-Language
Content-Alternative Content-Base Content-MD5 Content-Duration Content-Class
Accept-Language Auto-Submitted Archived-At VBR-Info));
- # note that we are signing Received despite the advise in RFC 4871;
+ # note that we are signing Received despite the advise in RFC 6376;
# some additional nonstandard header fields:
push(@sign_headers, qw(Organization Organisation User-Agent X-Mailer));
$signed_header_fields{lc($_)} = 1 for @sign_headers;
@@ -1662,7 +1660,7 @@ BEGIN {
['rpm', \&Amavis::Unpackers::do_uncompress, \$rpm2cpio],
# ['rpm2cpio.pl', 'rpm2cpio'] ],
[['cpio','tar'], \&Amavis::Unpackers::do_pax_cpio, \$pax],
- # ['/usr/local/heirloom/usr/5bin/pax', 'pax', 'gcpio', 'cpio']
+ # ['/usr/local/heirloom/usr/5bin/pax', 'pax', 'gcpio', 'cpio'] ],
# ['tar', \&Amavis::Unpackers::do_tar], # no longer supported
['deb', \&Amavis::Unpackers::do_ar, \$ar],
# ['a', \&Amavis::Unpackers::do_ar, \$ar], #unpacking .a seems an overkill
@@ -1968,6 +1966,9 @@ no warnings 'once';
*read_hash = \&Amavis::Util::read_hash;
*read_array = \&Amavis::Util::read_array;
*read_cidr = \&Amavis::Util::read_cidr;
+*idn_to_ascii = \&Amavis::Util::idn_to_ascii; # RFC 3490: ToASCII
+*idn_to_utf8 = \&Amavis::Util::idn_to_utf8; # RFC 3490: ToUnicode
+*mail_idn_to_ascii = \&Amavis::Util::mail_addr_idn_to_ascii;
*dump_hash = \&Amavis::Util::dump_hash;
*dump_array = \&Amavis::Util::dump_array;
*ask_daemon = \&Amavis::AV::ask_daemon;
@@ -2046,6 +2047,7 @@ use vars qw(%final_destiny_by_ccat %defang_by_ccat
[ qr'^(Heuristics\.)?Phishing\.' => 0.1 ],
[ qr'^(Email|HTML)\.Phishing\.(?!.*Sanesecurity)' => 0.1 ],
[ qr'^Sanesecurity\.(Malware|Rogue|Trojan)\.' => undef ],# keep as infected
+ [ qr'^Sanesecurity\.Foxhole\.' => undef ],# keep as infected
[ qr'^Sanesecurity\.' => 0.1 ],
[ qr'^Sanesecurity_PhishBar_' => 0 ],
[ qr'^Sanesecurity.TestSig_' => 0 ],
@@ -2182,8 +2184,9 @@ sub supply_after_defaults() {
# be provided by Net::Server for 'flock' serialization on a socket accept()
# $lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file;
local($1,$2);
- $X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE;
- $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG;
+ $X_HEADER_LINE = $myproduct_name . ' at ' .
+ Amavis::Util::idn_to_ascii($mydomain) if !defined $X_HEADER_LINE;
+ $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG;
if ($X_HEADER_TAG =~ /^[!-9;-\176]+\z/) {
# implicitly add to %allowed_added_header_fields for compatibility,
# unless the hash entry already exists
@@ -2195,19 +2198,25 @@ sub supply_after_defaults() {
$bunzip2 = "$bzip2 -d" if !defined $bunzip2 && $bzip2 ne '';
$unlzop = "$lzop -d" if !defined $unlzop && $lzop ne '';
- # substring ${myhostname} will be expanded later, just before use
- my $pname = '"Content-filter at ${myhostname}"';
- $hdrfrom_notify_sender = "$pname <postmaster\@\${myhostname}>"
+ # substring "${myhostname}" will be expanded later, just before use
+ my $pname = '"Content-filter at ${myhostname_utf8}"';
+ $hdrfrom_notify_sender = $pname . ' <postmaster@${myhostname_ascii}>'
if !defined $hdrfrom_notify_sender;
- $hdrfrom_notify_recip = $mailfrom_notify_recip ne ''
- ? "$pname <$mailfrom_notify_recip>"
- : $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip;
- $hdrfrom_notify_admin = $mailfrom_notify_admin ne ''
- ? "$pname <$mailfrom_notify_admin>"
- : $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin;
- $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne ''
- ? "$pname <$mailfrom_notify_spamadmin>"
- : $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin;
+ $hdrfrom_notify_recip = $mailfrom_notify_recip eq ''
+ ? $hdrfrom_notify_sender
+ : sprintf("%s <%s>", $pname,
+ Amavis::Util::mail_addr_idn_to_ascii($mailfrom_notify_recip))
+ if !defined $hdrfrom_notify_recip;
+ $hdrfrom_notify_admin = $mailfrom_notify_admin eq ''
+ ? $hdrfrom_notify_sender
+ : sprintf("%s <%s>", $pname,
+ Amavis::Util::mail_addr_idn_to_ascii($mailfrom_notify_admin))
+ if !defined $hdrfrom_notify_admin;
+ $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin eq ''
+ ? $hdrfrom_notify_sender
+ : sprintf("%s <%s>", $pname,
+ Amavis::Util::mail_addr_idn_to_ascii($mailfrom_notify_spamadmin))
+ if !defined $hdrfrom_notify_spamadmin;
$hdrfrom_notify_release = $hdrfrom_notify_sender
if !defined $hdrfrom_notify_release;
$hdrfrom_notify_report = $hdrfrom_notify_sender
@@ -2235,13 +2244,13 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&init &amavis_log_id &collect_log_stats
&log_to_stderr &log_fd &open_log &close_log &write_log);
import Amavis::Conf qw(:platform $DEBUG $TEMPBASE c cr ca
$myversion $logline_maxlen $daemon_user);
-# import Amavis::Util qw(untaint);
+# import Amavis::Util qw(untaint idn_to_utf8);
}
use subs @EXPORT_OK;
@@ -2407,16 +2416,17 @@ sub write_log($$) {
$log_lines++;
my $now = Time::HiRes::time;
if ($log_to_stderr || !defined $loghandle) {
+ # milliseconds in timestamp
my $prefix = sprintf('%s:%06.3f %s %s[%s]: ', # syslog-like prefix
strftime('%b %e %H:%M',localtime($now)), $now-int($now/60)*60,
- c('myhostname'), c('myprogram_name'), $$); # milliseconds in timestamp
+ Amavis::Util::idn_to_utf8(c('myhostname')), c('myprogram_name'), $$);
# avoid multiple calls to write(2), join the string first!
my $s = $prefix . $am_id . $alert_mark . $errmsg . "\n";
print STDERR ($s) or die "Error writing to STDERR: $!";
} else {
my $prefix = sprintf('%s %s %s[%s]: ', # prepare a syslog-like prefix
strftime('%b %e %H:%M:%S',localtime($now)),
- c('myhostname'), c('myprogram_name'), $$);
+ Amavis::Util::idn_to_utf8(c('myhostname')), c('myprogram_name'), $$);
my $s = $prefix . $am_id . $alert_mark . $errmsg . "\n";
# NOTE: a lock is on a file, not on a file handle
flock($loghandle,LOCK_EX) or die "Can't lock a log file: $!";
@@ -2439,7 +2449,7 @@ use re 'taint';
BEGIN {
use vars qw(@ISA $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
import Amavis::Conf qw(:platform $TEMPBASE);
import Amavis::Log qw(write_log);
}
@@ -2536,7 +2546,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&init §ion_time &report &get_time_so_far
&get_rusage &rusage_report);
@@ -2691,14 +2701,17 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&untaint &untaint_inplace &min &max &minmax
&unique_list &unique_ref &format_time_interval
- &safe_encode &safe_encode_ascii &safe_encode_utf8
- &safe_decode &safe_decode_latin1
- &q_encode &orcpt_encode &orcpt_decode
+ &is_valid_utf_8 &truncate_utf_8
+ &safe_encode &safe_encode_utf8 &safe_encode_ascii
+ &safe_decode &safe_decode_utf8 &safe_decode_latin1
+ &safe_decode_mime &q_encode &orcpt_encode &orcpt_decode
&xtext_encode &xtext_decode &proto_encode &proto_decode
+ &idn_to_ascii &idn_to_utf8 &clear_idn_cache
+ &mail_addr_decode &mail_addr_idn_to_ascii
&ll &do_log &do_log_safe &snmp_count &snmp_count64
&snmp_counters_init &snmp_counters_get &snmp_initial_oids
&debug_oneshot &update_current_log_level
@@ -2709,8 +2722,8 @@ BEGIN {
&generate_mail_id &make_password
&crunching_start_time &prolong_timer &get_deadline
&waiting_for_client &switch_to_my_time &switch_to_client_time
- &sanitize_str &sanitize_str_inplace &fmt_struct
- &freeze &thaw &ccat_split &ccat_maj &cmp_ccat &cmp_ccat_maj
+ &sanitize_str &fmt_struct &freeze &thaw
+ &ccat_split &ccat_maj &cmp_ccat &cmp_ccat_maj
&setting_by_given_contents_category_all
&setting_by_given_contents_category &rmdir_recursively
&read_file &read_text &read_l10n_templates
@@ -2718,7 +2731,7 @@ BEGIN {
&dynamic_destination &collect_equal_delivery_recips);
import Amavis::Conf qw(:platform $DEBUG c cr ca $mail_id_size_bits
- $myversion $myhostname $snmp_contact $snmp_location
+ $myversion $snmp_contact $snmp_location
$trim_trailing_space_in_lookup_result_fields);
import Amavis::Log qw(amavis_log_id write_log);
@@ -2730,14 +2743,15 @@ use Errno qw(ENOENT EACCES EAGAIN ESRCH EBADF);
use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
use Digest::MD5; # 2.22 provides 'clone' method, no longer needed since 2.7.0
use MIME::Base64;
-use Encode; # Perl 5.8 UTF-8 support
+use Encode (); # Perl 5.8 UTF-8 support
use Scalar::Util qw(tainted);
+use Net::LibIDN ();
use vars qw($enc_ascii $enc_utf8 $enc_latin1 $enc_tainted
$enc_taintsafe $enc_is_utf8_buggy);
BEGIN {
$enc_ascii = Encode::find_encoding('ascii');
- $enc_utf8 = Encode::find_encoding('UTF-8');
+ $enc_utf8 = Encode::find_encoding('UTF-8'); # same as utf-8-strict
$enc_latin1 = Encode::find_encoding('ISO-8859-1');
$enc_ascii or die "Amavis::Util: unknown encoding 'ascii'";
$enc_utf8 or die "Amavis::Util: unknown encoding 'UTF-8'";
@@ -2747,11 +2761,12 @@ BEGIN {
$enc_is_utf8_buggy = 1 if $] < 5.010;
if (!tainted($enc_tainted)) {
warn "Amavis::Util: can't obtain a tainted string";
+ $enc_taintsafe = 1; # guessing
} else {
# test for Encode taint laundering bug [rt.cpan.org #84879], fixed in 2.50
# NOTE: [rt.cpan.org #85489] - Encode::encode turns on the UTF8 flag
# on a passed argument. Give it a copy to avoid turning $enc_tainted
- # into an UTF8 string!
+ # into a UTF-8 string!
my $t = $enc_ascii->encode("$enc_tainted");
$enc_taintsafe = 1 if tainted($t);
}
@@ -2834,6 +2849,64 @@ sub format_time_interval($) {
sprintf("%s%d %d:%02d:%02d", $sign, $dd, $hh, $mm, int($t+0.5));
}
+# returns true if the provided string of octets represents a syntactically
+# valid UTF-8 string, otherwise a false is returned
+#
+sub is_valid_utf_8($) {
+# my $octets = $_[0];
+ return undef if !defined $_[0];
+ #
+ # RFC 6532: UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
+ # RFC 3629 section 4: Syntax of UTF-8 Byte Sequences
+ # UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
+ # UTF8-1 = %x00-7F
+ # UTF8-2 = %xC2-DF UTF8-tail
+ # UTF8-3 = %xE0 %xA0-BF UTF8-tail /
+ # %xE1-EC 2( UTF8-tail ) /
+ # %xED %x80-9F UTF8-tail /
+ # # U+D800..U+DFFF are utf16 surrogates, not legal utf8
+ # %xEE-EF 2( UTF8-tail )
+ # UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) /
+ # %xF1-F3 3( UTF8-tail ) /
+ # %xF4 %x80-8F 2( UTF8-tail )
+ # UTF8-tail = %x80-BF
+ #
+ # loose variant:
+ # [\x00-\x7F] | [\xC0-\xDF][\x80-\xBF] |
+ # [\xE0-\xEF][\x80-\xBF]{2} | [\xF0-\xF4][\x80-\xBF]{3}
+ #
+ $_[0] =~ /^ (?: [\x00-\x7F] |
+ [\xC2-\xDF] [\x80-\xBF] |
+ \xE0 [\xA0-\xBF] [\x80-\xBF] |
+ [\xE1-\xEC] [\x80-\xBF]{2} |
+ \xED [\x80-\x9F] [\x80-\xBF] |
+ [\xEE-\xEF] [\x80-\xBF]{2} |
+ \xF0 [\x90-\xBF] [\x80-\xBF]{2} |
+ [\xF1-\xF3] [\x80-\xBF]{3} |
+ \xF4 [\x80-\x8F] [\x80-\xBF]{2} )* \z/xs ? 1 : 0;
+}
+
+# cleanly chop a UTF-8 byte sequence to $max_len or less, RFC 3629;
+# if $max_len is undefined just chop off any partial last character
+#
+sub truncate_utf_8($;$) {
+ my($octets, $max_len) = @_;
+ return $octets if !defined $octets;
+ return '' if defined $max_len && $max_len <= 0;
+ substr($octets,$max_len) = ''
+ if defined $max_len && length($octets) > $max_len;
+ # missing one or more UTF8-tail octets? chop the entire last partial char
+ if ($octets =~ tr/\x00-\x7F//c) { # triage - is non-ASCII
+ $octets =~ s/[\xC0-\xDF]\z//s
+ or $octets =~ s/[\xE0-\xEF][\x80-\xBF]{0,1}\z//s
+ or $octets =~ s/[\xF0-\xF7][\x80-\xBF]{0,2}\z//s
+ or $octets =~ s/[\xF8-\xFB][\x80-\xBF]{0,3}\z//s # not strictly valid
+ or $octets =~ s/[\xFC-\xFD][\x80-\xBF]{0,4}\z//s # not strictly valid
+ or $octets =~ s/ \xFE [\x80-\xBF]{0,5}\z//sx; # not strictly valid
+ }
+ $octets;
+}
+
# A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes
# Encode::encode to loop and fill memory when given a tainted string.
# Also works around a CPAN bug #64642 in module Encode:
@@ -2847,6 +2920,7 @@ sub safe_encode($$;$) {
return undef if !defined $_[0]; # must return undef even in a list context!
my $enc = Encode::find_encoding($encoding);
$enc or die "safe_encode: unknown encoding '$encoding'";
+ # the resulting UTF8 flag is always off
return $enc->encode(@_) if $enc_taintsafe || !tainted($_[0]);
# Work around a taint laundering bug in Encode [rt.cpan.org #84879].
# Propagate taintedness across taint-related bugs in module Encode
@@ -2858,17 +2932,27 @@ sub safe_encode($$;$) {
sub safe_encode_ascii($) {
# my $str = $_[0];
return undef if !defined $_[0]; # must return undef even in a list context!
- return $enc_ascii->encode($_[0], 0) if $enc_taintsafe || !tainted($_[0]);
+ return $enc_ascii->encode($_[0]) if $enc_taintsafe || !tainted($_[0]);
# propagate taintedness across taint-related bugs in module Encode
- $enc_tainted . $enc_ascii->encode(untaint($_[0]), 0);
+ $enc_tainted . $enc_ascii->encode(untaint($_[0]));
}
+# Encodes logical characters to UTF-8 octets, or returns a string of octets
+# (with utf8 flag off) unchanged. Ensures the result is always a string of
+# octets (utf8 flag off). Unlike safe_encode(), a non-ASCII string with
+# utf8 flag off will be returned unchanged, so the result may not be a
+# valid UTF-8 string!
+#
sub safe_encode_utf8($) {
-# my $str = $_[0];
- return undef if !defined $_[0]; # must return undef even in a list context!
- return $enc_utf8->encode($_[0], 0) if $enc_taintsafe || !tainted($_[0]);
- # propagate taintedness across taint-related bugs in module Encode
- $enc_tainted . $enc_utf8->encode(untaint($_[0]), 0);
+ my $str = $_[0];
+ return undef if !defined $str; # must return undef even in a list context!
+ if ( ($enc_taintsafe && !$enc_is_utf8_buggy) || !tainted($str) ) {
+ Encode::is_utf8($str) ? $enc_utf8->encode($str) : $str;
+ } else { # work around bugs in Encode
+ # propagate taintedness across taint-related bugs in module Encode
+ untaint_inplace($str);
+ $enc_tainted . (Encode::is_utf8($str) ? $enc_utf8->encode($str) : $str);
+ }
}
sub safe_decode_latin1($) {
@@ -2877,6 +2961,20 @@ sub safe_decode_latin1($) {
$enc_latin1->decode($_[0]);
}
+sub safe_decode_utf8($;$) {
+ my($str,$check) = @_;
+ return undef if !defined $str; # must return undef even in a list context!
+ if ( ($enc_taintsafe && !$enc_is_utf8_buggy) || !tainted($str)) {
+ Encode::is_utf8($str) ? $str : $enc_utf8->decode($str,$check);
+ } else {
+ # Work around a taint laundering bug in Encode [rt.cpan.org #84879].
+ # Propagate taintedness across taint-related bugs in module Encode.
+ untaint_inplace($str);
+ $enc_tainted . (Encode::is_utf8($str) ? $str
+ : $enc_utf8->decode($str,$check));
+ }
+}
+
sub safe_decode($$;$) {
# my($encoding,$str,$check) = @_;
my $encoding = shift;
@@ -2889,6 +2987,144 @@ sub safe_decode($$;$) {
$enc_tainted . $enc->decode(untaint($_[0]), $_[1]);
}
+# Handle Internationalized Domain Names according to IDNA: RFC 5890, RFC 5891.
+# Similar to ToASCII (RFC 3490), but does not fail on garbage.
+# Takes a domain name (possibly with utf8 flag on) consisting of U-labels
+# or A-labels or NR-LDH labels, converting each label to A-label, lowercased.
+# Non- IDNA-valid strings are only encoded to UTF-8 octets but are otherwise
+# unchanged. Result is in octets regardless of input, taintedness of the
+# argument is propagated to the result.
+#
+my %idn_encode_cache;
+sub clear_idn_cache() { %idn_encode_cache = () }
+sub idn_to_ascii($) {
+ # propagate taintedness of the argument, but not its utf8 flag
+ return tainted($_[0]) ? $idn_encode_cache{$_[0]} . $enc_tainted
+ : $idn_encode_cache{$_[0]}
+ if exists $idn_encode_cache{$_[0]};
+ my $s = $_[0];
+ my $t = tainted($s); # taintedness of the argument
+ return undef if !defined $s;
+ untaint_inplace($s) if $t;
+ # to octets if needed, not necessarily valid UTF-8 (inlined safe_encode_utf8)
+ $s = $enc_utf8->encode($s) if Encode::is_utf8($s);
+ if ($s !~ tr/\x00-\x7F//c) { # is all-ASCII (including IP address literal)
+ $s = lc $s;
+ } else {
+ # Net::LibIDN does not like a leading dot (or '@') in a valid domain name,
+ # but we need it (e.g. in lookups, meaning subdomains are included), so
+ # we have to carry a prefix across the call to Net::LibIDN::idn_to_ascii().
+ my $prefix; local($1);
+ $prefix = $1 if $s =~ s/^([.\@])//s; # strip a leading dot or '@'
+ # to ASCII-compatible encoding (ACE)
+ my $sa = Net::LibIDN::idn_to_ascii($s, 'UTF-8');
+ $s = lc $sa if defined $sa;
+ $s = $prefix.$s if $prefix;
+ }
+ $idn_encode_cache{$_[0]} = $s;
+ $t ? $s.$enc_tainted : $s; # propagate taintedness of the argument
+}
+
+# Handle Internationalized Domain Names according to IDNA: RFC 5890, RFC 5891.
+# Implements ToUnicode (RFC 3490). ToUnicode always succeeds, because it just
+# returns the original string if decoding fails. In particular, this means that
+# ToUnicode has no effect on a label that does not begin with the ACE prefix.
+# Takes a domain name (as a string of octets or logical characters)
+# of "Internationalized labels" (A-labels, U-labels, or NR-LDH labels),
+# converting each label to U-label. Result is a string of octets encoded
+# as UTF-8 if input was valid.
+#
+sub idn_to_utf8($) {
+ my $s = $_[0];
+ return undef if !defined $s;
+ $s = safe_encode_utf8($s); # to octets (if not already)
+ if ($s =~ /(?: ^ | \. ) xn-- [\x00-\x2D\x2F-\xFF]{0,58} [\x00-\x2C\x2F-\xFF]
+ (?: \z | \. )/xsi) { # contains XN-label
+ my $su = Net::LibIDN::idn_to_unicode(lc $s, 'UTF-8');
+ return $su if defined $su;
+ }
+ $s;
+}
+
+# decode octets found in a mail header field body to a logical chars string
+#
+sub safe_decode_mime($) {
+ my $str = $_[0]; # octets
+ return undef if !defined $str;
+ my $chars; # logical characters
+
+ if ($str !~ tr/\x00-\x7F//c) { # is all-ASCII
+ # test for any RFC 2047 encoded-words
+ # encoded-text: Any printable ASCII character other than "?" or SPACE
+ # permissive: SPACE and other characters can be observed in Q encoded-word
+ if ($str !~ m{ =\? [^?]* \? (?: [Bb] \? [A-Za-z0-9+/=]*? |
+ [Qq] \? .*? ) \?= }xs) {
+ return $str; # good, keep as-is, all-ASCII with no encoded-words
+ }
+ # normal, all-ASCII with some encoded-words, try to decode encoded-words
+ eval { $chars = safe_decode('MIME-Header',$str); 1 } # RFC 2047
+ and return $chars;
+ # give up, is all-ASCII but not MIME, just return as-is
+ return $str;
+ }
+
+ # contains at least some non-ASCII
+
+ if ($str =~ m{ =\? [^?]* \? (?: [Bb] \? [A-Za-z0-9+/=]* |
+ [Qq] \? [\x20-\x3E\x40-\x7F]* ) \?= }xs) {
+ # strange/rare, non-ASCII, but also contains RFC 2047 encoded-words !?
+ # decode any RFC 2047 encoded-words, attempt to decode the rest
+ # as UTF-8 if valid, or as ISO-8859-1 otherwise
+ local($1);
+ $str =~ s{ ( =\? [^?]* \? (?: [Bb] \? [A-Za-z0-9+/=]* |
+ [Qq] \? [\x20-\x3E\x40-\x7F]* ) \?= ) |
+ ( [^=]* | . )
+ }{ my $s;
+ if (defined $1) {
+ $s = $1;
+ eval { $s = safe_decode('MIME-Header',$s) };
+ } else {
+ $s = $2;
+ eval { $s = safe_decode_utf8($s, 1|8); 1 }
+ or do { $s = safe_decode_latin1($s) };
+ }
+ $s;
+ }xgse;
+ return $str;
+ }
+
+ # contains at least some non-ASCII and no RFC 2047 encoded-words
+
+ # non-MIME-encoded KOI8 seems to be pretty common, attempt some guesswork
+ if (length($str) >= 4 &&
+ $str !~ tr/\x80-\xA2\xA5\xA8-\xAC\xAE-\xB2\xB5\xB8-\xBC\xBE-\xBF//) {
+ # does *not* contain UTF8-tail octets (sans KOI8-U letters in that range)
+ my $koi8_cyr_lett_cnt = # count cyrillic letters
+ $str =~ tr/\xA3\xA4\xA6\xA7\xAD\xB3\xB4\xB6\xB7\xBD\xC0-\xFF//;
+ if ($koi8_cyr_lett_cnt >= length($str)*2/3 && # mostly cyrillic letters
+ ($str =~ tr/A-Za-z//) <= 5 && # not many ASCII letters
+ !is_valid_utf_8($str) ) {
+ # try decoding as KOI8-U (like KOI8-R but with 8 extra letters)
+ eval { $chars = safe_decode('KOI8-U',$str,1|8); 1; }
+ and return $chars; # hopefully the result makes sense
+ }
+ }
+
+ # contains at least some non-ASCII, no RFC 2047 encoded-words, not KOI8
+
+ if ($enc_taintsafe || !tainted($str)) {
+ # FB_CROAK | LEAVE_SRC
+ eval { $chars = $enc_utf8->decode($str,1|8); 1; } # try strict UTF-8
+ and return $chars;
+ return $enc_latin1->decode($str); # fallback, assumed Latin1
+ } else { # work around bugs in Encode
+ untaint_inplace($str);
+ eval { $chars = $enc_utf8->decode($str,1|8); 1; } # try strict UTF-8
+ and return $enc_tainted . $chars;
+ return $enc_tainted . $enc_latin1->decode($str); # assumed Latin1
+ }
+}
+
# Do the Q-encoding manually, the MIME::Words::encode_mimeword does not
# encode spaces and does not limit to 75 ch, which violates the RFC 2047
#
@@ -2898,15 +3134,15 @@ sub q_encode($$$) {
my $suffix = '?='; local($1,$2,$3);
# FWS | utext (= NO-WS-CTL|rest of US-ASCII)
$octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?)
- ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx;
+ ( [ \t] [\001-\011\013\014\016-\177]* )? \z/xs;
my($head,$rest,$tail) = ($1,$2,$3);
# Q-encode $rest according to RFC 2047 (not for use in comments or phrase)
- $rest =~ s{([\000-\037\177\200-\377=?_])}{sprintf('=%02X',ord($1))}egs;
+ $rest =~ s{([\000-\037\177\200-\377=?_])}{sprintf('=%02X',ord($1))}gse;
$rest =~ tr/ /_/; # turn spaces into _ (RFC 2047 allows it)
my $s = $head; my $len = 75 - (length($prefix)+length($suffix)) - 2;
while ($rest ne '') {
$s .= ' ' if $s !~ /[ \t]\z/; # encoded words must be separated by FWS
- $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx;
+ $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/xs;
$s .= $prefix.$1.$suffix; $rest = $2;
}
$s.$tail;
@@ -2916,8 +3152,8 @@ sub q_encode($$$) {
#
sub xtext_encode($) { # RFC 3461
my $str = $_[0]; local($1);
- $str = safe_encode_utf8($str) if $enc_is_utf8_buggy || Encode::is_utf8($str);
- $str =~ s/([^\041-\052\054-\074\076-\176])/sprintf('+%02X',ord($1))/egs;
+ $str = safe_encode_utf8($str); # to octets (if not already)
+ $str =~ s/([^\041-\052\054-\074\076-\176])/sprintf('+%02X',ord($1))/gse;
$str;
}
@@ -2925,7 +3161,7 @@ sub xtext_encode($) { # RFC 3461
#
sub xtext_decode($) {
my $str = $_[0]; local($1);
- $str =~ s/\+([0-9a-fA-F]{2})/pack('C',hex($1))/egs;
+ $str =~ s/\+([0-9a-fA-F]{2})/pack('C',hex($1))/gse;
$str;
}
@@ -2933,90 +3169,254 @@ sub proto_encode($@) {
my($attribute_name, at strings) = @_; local($1);
for ($attribute_name, at strings) {
# just in case, handle non-octet characters:
- s/([^\000-\377])/sprintf('\\x{%04x}',ord($1))/egs and
+ s/([^\000-\377])/sprintf('\\x{%04x}',ord($1))/gse and
do_log(-1,'proto_encode: non-octet character encountered: %s', $_);
}
$attribute_name =~ # encode all but alfanumerics, . _ + -
- s/([^0-9a-zA-Z._+-])/sprintf('%%%02x',ord($1))/egs;
+ s/([^0-9a-zA-Z._+-])/sprintf('%%%02x',ord($1))/gse;
for (@strings) { # encode % and nonprintables
- s/([^\041-\044\046-\176])/sprintf('%%%02x',ord($1))/egs;
+ s/([^\041-\044\046-\176])/sprintf('%%%02x',ord($1))/gse;
}
$attribute_name . '=' . join(' ', at strings);
}
sub proto_decode($) {
my $str = $_[0]; local($1);
- $str =~ s/%([0-9a-fA-F]{2})/pack('C',hex($1))/egs;
+ $str =~ s/%([0-9a-fA-F]{2})/pack('C',hex($1))/gse;
$str;
}
-# xtext_encode and prepend 'rfc822;' to form a string to be used as ORCPT
+# Expects an e-mail address as a string of octets, where a local part
+# may be encoded as UTF-8, and the domain part may be an international
+# domain name (IDN) consisting either of U-labels or A-labels or NR-LDH
+# labels. Decodes A-labels to U-labels in domain name. If $result_as_octets
+# is false decodes the resulting UTF-8 octets from previous step and returns
+# a string of characters. If $result_as_octets is true the subroutine skips
+# decoding of UTF-8 octets, the result will be a string of octets, only valid
+# as UTF-8 if the provided $addr was a valid UTF-8 (garbage-in/garbage-out).
+#
+sub mail_addr_decode($;$) {
+ my($addr, $result_as_octets) = @_;
+ return undef if !defined $addr;
+ $addr = safe_encode_utf8($addr); # to octets (if not already)
+ local($1); my $domain;
+ my $bracketed = $addr =~ s/^<(.*)>\z/$1/s;
+ if ($addr =~ s{ \@ ( [^\@]* ) \z}{}xs) {
+ $domain = $1;
+ $domain = idn_to_utf8($domain) if $domain =~ /(?:^|\.)xn--/si;
+ if ($domain !~ tr/\x00-\x7F//c) { # all-ASCII
+ $domain = lc $domain;
+ } elsif (!$result_as_octets) { # non-ASCII, attempt decoding UTF-8
+ # attempt decoding as strict UTF-8, otherwise fall back to Latin1
+ # Not lowercased.
+ eval { $domain = safe_decode_utf8($domain, 1|8); 1 }
+ or do { $domain = safe_decode_latin1($domain) };
+ }
+ }
+ # deal with localpart
+ if (!$result_as_octets && $addr =~ tr/\x00-\x7F//c) { # non-ASCII
+ # attempt decoding as strict UTF-8, otherwise fall back to Latin1
+ eval { $addr = safe_decode_utf8($addr, 1|8); 1 }
+ or do { $addr = safe_decode_latin1($addr) };
+ }
+ $addr .= '@'.$domain if defined $domain; # put back the domain part
+ $bracketed ? '<'.$addr.'>' : $addr;
+}
+
+# Expects an e-mail address as a string of octets or as logical characters
+# (with utf8 flag on), where a local part may be encoded as UTF-8, and the
+# domain part may be an international domain name (IDN) consisting either
+# of U-labels or A-labels or NR-LDH. Leaves the localpart unchanged, encodes
+# the domain name to ASCII-compatible encoding (ACE) if it is non-ASCII.
+# The result is always in octets (UTF-8), domain part is lowercased.
+#
+sub mail_addr_idn_to_ascii($) {
+ my $addr = $_[0];
+ return undef if !defined $addr;
+ $addr = safe_encode_utf8($addr); # to octets (if not already)
+ local($1);
+ my $bracketed = $addr =~ s/^<(.*)>\z/$1/s;
+ $addr =~ s{ (\@ [^\@]*) \z }{ idn_to_ascii($1) }xse;
+ $bracketed ? '<'.$addr.'>' : $addr;
+}
+
+# RFC 6533: encode an ORCPT mail address (as obtained from orcpt_decode,
+# logical characters (utf8 flag may be on)) into one of the forms:
+# utf-8-address, utf-8-addr-unitext, utf-8-addr-xtext, or as a legacy
+# xtext (RFC 3461), returning a string of octets
#
-sub orcpt_encode($) { # RFC 3461
+sub orcpt_encode($;$$) {
+ my($str, $smtputf8, $encode_for_smtp) = @_;
+ return (undef,undef) if !defined $str;
+
+ # "Original-Recipient" ":" address-type ";" generic-address
+ # address-type = atom
+ # atom = [CFWS] 1*atext [CFWS]
+
# RFC 3461: Due to limitations in the Delivery Status Notification format,
# the value of the original recipient address prior to encoding as "xtext"
# MUST consist entirely of printable (graphic and white space) characters
# from the US-ASCII [4] repertoire.
- my $str = $_[0]; local($1); # argument should be SMTP-quoted address
- $str = $1 if $str =~ /^<(.*)>\z/s; # strip-off <>
- $str =~ s/[^\040-\176]/?/gs;
- 'rfc822;' . xtext_encode($str);
-}
-
-sub orcpt_decode($) { # RFC 3461
- my $str = $_[0]; # argument should be RFC 3461 -encoded address
- my($addr_type,$orcpt); local($1,$2);
- if (defined $str) {
- if ($str =~ /^([^\000-\040\177()<>\[\]\@\\:;,."]*);(.*\z)/si){ # atom;xtext
- ($addr_type,$orcpt) = ($1,$2);
+
+ my $addr_type = ''; # expected 'rfc822' or 'utf-8', possibly empty
+ local($1); # get address-type (atom, up to a semicolon) and remove it
+ if ($str =~ s{^[ \t]*([0-9A-Za-z!\#\$%&'*/=?^_`{|}~+-]*)[ \t]*;[ \t]*}{}s) {
+ $addr_type = lc $1;
+ }
+ ll(5) && do_log(5, 'orcpt_encode %s, %s%s%s%s',
+ $addr_type, $str,
+ $smtputf8 ? ', smtputf8' : '',
+ $encode_for_smtp ? ', encode_for_smtp' : '',
+ Encode::is_utf8($str) ? ', is_utf8' : '');
+ $str = $1 if $str =~ /^<(.*)>\z/s;
+
+ if ($smtputf8 && Encode::is_utf8($str) &&
+ ($addr_type eq 'utf-8' || $str =~ tr/\x00-\x7F//c)) {
+ # for use in SMTPUTF8 (RCPT TO) or in message/global-delivery-status
+ if ($encode_for_smtp && $str =~ tr{\x00-\x20+=\\}{}) {
+ # contains +,=,\,SP,ctrl -> encode as utf-8-addr-unitext
+ # HEXPOINT in EmbeddedUnicodeChar is 2 to 6 hexadecimal digits.
+ $str =~ s{ ( [^\x21-\x2A\x2C-\x3C\x3E-\x5B\x5D-\x7E\x80-\xF4] ) }
+ { sprintf('\\x{%02X}', ord($1)) }xgse; # 2..6 uppercase hex!
+ } else {
+ # no restricted characters or not for SMTP -> keep as utf-8-address
+ #
+ # The utf-8-address form MAY be used in the ORCPT parameter when the
+ # SMTP server also advertises support for SMTPUTF8 and the address
+ # doesn't contain any ASCII characters not permitted in the ORCPT
+ # parameter. It SHOULD be used in a message/global-delivery-status
+ # "Original-Recipient:" or "Final-Recipient:" DSN field, or in an
+ # "Original-Recipient:" header field [RFC3798] if the message is a
+ # SMTPUTF8 message.
+ }
+ $str = safe_encode_utf8($str); # to octets (if not already)
+ $addr_type = 'utf-8';
+
+ } else {
+ # RFC 6533: utf-8-addr-xtext MUST be used in the ORCPT parameter
+ # when the SMTP server doesn't advertise support for SMTPUTF8
+ if ($str =~ tr/\x00-\x7F//c && Encode::is_utf8($str)) {
+ # non-ASCII UTF-8, encode as utf-8-addr-xtext
+ # RFC 6533: QCHAR = %x21-2a / %x2c-3c / %x3e-5b / %x5d-7e
+ # HEXPOINT in EmbeddedUnicodeChar is 2 to 6 hexadecimal digits.
+ $str =~ s{ ( [^\x21-\x2A\x2C-\x3C\x3E-\x5B\x5D-\x7E] ) }
+ { sprintf('\\x{%02X}', ord($1)) }xgse; # 2..6 uppercase hex!
+ $str = safe_encode_utf8($str); # to octets (if not already)
+ $addr_type = 'utf-8';
+ } else { # encode as legacy RFC 3461 xtext
+ # encode +, =, \, SP, controls
+ $str = safe_encode_utf8($str); # encode to octets first!
+ $str =~ s{ ( [^\x21-\x2A\x2C-\x3C\x3E-\x5B\x5D-\x7E] ) }
+ { sprintf('+%02X', ord($1)) }xgse; # exactly two uppercase hex
+ $addr_type = 'rfc822';
+ }
+ }
+ ($addr_type, $str);
+}
+
+# Decode an encoded ORCPT e-mail address (a string of octets, encoded as
+# xtext, utf-8-addr-xtext, utf-8-addr-unitext, or utf-8-address) as per
+# RFC 3461 and RFC 6533. Result is presumably an RFC 5322 -encoded mail
+# address, possibly as utf8-flagged characters string (if valid UTF-8),
+# no angle brackets.
+#
+sub orcpt_decode($;$) {
+ my($str, $smtputf8) = @_;
+ return (undef,undef) if !defined $str;
+
+ my $addr_type = ''; local($1);
+ # get address-type (atom, up to a semicolon) and remove it
+ if ($str =~ s{^[ \t]*([0-9A-Za-z!\#\$%&'*/=?^_`{|}~+-]*)[ \t]*;[ \t]*}{}s) {
+ $addr_type = lc $1;
+ }
+
+ if ($addr_type eq '') {
+ # assumed not encoded (e.g. internally generated)
+ if ($str =~ tr/\x00-\x7F//c && is_valid_utf_8($str) &&
+ eval { $str = safe_decode_utf8($str, 1|8); 1 }) {
+ $addr_type = 'utf-8';
} else {
- ($addr_type,$orcpt) = ('rfc822',$str); # RFC 3464 address-type
+ $addr_type = 'rfc822';
+ }
+
+ } elsif ($addr_type ne 'utf-8') { # presumably 'rfc822'
+ # decode xtext-encoded string as per RFC 3461,
+ # hexchar = ASCII "+" immediately followed by two UPPER CASE hex digits
+ $str =~ s{ \+ ( [0-9A-F]{2} ) }{ pack('C',hex($1)) }xgse;
+ # now have a string of octets, possibly with (invalid) 8bit characters
+
+ # we may have a legacy encoding which should really be a utf-8 addr_type
+ if ($smtputf8 && lc $addr_type eq 'rfc822' &&
+ $str =~ tr/\x00-\x7F//c && is_valid_utf_8($str) &&
+ eval { $str = safe_decode_utf8($str, 1|8); 1 }) {
+ $addr_type = 'utf-8';
}
- $orcpt = xtext_decode($orcpt); # decode
- $orcpt =~ s/[^\040-\176]/?/gs; # some minimal sanitation
+
+ } elsif ($str !~ tr/\x00-\x7F//c) { # address-type is 'utf-8', is all-ASCII
+ # Looks like utf-8-addr-xtext or utf-8-addr-unitext.
+ # Permissive decoding of EmbeddedUnicodeChar, as well as a legacy xtext:
+ # RFC 6533: UTF-8 address type has 3 forms:
+ # utf-8-addr-xtext, utf-8-addr-unitext, and utf-8-address.
+ $str =~ s{ \\ x \{ ( [0-9A-Fa-f]{2,6} ) \} |
+ \+ ( [0-9A-F]{2} ) }
+ { pack('U', hex(defined $1 ? $1 : $2)) }xgse;
+ # RFC 6533 prohibits <NUL> and surrogates in EmbeddedUnicodeChar,
+ # as well as encoded printable ASCII chars except xtext-specials +, =, \
+
+ } elsif (is_valid_utf_8($str) &&
+ eval { $str = safe_decode_utf8($str, 1|8); 1 }) {
+ # Looks like a utf-8-address. Successfully decoded UTF-8 octets to chars.
+ # permissive decoding of EmbeddedUnicodeChar, as well as a legacy xtext
+ $str =~ s{ \\ x \{ ( [0-9A-Fa-f]{2,6} ) \} |
+ \+ ( [0-9A-F]{2} ) }
+ { pack('U', hex(defined $1 ? $1 : $2)) }xgse;
+
+ } else { # address-type is 'utf-8', non-ASCII, invalid UTF-8 string
+ # RFC 6533: if an address is labeled with the UTF-8 address type
+ # but does not conform to utf-8 syntax, then it MUST be copied into
+ # the message/global-delivery-status field without alteration.
+ # --> just leave unchanged
}
- # result in $orcpt is presumably an RFC 5322 -encoded addr, no angle brackets
- ($addr_type,$orcpt);
+
+ # result in $str is presumably an RFC 5322 -encoded addr,
+ # possibly as utf8-flagged characters, no angle brackets
+ ($addr_type, $str);
}
# Mostly for debugging and reporting purposes:
# Convert nonprintable characters in the argument
-# to \[rnftbe], or \octal code, ( and '\' to '\\' ???),
+# to \[rnftbe], or hex code, ( and '\' to '\\' ???),
# and Unicode characters to UTF-8, returning a sanitized string.
#
use vars qw(%quote_controls_map);
BEGIN {
%quote_controls_map =
- ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t',
- "\b" => '\\b', "\e" => '\\e' ); # "\\" => '\\\\'
+ ("\r" => '\\r', "\n" => '\\n', "\t" => '\\t', "\\" => '\\\\');
+
+# leave out the <FF>, <BS> and <ESC>, these are too confusing in the log,
+# better to just hand them over to hex quoting ( \xHH )
+# ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t',
+# "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\');
+
}
sub sanitize_str {
my($str, $keep_eol) = @_;
return '' if !defined $str;
- $str = safe_encode_utf8($str) if $enc_is_utf8_buggy || Encode::is_utf8($str);
+ $str = safe_encode_utf8($str);
+ # $str is now in octets, UTF8 flag is off
local($1);
if ($keep_eol) {
- $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ?
- exists($quote_controls_map{$1}) ? $quote_controls_map{$1} :
- sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/egs;
+ # controls except LF, DEL, backslash
+ $str =~ s/([\x00-\x09\x0B-\x1F\x7F\\])/
+ $quote_controls_map{$1} || sprintf('\\x%02X', ord($1))/gse;
} else {
- $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ?
- exists($quote_controls_map{$1}) ? $quote_controls_map{$1} :
- sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/egs;
+ # controls, DEL, backslash
+ $str =~ s/([\x00-\x1F\x7F\\])/
+ $quote_controls_map{$1} || sprintf('\\x%02X', ord($1))/gse;
}
$str;
}
-sub sanitize_str_inplace {
- $_[0] = safe_encode_utf8($_[0]) if $enc_is_utf8_buggy ||
- Encode::is_utf8($_[0]);
- local($1);
- $_[0] =~ s/([^\040-\133\135-\176])/ # and \240-\376 ?
- exists($quote_controls_map{$1}) ? $quote_controls_map{$1} :
- sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/egs;
- 1;
-}
-
# Set or get Amavis internal task id (also called: log id).
# This task id performs a similar function as queue-id in MTA responses.
# It may only be used in generating text part of SMTP responses,
@@ -3046,6 +3446,7 @@ use vars qw($entropy); # MD5 ctx (128 bits, 32 hex digits or 22 base64 chars)
sub add_entropy(@) { # arguments may be strings or array references
$entropy = Digest::MD5->new if !defined $entropy;
my $s = join(',', map((!defined $_ ? 'U' : ref eq 'ARRAY' ? @$_ : $_), @_));
+ $s = $enc_utf8->encode($s) if Encode::is_utf8($s);
# do_log(5,'add_entropy: %s',$s);
$entropy->add($s);
}
@@ -3202,14 +3603,15 @@ sub snmp_counters_get() { \@counter_names }
sub snmp_initial_oids() {
return [
- ['sysDescr', 'STR', $myversion],
+ ['sysDescr', 'STR', $myversion], # 0..255 octets
['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2'],
# iso.org.dod.internet.private.enterprise.ijs.amavisd-new
['sysUpTime', 'INT', int(time)], # to be converted to TIM
# later it must be converted to timeticks (10ms units since start)
- ['sysContact', 'STR', $snmp_contact],
- ['sysName', 'STR', $myhostname],
- ['sysLocation', 'STR', $snmp_location],
+ ['sysContact', 'STR', safe_encode_utf8($snmp_contact)], # 0..255 octets
+ # Network Unicode format (Net-Unicode) RFC 5198, instead of NVT ASCII
+ ['sysName', 'STR', idn_to_utf8(c('myhostname'))], # 0..255 octets
+ ['sysLocation', 'STR', safe_encode_utf8($snmp_location)], # 0..255 octets
['sysServices', 'INT', 64], # application
];
}
@@ -3269,9 +3671,24 @@ sub do_log($$;@) {
( ($DEBUG || $debug_oneshot) && $level > 0
&& 0 <= $current_config_log_level ) ||
$dbg_log ) {
- # treat $errmsg as sprintf format string if additional args are provided
- my $errmsg = @_ <= 2 ? $_[1] : sprintf($_[1], @_[2..$#_]);
- sanitize_str_inplace($errmsg);
+ my $errmsg; # the $_[1] is expected to be all-ASCII (for now)
+ if (@_ <= 2) {
+ $errmsg = $_[1];
+ } elsif (@_ == 3) { # optimized common case
+ $errmsg = sprintf($_[1], Encode::is_utf8($_[2])? $enc_utf8->encode($_[2])
+ : $_[2]);
+ } else {
+ # treat $errmsg as sprintf format string if additional args are provided;
+ # encode arguments individually to avoid mojibake when UTF8-flagged and
+ # non- UTF8-flagged strings are concatenated;
+ # inlined safe_encode_utf8() call for speed, don't care for taint bugs
+ $errmsg = sprintf($_[1], map(Encode::is_utf8($_) ? $enc_utf8->encode($_)
+ : $_, @_[2..$#_]));
+ }
+ local($1);
+ # protect controls, DEL, and backslash
+ $errmsg =~ s/([\x00-\x1F\x7F\\])/
+ $quote_controls_map{$1} || sprintf('\\x%02X', ord($1))/gse;
$dbg_log->write_dbg_log($level,$errmsg) if $dbg_log;
$level = 0 if ($DEBUG || $debug_oneshot) && $level > 0;
if ($level <= $current_config_log_level) {
@@ -3341,7 +3758,7 @@ sub get_deadline(@) {
$time_to_deadline = $timestamp_of_last_reception + $child_t_o - $now;
$timer_interval = $time_to_deadline;
if (!defined $allowed_share) {
- $allowed_share = 0.7;
+ $allowed_share = 0.6;
$timer_interval *= $allowed_share;
} elsif ($allowed_share <= 0) {
$timer_interval = 0;
@@ -3350,20 +3767,20 @@ sub get_deadline(@) {
} else {
$timer_interval *= $allowed_share;
}
- $reserve = 3 if !defined $reserve;
+ $reserve = 4 if !defined $reserve;
if ($reserve > 0 && $timer_interval > $time_to_deadline - $reserve) {
$timer_interval = $time_to_deadline - $reserve;
}
- if ($timer_interval < 8) { # try to be generous
- $timer_interval = max(4, min(8,$time_to_deadline));
+ if ($timer_interval < 8) { # be generous, allow at least 6 seconds
+ $timer_interval = max(6, min(8,$time_to_deadline));
}
my $j = int($timer_interval);
$timer_interval = $timer_interval > $j ? $j+1 : $j; # ceiling
if (defined $max_time && $max_time > 0 && $timer_interval > $max_time) {
$timer_interval = $max_time;
}
- ll(5) && do_log(5, 'get_deadline %s - deadline in %.1f s, set to %.3f s',
- $which_section, $time_to_deadline, $timer_interval);
+ ll(5) && do_log(5,'get_deadline %s - deadline in %.1f s, set to %.3f s',
+ $which_section, $time_to_deadline, $timer_interval);
$timer_deadline = $now + $timer_interval;
}
!wantarray ? $timer_interval
@@ -3408,12 +3825,13 @@ sub switch_to_client_time($) { # processing is now in client's hands
sub fmt_struct($); # prototype
sub fmt_struct($) {
my $arg = $_[0];
- !defined($arg) ? 'undef'
- : !ref($arg) ? '"'.$arg.'"'
- : ref($arg) eq 'ARRAY' ?
- '[' . join(',', map(fmt_struct($_),@$arg)) . ']'
- : ref($arg) eq 'HASH' ?
- '{' . join(',', map($_.'=>'.fmt_struct($arg->{$_}),keys(%$arg))) . '}'
+ my $r = ref $arg;
+ !$r ?
+ (defined($arg) ? '"'.$arg.'"' : 'undef')
+ : $r eq 'ARRAY' ?
+ '[' . join(',', map(fmt_struct($_), @$arg)) . ']'
+ : $r eq 'HASH' ?
+ '{' . join(',', map($_.'=>'.fmt_struct($arg->{$_}), keys %$arg)) . '}'
: $arg;
};
@@ -3421,9 +3839,10 @@ sub fmt_struct($) {
#
sub st_encode($) {
my $str = $_[0]; local($1);
- { # concession on a perl 5.20.0 bug [perl #122148] - just warn, do not abort
+ { # concession on a perl 5.20.0 bug [perl #122148] (fixed in 5.20.1)
+ # - just warn, do not abort
use warnings NONFATAL => qw(utf8);
- $str =~ s/([%~\000\200])/sprintf('%%%02X',ord($1))/egs;
+ $str =~ s/([%~\000\200])/sprintf('%%%02X',ord($1))/gse;
};
$str;
}
@@ -3451,7 +3870,7 @@ sub thaw($) {
my $str = $_[0];
return undef if !defined $str; # must return undef even in a list context!
my($ty, at val) = split(/~/,$str,-1);
- for (@val) { s/%([0-9a-fA-F]{2})/pack('C',hex($1))/egs }
+ s/%([0-9a-fA-F]{2})/pack('C',hex($1))/gse for @val;
if ($ty eq 'U') { undef }
elsif ($ty eq '') { $val[0] }
elsif ($ty eq 'S') { my $obj = thaw($val[0]); \$obj }
@@ -3593,7 +4012,7 @@ sub rmdir_recursively($;$) {
last;
}
}
- # perl5.20: readdir() now only sets $! on error. $! is no longer
+ # fixed by perl5.20: readdir() now only sets $! on error. $! is no longer
# set to EBADF when then terminating undef is read from the directory
# unless the system call sets $!. [perl #118651]
closedir(DIR) or die "Error closing directory $dir: $!";
@@ -3742,6 +4161,7 @@ sub read_l10n_templates($;$) {
# empty lines (containing only whitespace or comment) are ignored.
# Addresses (lefthand-side) are converted from RFC 5321 -quoted form
# into internal (raw) form and inserted as keys into a given hash.
+# International domain names (IDN) in UTF-8 are encoded to ASCII.
# NOTE: the format is partly compatible with Postfix maps (not aliases):
# no continuation lines are honoured, Postfix maps do not allow
# RFC 5321 -quoted addresses containing whitespace, Postfix only allows
@@ -3769,7 +4189,7 @@ sub read_hash(@) {
# carefully handle comments, '#' within "" does not count as a comment
my $lhs = ''; my $rhs = ''; my $at_rhs = 0; my $trailing_comment = 0;
for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
- [^#" \t]+ | [ \t]+ | . )/gsx) {
+ [^#" \t]+ | [ \t]+ | . )/xgs) {
if ($t eq '#') { $trailing_comment = 1; last }
if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 }
else { ($at_rhs ? $rhs : $lhs) .= $t }
@@ -3777,10 +4197,10 @@ sub read_hash(@) {
$rhs =~ s/[ \t]+\z// if $trailing_comment ||
$trim_trailing_space_in_lookup_result_fields;
next if $lhs eq '' && $rhs eq '';
- my($source_route,$localpart,$domain) =
- Amavis::rfc2821_2822_Tools::parse_quoted_rfc2821($lhs,1);
+ my($source_route, $localpart, $domain) =
+ Amavis::rfc2821_2822_Tools::parse_quoted_rfc2821($lhs,1);
$localpart = lc($localpart) if !$lpcs;
- my $addr = $localpart . lc($domain);
+ my $addr = $localpart . idn_to_ascii($domain);
$hashref->{$addr} = $rhs eq '' ? 1 : $rhs;
# do_log(5, 'read_hash: address: <%s>: %s', $addr, $hashref->{$addr});
}
@@ -3801,7 +4221,7 @@ sub read_array(@) {
chomp($ln); my $lhs = '';
# carefully handle comments, '#' within "" does not count as a comment
for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
- [^#" \t]+ | [ \t]+ | . )/gsx) {
+ [^#" \t]+ | [ \t]+ | . )/xgs) {
last if $t eq '#';
$lhs .= $t;
}
@@ -3896,13 +4316,26 @@ sub dump_array($) {
do_log(0, 'dump_array: %s', $_) for @$ar;
}
+# use Devel::Symdump;
+# sub dump_subs() {
+# my $obj = Devel::Symdump->rnew;
+# # list of all subroutine names and their memory addresses
+# my @a = map([$_, \&$_], $obj->functions, $obj->scalars,
+# $obj->arrays, $obj->hashes);
+# open(SUBLIST, ">/tmp/1.log") or die "Can't create a file: $!";
+# for my $s (sort { $a->[1] <=> $b->[1] } @a) { # sorted by memory address
+# printf SUBLIST ("%s %s\n", $s->[1], $s->[0]);
+# }
+# close(SUBLIST) or die "Can't close a file: $!";
+# }
+
# (deprecated, only still used with Amavis::OS_Fingerprint)
sub dynamic_destination($$) {
my($method,$conn) = @_;
if ($method =~ /^(?:[a-z][a-z0-9.+-]*)?:/si) {
my(@list); $list[0] = ''; my $j = 0;
for ($method =~ /\G \[ (?: \\. | [^\]\\] )* \] | " (?: \\. | [^"\\] )* "
- | : | [ \t]+ | [^:"\[ \t]+ | . /gsx) { # real parsing
+ | : | [ \t]+ | [^:"\[ \t]+ | . /xgs) { # real parsing
if ($_ eq ':') { $list[++$j] = '' } else { $list[$j] .= $_ }
};
if ($list[1] =~ m{^/}) {
@@ -3910,11 +4343,11 @@ sub dynamic_destination($$) {
} else {
my $new_method; my($proto,$relayhost,$relayport) = @list;
if ($relayhost eq '*') {
- my $client_ip; $client_ip = $conn->client_ip if defined $conn;
+ my $client_ip; $client_ip = $conn->client_ip if $conn;
$relayhost = "[$client_ip]" if defined $client_ip && $client_ip ne '';
}
if ($relayport eq '*') {
- my $socket_port; $socket_port = $conn->socket_port if defined $conn;
+ my $socket_port; $socket_port = $conn->socket_port if $conn;
$relayport = $socket_port + 1
if defined $socket_port && $socket_port ne '';
}
@@ -3973,12 +4406,12 @@ package Amavis::JSON;
use strict;
use re 'taint';
-# serialize a data structure to JSON, RFC 4627
+# serialize a data structure to JSON, RFC 7159
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&boolean &numeric);
}
@@ -3989,12 +4422,15 @@ our %jesc = ( # JSON escaping
"\x08" => '\\b', "\x09" => '\\t',
"\x0A" => '\\n', "\x0C" => '\\f', "\x0D" => '\\r',
"\x{2028}" => '\\u2028', "\x{2029}" => '\\u2029' );
+ # escape also the Line Separator (U+2028) and Paragraph Separator (U+2029)
+ # http://timelessrepo.com/json-isnt-a-javascript-subset
our($FALSE, $TRUE) = ('false', 'true');
sub boolean { bless($_[0] ? \$TRUE : \$FALSE) }
sub numeric { my $value = $_[0]; bless(\$value) }
-# serialize a data structure to JSON, RFC 4627
+# serialize a data structure to JSON, RFC 7159
+# expects logical characters in scalars, returns a string of logical chars
#
sub encode($); # prototype
sub encode($) {
@@ -4009,9 +4445,8 @@ sub encode($) {
join(',',
map {
my $k = $_;
- $k =~ s{ ([\x00-\x1f\x7f\x{2028}\x{2029}"\\]) }
- { exists $jesc{$1} ? $jesc{$1}
- : sprintf('\\u%.4X',ord($1)) }xgse;
+ $k =~ s{ ([\x00-\x1F\x7F\x{2028}\x{2029}"\\]) }
+ { $jesc{$1} || sprintf('\\u%04X',ord($1)) }xgse;
'"' . $k . '":' . encode($val->{$_});
} sort keys %$val
) . '}';
@@ -4021,12 +4456,13 @@ sub encode($) {
# fall through, encode other refs as strings, helps debugging
}
return 'null' if !defined $val;
- { # concession on a perl 5.20.0 bug [perl #122148] - just warn, do not abort
+ { # concession on a perl 5.20.0 bug [perl #122148] (fixed in 5.20.1)
+ # - just warn, do not abort
use warnings NONFATAL => qw(utf8);
- $val =~ s{ ([\x00-\x1f\x7f\x{2028}\x{2029}"\\]) }
- { exists $jesc{$1} ? $jesc{$1} : sprintf('\\u%.4X',ord($1)) }xgse;
+ $val =~ s{ ([\x00-\x1F\x7F\x{2028}\x{2029}"\\]) }
+ { $jesc{$1} || sprintf('\\u%04X',ord($1)) }xgse;
};
- return '"' . $val . '"';
+ '"' . $val . '"';
}
1;
@@ -4039,7 +4475,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&exit_status_str &proc_status_ok &kill_proc &cloexec
&run_command &run_command_consumer &run_as_subprocess
@@ -4299,7 +4735,8 @@ sub run_command($$@) {
do_log_safe(-1,"run_command: child process [%s]: %s", $$,$err);
} or 1; # ignore failures, make perlcritic happy
{ # no warnings;
- POSIX::_exit(6); # avoid END and destructor processing
+ POSIX::_exit(3); # SIGQUIT, avoid END and destructor processing
+ # POSIX::_exit(6); # SIGABRT, avoid END and destructor processing
kill('KILL',$$); exit 1; # still kicking? die!
}
}
@@ -4375,7 +4812,8 @@ sub run_command_consumer($$@) {
do_log_safe(-1,"run_command_consumer: child process [%s]: %s", $$,$err);
} or 1; # ignore failures, make perlcritic happy
{ # no warnings;
- POSIX::_exit(6); # avoid END and destructor processing
+ POSIX::_exit(3); # SIGQUIT, avoid END and destructor processing
+ # POSIX::_exit(6); # SIGABRT, avoid END and destructor processing
kill('KILL',$$); exit 1; # still kicking? die!
}
}
@@ -4479,7 +4917,8 @@ sub run_as_subprocess($@) {
do_log_safe($ll, 'run_as_subprocess: child process [%s]: %s',
$myownpid, $eval2_stat);
} or 1; # ignore failures, make perlcritic happy
- POSIX::_exit(6); # avoid END and destructor processing in a subprocess
+ POSIX::_exit(3); # SIGQUIT, avoid END and destructor processing
+ # POSIX::_exit(6); # SIGABRT, avoid END and destructor processing
}
# parent
ll(5) && do_log(5,"run_as_subprocess: spawned a subprocess [%s]", $pid);
@@ -4586,7 +5025,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT = qw(
&rfc2822_timestamp &rfc2822_utc_timestamp
@@ -4594,13 +5033,15 @@ BEGIN {
&iso8601_week &iso8601_yearweek &iso8601_year_and_week &iso8601_weekday
&make_received_header_field &parse_received
&fish_out_ip_from_received &parse_message_id
- &split_address &split_localpart &replace_addr_fields &make_query_keys
+ &split_address &split_localpart &replace_addr_fields
+ &clear_query_keys_cache &make_query_keys
"e_rfc2821_local &qquote_rfc2821_local
&parse_quoted_rfc2821 &unquote_rfc2821_local &parse_address_list
&wrap_string &wrap_smtp_resp &one_response_for_all
&EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM);
import Amavis::Conf qw(:platform c cr ca $myproduct_name);
- import Amavis::Util qw(ll do_log unique_ref unique_list);
+ import Amavis::Util qw(ll do_log unique_ref unique_list safe_encode_utf8
+ idn_to_ascii idn_to_utf8 mail_addr_idn_to_ascii);
}
use subs @EXPORT;
@@ -4658,7 +5099,7 @@ sub rfc2822_timestamp($) {
}
# Given a Unix time, provide date-time timestamp as specified in RFC 5322
-# in an UTC time zone. See also RFC 3339 and RFC 6692.
+# in a UTC time zone. See also RFC 3339 and RFC 6692.
#
sub rfc2822_utc_timestamp($) {
my $t = $_[0];
@@ -4745,19 +5186,33 @@ sub make_received_header_field($$) {
# RFC 5321 (ex RFC 2821), section 4.1.3
$_ = 'IPv6:'.$_ if /:[0-9a-f]*:/i && !/^IPv6:/is;
}
+ my $myhost = c('myhostname'); # my FQDN (DNS) name, UTF-8 octets
+ my $myhelo = c('localhost_name'); # my EHLO/HELO/LHLO name, UTF-8 octets
+ $myhelo = 'localhost' if $myhelo eq '';
+ if ($msginfo->smtputf8) {
+ $myhost = idn_to_utf8($myhost); $myhelo = idn_to_utf8($myhelo);
+ } else {
+ $myhost = idn_to_ascii($myhost); $myhelo = idn_to_ascii($myhelo);
+ }
my $tls = $msginfo->tls_cipher;
my $s = sprintf("from %s%s%s\n by %s%s (%s, %s)",
$conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo,
$client_ip eq '' ? '' : " ([$client_ip])",
!defined $tls ? '' : " (using TLS with cipher $tls)",
- c('localhost_name'),
- $socket_ip eq '' ? '' : sprintf(" (%s [%s])", c('myhostname'), $socket_ip),
+ $myhelo,
+ $socket_ip eq '' ? '' : sprintf(" (%s [%s])", $myhost, $socket_ip),
$myproduct_name,
$conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port);
- $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; #RFC 3848
+ # RFC 3848, RFC 6531
+ # http://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
+ $s .= "\n with $smtp_proto"
+ if $smtp_proto =~ /^ (?: SMTP | (?: ES|L|UTF8S|UTF8L) MTP S? A? ) \z/xsi;
$s .= "\n id $id" if defined $id && $id ne '';
- # do not disclose recipients if more than one
- $s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1;
+ if (@$recips == 1) { # do not disclose recipients if more than one
+ my $recip = $recips->[0];
+ $recip = mail_addr_idn_to_ascii($recip) if !$msginfo->smtputf8;
+ $s .= "\n for " . qquote_rfc2821_local($recip);
+ }
$s .= ";\n " . rfc2822_timestamp($msginfo->rx_time);
$s =~ s/\n//g if !$folded;
$s;
@@ -4851,30 +5306,33 @@ sub parse_received($) {
# like a domain in the optional TCP-info, is actually a comment in CFWS
local($_) = $fld{$f};
if (!defined($_)) {}
- elsif (/\[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) {}
+ elsif (/\[ \d{1,3} (?: \. \d{1,3} ){3} \] /x) {}
elsif (/\[ .* : .* : /x && # triage, contains at least two colons
/\[ (?: IPv6: )? [0-9a-f]{0,4}
(?: : [0-9a-f]{0,4} | \. [0-9]{1,3} ){2,9}
(?: % [A-Z0-9_-]+ )?
\] /xi) {}
# elsif (/ (?: ^ | \D ) ( \d{1,3} (?: \. \d{1,3}){3}) (?! [0-9.] ) /x) {}
- elsif (/^(?: localhost | ( [a-z0-9_\/+-]{1,63} \. )+ [a-z-]{2,} )\b/xi) {}
+ elsif (/^(?: localhost |
+ (?: [\x80-\xF4a-zA-Z0-9_\/+-]{1,63} \. )+
+ [\x80-\xF4a-zA-Z0-9-]{2,} ) \b/xs) {}
else {
my $fc = $f; $fc =~ s/-tcp\z/-com/;
$fld{$fc} = '' if !defined $fld{$fc};
- $fld{$fc} = $_ . (/[ \t]\z/||$fld{$fc}=~/^[ \t]/?'':' ') .$fld{$fc};
+ $fld{$fc} = $_ . (/[ \t]\z/||$fld{$fc}=~/^[ \t]/?'':' ') . $fld{$fc};
delete $fld{$f};
}
}
for (values %fld) { s/[ \t]+\z//; s/^[ \t]+// }
+ delete $fld{""} if exists $fld{""} && $fld{""} eq "";
# for my $f (sort {$fld{$a} cmp $fld{$b}} keys %fld)
# { do_log(5, "RECVD: %-8s -> /%s/", $f,$fld{$f}) }
\%fld;
}
-sub fish_out_ip_from_received($) {
- my $received = $_[0];
- my $fields_ref = parse_received($received);
+sub fish_out_ip_from_received($;$) {
+ my($received,$fields_ref) = @_;
+ $fields_ref = parse_received($received) if !defined $fields_ref;
my $ip; local($1);
for (@$fields_ref{qw(from-tcp from from-com)}) {
next if !defined($_);
@@ -4979,7 +5437,7 @@ sub parse_message_id($) {
< (?: " (?: \\. | [^"\\>] ){0,999} " |
\[ (?: \\. | [^\]\\>]){0,999} \] |
[^"<>\[\]\\]+ )* > |
- [^<( \t]+ | . )/gsx ) {
+ [^<( \t]+ | . )/xgs ) {
if ($t =~ /^<.*>\z/) { push(@message_id,$t) }
elsif ($t =~ /^[ \t]*\z/) {} # ignore FWS
elsif ($t =~ /^\(.*\)\z/) # ignore CFWS
@@ -4998,7 +5456,7 @@ sub parse_message_id($) {
# For a given email address (e.g. for User+Foo at sub.exAMPLE.CoM)
# prepare and return a list of lookup keys in the following order:
-# User+Foo at sub.exAMPLE.COM (as-is, no lowercasing)
+# User+Foo at sub.exAMPLE.COM (as-is, no lowercasing, no ToASCII)
# user+foo at sub.example.com
# user at sub.example.com (only if $recipient_delimiter nonempty)
# user+foo(@) (only if $include_bare_user)
@@ -5008,22 +5466,46 @@ sub parse_message_id($) {
# (@).example.com
# (@).com
# (@).
+# Another example with EAI and international domain names (IDN):
+# Pingüino at Pájaro.Niño.exAMPLE.COM (as-is, no lowercasing, no ToASCII)
+# pingüino at xn--pjaro-xqa.xn--nio-8ma.example.com
+# pingüino(@) (only if $include_bare_user)
+# (@)xn--pjaro-xqa.xn--nio-8ma.example.com
+# (@).xn--pjaro-xqa.xn--nio-8ma.example.com
+# (@).xn--pjaro-xqa.example.com
+# (@).example.com
+# (@).com
+# (@).
+#
# Note about (@): if $at_with_user is true the user-only keys (without domain)
# get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash.
# If $at_with_user is false the domain-only (without localpart) keys
# get a '@' prepended (e.g. '@.example.com'). Usual for SQL and LDAP lookups.
#
-# The domain part is lowercased in all but the first item in the resulting
-# list; the localpart is lowercased iff $localpart_is_case_sensitive is true.
+# The domain part is lowercased and IDN converted to ASCII in all but
+# the first item in the resulting list; the localpart is lowercased
+# iff $localpart_is_case_sensitive is true. The $addr may be a string
+# of octets (assumed to be UTF-8 encoded), or a string of characters.
#
+my %query_keys_cache;
+sub clear_query_keys_cache() { %query_keys_cache = () }
sub make_query_keys($$$;$) {
- my($addr,$at_with_user,$include_bare_user,$append_string) = @_;
- my($localpart,$domain) = split_address($addr); $domain = lc($domain);
+ my($addr, $at_with_user, $include_bare_user, $append_string) = @_;
+ $addr = safe_encode_utf8($addr); # make sure it's in octets
+ my $query_keys_slot = join("\x00",
+ $at_with_user?1:0, $include_bare_user?1:0,
+ $append_string, $addr);
+ if (exists $query_keys_cache{$query_keys_slot}) {
+ do_log(5,'query_keys: cached '.$addr); # concat, knowing it's in octets
+ return @{$query_keys_cache{$query_keys_slot}}; # ($keys_ref, $rhs)
+ }
+ my($localpart, $domain) = split_address($addr);
my $saved_full_localpart = $localpart;
$localpart = lc($localpart) if !c('localpart_is_case_sensitive');
# chop off leading @, and trailing dots
local($1);
$domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
+ $domain = idn_to_ascii($domain) if $domain ne ''; # lowercase, ToASCII
my $extension; my $delim = c('recipient_delimiter');
if ($delim ne '') {
($localpart,$extension) = split_localpart($localpart,$delim);
@@ -5067,8 +5549,9 @@ sub make_query_keys($$$;$) {
$saved_full_localpart, # $2 = User+Foo
$localpart, # $3 = user (lc if localpart_is_case_sensitive)
$extension, # $4 = +foo (lc if localpart_is_case_sensitive)
- $domain, # $5 = sub.example.com (lowercased unconditionally)
+ $domain, # $5 = sub.example.com (lowercase, ToASCII)
];
+ $query_keys_cache{$query_keys_slot} = [$keys_ref, $rhs];
($keys_ref, $rhs);
}
@@ -5081,18 +5564,50 @@ sub make_query_keys($$$;$) {
#
# To re-insert message back via SMTP, the local-part of the address needs
# to be quoted again if it contains reserved characters or otherwise
-# does not obey the dot-atom syntax, as specified in RFC 5321 (ex RFC 2821).
+# does not obey the dot-atom syntax, as specified in RFC 5321 and RFC 6531.
#
sub quote_rfc2821_local($) {
my $mailbox = $_[0];
- # atext: any character except controls, SP, and specials (RFC 5321/RFC 5322)
- my $atext = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-";
+ # RFC 5321/RFC 5322: atext: any character except controls, SP, and specials
+ # RFC 6531 section 3.3: The definition of <atext> is extended to permit
+ # both the RFC 5321 definition and a UTF-8 string. That string MUST NOT
+ # contain any of the ASCII graphics or control characters.
+ # RFC 6531: atext =/ UTF8-non-ascii
+ # qtextSMTP =/ UTF8-non-ascii
+ # RFC 6532: UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
+ # RFC 3629 section 4: Syntax of UTF-8 Byte Sequences
+ # non-atext: [\x00-\x20"(),.:;<>@\[\]\\\x7F]
+ my $atext = "a-zA-Z0-9!\#\$%&'*/=?^_`{|}~+-";
# my $specials = '()<>\[\]\\\\@:;,."';
+ # HTML5 - 4.10.5.1.5 E-mail state (type=email):
+ # email = 1*( atext / "." ) "@" label *( "." label )
+ # i.e. localpart is: [a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+
my($localpart,$domain) = split_address($mailbox);
- if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { # not dot-atom, needs q.
+ if ($localpart =~ /^[$atext]+(?:\.[$atext]+)*\z/so) {
+ # plain RFC 5321 dot-atom, no need for quoting
+ } elsif ($localpart =~ /[\x80-\xBF\xC2-\xF4]/s && # triage, RFC 3629
+ $localpart =~ /^ ( [$atext] |
+ [\xC2-\xDF][\x80-\xBF]{1} |
+ [\xE0-\xEF][\x80-\xBF]{2} |
+ [\xF0-\xF4][\x80-\xBF]{3}
+ )+
+ ( \. ( [$atext] |
+ [\xC2-\xDF][\x80-\xBF]{1} |
+ [\xE0-\xEF][\x80-\xBF]{2} |
+ [\xF0-\xF4][\x80-\xBF]{3}
+ )+
+ )* \z/xso) {
+ # Extended RFC 6531 UTF-8 atext / dot-atom, no need for quoting.
+ # The \xC0 and \xC1 could only be used for overlong encoding of basic
+ # ASCII characters. Tolerate other non-shortest UTF-8 encodings here.
+ # UTF-8 is restricted by RFC 3629 to end at U+10FFFF, this removed
+ # all 5- and 6-byte sequences, and about half of the 4-byte sequences.
+ # The RFC 5198 also prohibits "C1 Controls" (U+0080 through U+009F)
+ # (i.e. in UTF-8: C2 80 .. C2 9F) for Net-Unicode.
+ } else { # needs quoting or is invalid
local($1); # qcontent = qtext / quoted-pair
- $localpart =~ s/([\000-\037\177-\377"\\])/\\$1/g; # quote non-qtext
- $localpart = '"'.$localpart.'"'; # make it a qcontent
+ $localpart =~ s{ ( ["\\] ) }{\\$1}xgs;
+ $localpart = '"'.$localpart.'"'; # non-qtext, make it a qcontent
# Postfix hates ""@domain but is not so harsh on @domain
# Late breaking news: don't bother, both forms are rejected by Postfix
# when strict_rfc821_envelopes=yes, and both are accepted otherwise
@@ -5127,25 +5642,25 @@ sub parse_quoted_rfc2821($$) {
# A-d-l = At-domain *( "," A-d-l )
# At-domain = "@" domain
if (index($addr,':') >= 0 && # triage before more testing for source route
- $addr =~ m{^ ( [ \t]* \@ (?: [0-9A-Za-z.!\#\$%&*/^{}=_+-]* |
- \[ (?: \\. | [^\]\\] ){0,999} \] ) [ \t]*
- (?: , [ \t]* \@ (?: [0-9A-Za-z.!\#\$%&*/^{}=_+-]* |
- \[ (?: \\. | [^\]\\] ){0,999} \] ) [ \t]* )*
- : [ \t]* ) (.*) \z }xs)
+ $addr =~ m{^( [ \t]* \@ (?: [\x80-\xF4A-Za-z0-9.!\#\$%&*/^{}=_+-]* |
+ \[ (?: \\. | [^\]\\] ){0,999} \] ) [ \t]*
+ (?: ,[ \t]* \@ (?: [\x80-\xF4A-Za-z0-9.!\#\$%&*/^{}=_+-]* |
+ \[ (?: \\. | [^\]\\] ){0,999} \] ) [ \t]*
+ )* : [ \t]* ) (.*) \z }xs)
{ # NOTE: we are quite liberal on allowing whitespace around , and : here,
# and liberal in allowed character set and syntax of domain names,
# we mainly avoid stop-characters in the domain names of source route
$source_route = $1; $addr = $2;
}
if ($addr =~ m{^ ( .*? )
- ( \@ (?: [^\@\[\]]+ | \[ (?: \\. | [^\]\\] ){0,999} \]
- | [^\@] )* )
+ ( \@ (?: [^\@\[\]]+ | \[ (?: \\. | [^\]\\] ){0,999} \]
+ | [^\@] )* )
\z}xs) {
($localpart,$domain) = ($1,$2);
} else {
($localpart,$domain) = ($addr,'');
}
- $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg if $unquote; # undo quoted-pairs
+ $localpart =~ s/ " | \\ (.) | \\ \z /$1/xgs if $unquote; # undo quoted-pairs
($source_route, $localpart, $domain);
}
@@ -5169,9 +5684,12 @@ sub unquote_rfc2821_local($) {
# addresses. Properly deals with group addresses, nested comments, address
# literals, qcontent, addresses with source route, discards display
# names and comments. The following header fields accept address-list:
-# To, Cc, Bcc, Reply-To. A header field 'From' accepts a 'mailbox-list'
-# syntax (which is similar, but does not allow groups); a header field
-# 'Sender' accepts a 'mailbox' syntax, i.e. only one address and not a group.
+# To, Cc, Bcc, Reply-To, (and since RFC 6854 also:) From and Sender.
+#
+# RFC 6854 relaxed the syntax on 'From' and 'Sender', where the group syntax
+# is now allowed. Prior to RFC 6854 the 'From' accepted a 'mailbox-list'
+# syntax (does not allow groups), and 'Sender' accepted a 'mailbox' syntax,
+# i.e. only one address and not a group.
#
use vars qw($s $p @addresses);
sub flush_a() {
@@ -5246,7 +5764,7 @@ sub parse_address_list($) {
#
sub displayed_length($$) {
my($str,$ind) = @_;
- for my $t ($str =~ /\G ( \t | [^\t]+ )/gsx)
+ for my $t ($str =~ /\G ( \t | [^\t]+ )/xgs)
{ $ind += $t ne "\t" ? length($t) : 8 - $ind % 8 }
$ind;
}
@@ -5298,7 +5816,7 @@ sub wrap_string($;$$$$) {
$str =~ s/\n//g; # unfold (knowing a space at folds is not missing)
# unbreakable parts are non- all-whitespace substrings
@chunks = $str =~ /\G ( (?: ^ .*? | [ \t]) [^ \t]+ [ \t]* )
- (?= \z | [ \t] [^ \t] )/gsx;
+ (?= \z | [ \t] [^ \t] )/xgs;
}
# do_log(5,"wrap_string chunk: <%s>", $_) for @chunks;
my $result = ''; # wrapped multiline string will accumulate here
@@ -5530,7 +6048,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Util qw(ll do_log fmt_struct);
}
@@ -5612,7 +6130,7 @@ sub lookup_re($$;$%) {
# do the righthand side replacements if any $n, ${n} or $(n) is specified
if (defined($r) && !ref($r) && index($r,'$') >= 0) { # triage
my $any = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
- { my $j=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse;
+ { my $j=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }xgse;
# bring taintedness of input to the result
$r .= substr($addr,0,0) if $any;
}
@@ -5625,18 +6143,14 @@ sub lookup_re($$;$%) {
} elsif (!@result) {
do_log(5, "lookup_re(%s), no matches", fmt_struct($addr));
} else { # pretty logging
- my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
- e => "\e", a => "\a", t => "\t");
- my(@mk) = @matchingkey;
- for my $mk (@mk) # undo the \-quoting, will be redone by logging routines
- { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx }
if (!$get_all) { # first match wins
do_log(5, 'lookup_re(%s) matches key "%s", result=%s',
- fmt_struct($addr), $mk[0], fmt_struct($result[0]));
+ fmt_struct($addr), $matchingkey[0], fmt_struct($result[0]));
} else { # want all matches
do_log(5, "lookup_re(%s) matches keys: %s", fmt_struct($addr),
- join(', ', map {sprintf('"%s"=>%s', $mk[$_],fmt_struct($result[$_]))}
- (0..$#result)));
+ join(', ', map { sprintf('"%s"=>%s',
+ $matchingkey[$_], fmt_struct($result[$_]))
+ } (0..$#result)));
}
}
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
@@ -5653,7 +6167,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION $have_patricia);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&lookup_ip_acl &ip_to_vec &normalize_ip_addr);
import Amavis::Util qw(ll do_log);
@@ -6140,10 +6654,11 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&lookup &lookup2 &lookup_hash &lookup_acl);
- import Amavis::Util qw(ll do_log fmt_struct unique_list);
+ import Amavis::Util qw(ll do_log fmt_struct unique_list
+ safe_encode_utf8 idn_to_ascii);
import Amavis::Conf qw(:platform c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys);
@@ -6155,6 +6670,13 @@ use subs @EXPORT_OK;
# whatever the map returns, otherwise undef is returned. First match wins,
# aborting further search sequence.
#
+# The $addr may be a string of octets (assumed to be UTF-8 encoded)
+# or a string of characters which gets first encoded to UTF-8 octets.
+# International domain name (IDN) in $addr will be converted to ACE and
+# lowercased. Keys of a hash table are expected to be in octets (utf8 flag
+# off) and their international domain names encoded in ASCII-compatible
+# encoding (ACE).
+#
sub lookup_hash($$;$%) {
my($addr, $hash_ref,$get_all,%options) = @_;
ref($hash_ref) eq 'HASH'
@@ -6172,7 +6694,7 @@ sub lookup_hash($$;$%) {
for my $r (@result) { # $r is just an alias to array elements
if (defined($r) && !ref($r) && index($r,'$') >= 0) { # plain string with $
my $any = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
- { my $j = $2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse;
+ { my $j = $2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }xgse;
# bring taintedness of input to the result
$r .= substr($addr,0,0) if $any;
}
@@ -6196,6 +6718,13 @@ sub lookup_hash($$;$%) {
# lookup_acl() performs a lookup for an e-mail address against
# access control list.
#
+# The $addr may be a string of octets (assumed to be UTF-8 encoded)
+# or a string of characters which gets first encoded to UTF-8 octets.
+# International domain name (IDN) in $addr will be converted to ACE
+# and lowercased. Array elements are expected to be in octets (utf8
+# flag off) and their international domain names encoded in
+# ASCII-compatible encoding (ACE).
+#
# The supplied e-mail address is compared with each member of the
# lookup list in turn, the first match wins (terminates the search),
# and its value decides whether the result is true (yes, permit, pass)
@@ -6256,12 +6785,14 @@ sub lookup_acl($$%) {
ref($acl_ref) eq 'ARRAY'
or die "lookup_acl: arg2 must be a list ref: $acl_ref";
return if !@$acl_ref; # empty list can't match anything
+ $addr = safe_encode_utf8($addr); # make sure it's in octets
my $lpcs = c('localpart_is_case_sensitive');
- my($localpart,$domain) = split_address($addr); $domain = lc($domain);
- $localpart = lc($localpart) if !$lpcs;
+ my($localpart,$domain) = split_address($addr);
+ $localpart = lc $localpart if !$lpcs;
local($1,$2);
- # chop off leading @ and trailing dots
+ # chop off leading '@' and trailing dots
$domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
+ $domain = idn_to_ascii($domain) if $domain ne ''; # lowercase, ToASCII
$domain .= $options{AppendStr} if defined $options{AppendStr};
my($matchingkey, $result); my $found = 0;
for my $e (@$acl_ref) {
@@ -6428,7 +6959,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&expand &tokenize);
import Amavis::Util qw(ll do_log);
@@ -6487,11 +7018,11 @@ sub tokenize($;$) {
: /^(%\#?.)\z/s ? \"$1" # unknown builtins
: /^\\([0-7]{1,3})\z/ ? chr(oct($1)) # \nnn
: /^\\(.)\z/s ? (exists($esc{$1}) ? $esc{$1} : $1) # \r, \n, \f, ...
- : /^(_ [A-Z]+ (?: \( [^)]* \) )? _)\z/sx ? \"$1" # SpamAssassin-compatible
+ : /^(_ [A-Z]+ (?: \( [^)]* \) )? _)\z/xs ? \"$1" # SpamAssassin-compatible
: $_ }
$$str_ref =~ /\G \# | \[ [?~\@:="]? | "\] | \] | \| | % \#? . | \\ [^0-7] |
\\ [0-7]{1,3} | _ [A-Z]+ (?: \( [^)]* \) )? _ |
- [^\[\]\\|%\n#"_]+ | [^\n]+? | \n /gsx;
+ [^\[\]\\|%\n#"_]+ | [^\n]+? | \n /xgs;
$tokens_ref;
}
@@ -6555,7 +7086,7 @@ sub evalmacro($$;@) {
}
if (exists($builtins_href->{$name})) {
my $s = $builtins_href->{$name};
- if (ref($s) eq 'Amavis::Expand') { # expand a dynamically defined macro
+ if (UNIVERSAL::isa($s,'Amavis::Expand')) { # dynamically defined macro
my(@margs) = ($name); # no arguments beyond %0
my(@res) = map(!ref || $$_ !~ /^%([0-9])\z/ ? $_
: ref($margs[$1]) ? @{$margs[$1]} : (), @$s);
@@ -6593,7 +7124,7 @@ sub evalmacro($$;@) {
if (!ref($s)) { # macro expands to a plain string
if (!$cardinality_only) { @result = $s }
else { @result = $s !~ /^\s*\z/ ? 1 : 0 }; # %#x => nonwhite=1, other 0
- } elsif (ref($s) eq 'Amavis::Expand') { # dynamically defined macro
+ } elsif (UNIVERSAL::isa($s,'Amavis::Expand')) { # dynamically defined macro
$args[0] = $name; # replace name with a stringified and trimmed form
# expanding a dynamically-defined macro produces a list of tokens;
# formal argument lexels %0, %1, ... %9 are replaced by actual arguments
@@ -6702,7 +7233,7 @@ sub expand($$) {
if (defined $whereto) { push(@$whereto,@$result_ref) }
# else { $output_str .= tokens_list_to_str($result_ref) }
else { $output_str .= join('', map(ref($_) ? $$_ : $_, @$result_ref)) }
- } elsif ($$t =~ /^_ ([A-Z]+) (?: \( ( [^)]* ) \) )? _\z/sx) {
+ } elsif ($$t =~ /^_ ([A-Z]+) (?: \( ( [^)]* ) \) )? _\z/xs) {
# neutral simple SA-like macro call, $1 is name, $2 is a single! argument
my $result_ref = evalmacro($lx_lbC, $builtins_href, [$1],
!defined($2) ? () : [$2] );
@@ -6730,7 +7261,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Timing qw(section_time);
@@ -7265,7 +7796,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
}
use Errno qw(EIO);
@@ -7299,7 +7830,7 @@ sub close {
sub DESTROY {
my $self = $_[0]; local($@,$!,$_);
# ignore failure, make perlcritic happy
- if (ref $self && $self->{fh}) { eval { $self->close } or 1 }
+ if ($self && $self->{fh}) { eval { $self->close } or 1 }
}
sub open {
@@ -7401,10 +7932,10 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform);
- import Amavis::Util qw(ll do_log min max minmax);
+ import Amavis::Util qw(ll do_log min max minmax idn_to_ascii);
}
use Errno qw(EIO EINTR EAGAIN EPIPE ENOTCONN ECONNRESET);
@@ -7500,10 +8031,10 @@ sub connect_attempt {
my($peeraddress, $peerport, $is_inet); local($1,$2,$3);
if ($socketname =~ m{^/}) { # simpleminded: unix vs. inet
$is_inet = 0;
- } elsif ($socketname =~ /^(?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*)/sx) {
+ } elsif ($socketname =~ /^(?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*)/xs) {
# ignore possible further fields after the "proto:addr:port:..." last colon
$peeraddress = defined $1 ? $1 : $2; $peerport = $3; $is_inet = 1;
- } elsif ($socketname =~ /^(?: \[ ([^\]]*) \] | ([0-9a-fA-F.:]+) ) \z/sx) {
+ } elsif ($socketname =~ /^(?: \[ ([^\]]*) \] | ([0-9a-fA-F.:]+) ) \z/xs) {
$peeraddress = defined $1 ? $1 : $2; $is_inet = 1;
} else { # probably a syntax error, but let's assume it is a Unix socket
$is_inet = 0;
@@ -7534,8 +8065,8 @@ sub connect_attempt {
if (!$is_inet) {
# unix socket
- ll(3) && do_log(3, "new socket by IO::Socket::UNIX to %s, timeout %s",
- $socketname, $timeout_displ);
+ ll(3) && do_log(3, "new socket by IO::Socket::UNIX to %s, ".
+ "timeout set to %s", $socketname, $timeout_displ);
$sock = IO::Socket::UNIX->new(
# Domain => AF_UNIX,
Type => SOCK_STREAM, Timeout => $timeout);
@@ -7548,6 +8079,7 @@ sub connect_attempt {
defined $io_socket_module_name
or die "No INET or INET6 socket module is available";
my $local_sock_displ = '';
+ $peeraddress = idn_to_ascii($peeraddress);
my(%args) = (Type => SOCK_STREAM, Proto => 'tcp', Blocking => $blocking,
PeerAddr => $peeraddress, PeerPort => $peerport);
# Timeout => $timeout, # produces: Invalid argument
@@ -7881,7 +8413,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
}
@@ -7890,6 +8422,8 @@ sub new
sub client_ip # client IP address (immediate SMTP client, i.e. our MTA)
{ @_<2 ? shift->{client_ip} : ($_[0]->{client_ip} = $_[1]) }
+sub client_port # TCP source port number (immediate SMTP client)
+ { @_<2 ? shift->{client_port} : ($_[0]->{client_port} = $_[1]) }
sub socket_ip # IP address of our interface that received connection
{ @_<2 ? shift->{socket_ip} : ($_[0]->{socket_ip} = $_[1]) }
sub socket_port # TCP port of our interface that received connection
@@ -7915,7 +8449,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform);
import Amavis::Util qw(setting_by_given_contents_category_all
@@ -7946,7 +8480,7 @@ sub recip_penpals_score # penpals score (info, also added to spam_level)
{ @_<2 ? shift->[8] : ($_[0]->[8] = $_[1]) }
sub dsn_notify # ESMTP RCPT command NOTIFY option (DSN-RFC 3461, listref)
{ @_<2 ? shift->[9] : ($_[0]->[9] = $_[1]) }
-sub dsn_orcpt # ESMTP RCPT command ORCPT option (DSN-RFC 3461, encoded)
+sub dsn_orcpt # ESMTP RCPT command ORCPT option (decoded: RFC 3461, RFC 6533)
{ @_<2 ? shift->[10] : ($_[0]->[10] = $_[1]) }
sub dsn_suppress_reason # if defined disable sending DSN and supply a reason
{ @_<2 ? shift->[11] : ($_[0]->[11] = $_[1]) }
@@ -8106,12 +8640,12 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform);
import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp quote_rfc2821_local
qquote_rfc2821_local);
- import Amavis::Util qw(orcpt_encode ll do_log);
+ import Amavis::Util qw(ll do_log orcpt_decode);
import Amavis::In::Message::PerRecip;
}
@@ -8171,12 +8705,14 @@ sub dsn_passed_on # obligation to send notification on SUCCESS was relayed
{ @_<2 ? shift->{dsn_pass_on}: ($_[0]->{dsn_pass_on} = $_[1]) }
sub requested_by # Resent-From addr who requested release from a quarantine
{ @_<2 ? shift->{requested_by}:($_[0]->{requested_by} = $_[1])}
-sub body_type # ESMTP BODY param (RFC 1652: 7BIT, 8BITMIME) or BINARYMIME
+sub body_type # ESMTP BODY param (RFC 6152: 7BIT, 8BITMIME) or BINARYMIME
{ @_<2 ? shift->{body_type} : ($_[0]->{body_type} = $_[1]) }
-sub header_8bit # true if header contains characters with code above 255
+sub smtputf8 # ESMTP SMTPUTF8 param, boolean (RFC 6531)
+ { @_<2 ? shift->{smtputf8} : ($_[0]->{smtputf8} = $_[1]) }
+sub header_8bit # true if header contains non-ASCII characters
{ @_<2 ? shift->{header_8bit}: ($_[0]->{header_8bit} = $_[1]) }
-sub body_8bit # true if body contains chars with code above 255
- { @_<2 ? shift->{body_8bit}: ($_[0]->{body_8bit} = $_[1]) }
+sub body_8bit # true if body contains non-ASCII characters
+ { @_<2 ? shift->{body_8bit} : ($_[0]->{body_8bit} = $_[1]) }
sub sender # envelope sender, internal form, e.g.: j doe at example.com
{ @_<2 ? $_[0]->{sender} : ($_[0]->{sender} = $_[1]) }
sub sender_smtp # env sender, SMTP form in <>, e.g.: <"j doe"@example.com>
@@ -8231,8 +8767,8 @@ sub body_start_pos # byte offset into a msg where mail body starts (if known)
{ @_<2 ? shift->{body_pos}: ($_[0]->{body_pos} = $_[1]) }
sub body_digest # digest of a message body (e.g. MD5, SHA1, SHA256), hex
{ @_<2 ? shift->{body_digest}: ($_[0]->{body_digest} = $_[1]) }
-sub ip_addr_trace # IP addresses in 'Received from' hdr flds, top-down, array
- { @_<2 ? shift->{iptrace} : ($_[0]->{iptrace} = $_[1]) }
+sub trace # info from Received header fields, top-down, array of hashrefs
+ { @_<2 ? shift->{trace} : ($_[0]->{trace} = $_[1]) }
sub ip_addr_trace_public # public IP addresses in 'Received from' hdr flds
{ @_<2 ? shift->{iptracepub} : ($_[0]->{iptracepub} = $_[1]) }
sub is_mlist # mail is from a mailing list (boolean/string)
@@ -8330,7 +8866,8 @@ sub recips { # get or set a listref of envelope recipients
my $per_recip_obj = Amavis::In::Message::PerRecip->new;
$per_recip_obj->recip_addr($_);
$per_recip_obj->recip_addr_smtp(qquote_rfc2821_local($_));
- $per_recip_obj->dsn_orcpt(orcpt_encode($per_recip_obj->recip_addr_smtp))
+ $per_recip_obj->dsn_orcpt(
+ join(';', orcpt_decode(';'.$per_recip_obj->recip_addr_smtp)))
if $set_dsn_orcpt_too;
$per_recip_obj->recip_destiny(D_PASS); # default is Pass
$per_recip_obj } @{$recips_list_ref} ]);
@@ -8426,7 +8963,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&hdr);
import Amavis::Conf qw(:platform c cr ca);
@@ -8435,48 +8972,49 @@ BEGIN {
import Amavis::Util qw(ll do_log min max q_encode
safe_encode safe_encode_ascii safe_encode_utf8);
}
-use MIME::Words;
use Errno qw(EBADF);
+use Encode ();
+use MIME::Words;
sub new {
my $class = $_[0];
bless { prepend=>[], append=>[], addrcvd=>[], edit=>{} }, $class;
}
-sub prepend_header($$$;$) {
- my($self, $field_name, $field_body, $structured) = @_;
- unshift(@{$self->{prepend}}, hdr($field_name,$field_body,$structured));
+sub prepend_header {
+ my $self = shift;
+ unshift(@{$self->{prepend}}, hdr(@_));
}
-sub append_header($$$;$) {
- my($self, $field_name, $field_body, $structured) = @_;
- push(@{$self->{append}}, hdr($field_name,$field_body,$structured));
+sub append_header {
+ my $self = shift;
+ push(@{$self->{append}}, hdr(@_));
}
-sub append_header_above_received($$$;$) {
- my($self, $field_name, $field_body, $structured) = @_;
- push(@{$self->{addrcvd}}, hdr($field_name,$field_body,$structured));
+sub append_header_above_received {
+ my $self = shift;
+ push(@{$self->{addrcvd}}, hdr(@_));
}
# now a synonym for append_header_above_received() (old semantics: prepend
# or append, depending on setting of $append_header_fields_to_bottom)
#
-sub add_header($$$;$) {
- my($self, $field_name, $field_body, $structured) = @_;
- push(@{$self->{addrcvd}}, hdr($field_name,$field_body,$structured));
+sub add_header {
+ my $self = shift;
+ push(@{$self->{addrcvd}}, hdr(@_));
}
# delete all header fields with a $field_name
#
-sub delete_header($$) {
+sub delete_header {
my($self, $field_name) = @_;
- $self->{edit}{lc($field_name)} = [undef];
+ $self->{edit}{lc $field_name} = [undef];
}
# all header fields with $field_name will be edited by a supplied subroutine
#
-sub edit_header($$$;$) {
- my($self, $field_name, $field_edit_sub, $structured) = @_;
+sub edit_header {
+ my($self, $field_name, $field_edit_sub) = @_;
# $field_edit_sub will be called with 2 args: a field name and a field body;
# It should return a pair consisting of a replacement field body (no field
# name and no colon, with or without a trailing NL), and a boolean 'verbatim'
@@ -8486,7 +9024,7 @@ sub edit_header($$$;$) {
# changes are allowed on a replacement body such as folding or encoding).
!defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE'
or die "edit_header: arg#3 must be undef or a subroutine ref";
- $field_name = lc($field_name);
+ $field_name = lc $field_name;
if (!exists($self->{edit}{$field_name})) {
$self->{edit}{$field_name} = [$field_edit_sub];
} else {
@@ -8521,20 +9059,36 @@ sub inherit_header_edits($$) {
# contains non-ASCII characters, fold long lines if needed, prepend space
# before each NL if missing, append NL if missing. Header lines with only
# spaces are not allowed. (RFC 5322: Each line of characters MUST be no more
-# than 998 characters, and SHOULD be no more than 78 characters, excluding
-# the CRLF). $structured==0 indicates an unstructured header field,
-# folding may be inserted at any existing whitespace character position;
-# $structured==1 indicates that folding is only allowed at positions
+# than 998 octets(!) (RFC 6532), and SHOULD be no more than 78 characters(!)
+# (RFC 6532), excluding the CRLF). $structured==0 indicates an unstructured
+# header field, folding may be inserted at any existing whitespace character
+# position; $structured==1 indicates that folding is only allowed at positions
# indicated by \n in the provided header body, original \n will be removed.
# With $structured==2 folding is preserved, wrapping step is skipped.
#
-sub hdr($$$;$) {
- my($field_name, $field_body, $structured, $wrap_char) = @_;
+sub hdr {
+ my($field_name, $field_body, $structured, $wrap_char, $smtputf8) = @_;
$wrap_char = "\t" if !defined $wrap_char;
+ $field_name = safe_encode_ascii($field_name); # must be all-ASCII
+ my $field_body_is_utf8 = Encode::is_utf8($field_body);
local($1);
- if ($field_name =~ /^ (?: Subject\z | Comments\z |
- X- (?! Envelope- (?:From|To)\z ) )/six &&
- $field_body !~ /^[\t\n\040-\176]*\z/ # not all printable (or TAB or LF)
+ if ($field_body !~ tr/\x00-\x7F//c) { # is all-ASCII
+ # no encoding necessary, just clear the utf8 flag if set
+ if ($field_body_is_utf8) {
+ do_log(5,'header encoded (utf8:Y) (all-ASCII): %s: %s',
+ $field_name, $field_body);
+ $field_body = safe_encode_utf8($field_body);
+ } else {
+ do_log(5,'header encoded (all-ASCII): %s: %s', $field_name, $field_body);
+ }
+ } elsif ($smtputf8) { # UTF-8 in header field bodies is allowed
+ $field_body = safe_encode_utf8($field_body) if $field_body_is_utf8;
+ ll(5) && do_log(5,'header encoded (utf8:%s) to UTF-8 (SMTPUTF8): %s: %s',
+ $field_body_is_utf8?'Y':'N', $field_name, $field_body);
+ } elsif ($field_name =~ /^(?: Subject | Comments |
+ (?:Resent-)? (?: From|Sender|To|Cc ) )\z/six &&
+ $field_body !~ /^[\t\n\x20-\x7F]*\z/ # but printable or HT or LF
+ # consider also: | X- (?! Envelope- (?:From|To)\z )
) { # encode according to RFC 2047
# actually RFC 2047 also allows encoded-words in rfc822 extension
# message header fields (now: optional header fields), within comments
@@ -8542,22 +9096,24 @@ sub hdr($$$;$) {
# we are being sloppy here!
$field_body =~ s/\n(?=[ \t])//gs; # unfold
chomp($field_body);
- my $field_body_octets;
- my $chset = c('hdr_encoding'); my $qb = c('hdr_encoding_qb');
- $field_body_octets = safe_encode($chset, $field_body);
-# do_log(5, "hdr - UTF-8 body: %s", $field_body);
-# do_log(5, "hdr - body octets: %s", $field_body_octets);
- my $encoder_func = uc($qb) eq 'Q' ? \&q_encode
- : \&MIME::Words::encode_mimeword;
+ my $chset = c('hdr_encoding');
+ my $field_body_octets = safe_encode($chset, $field_body);
+ ll(5) && do_log(5,'header encoded (utf8:%s) to %s, %s: %s -> %s',
+ $field_body_is_utf8?'Y':'N', $chset,
+ $field_name, $field_body, $field_body_octets);
+ my $qb = c('hdr_encoding_qb');
+ my $encoder_func = uc $qb eq 'Q' ? \&q_encode
+ : \&MIME::Words::encode_mimeword;
$field_body = join("\n", map { /^[\001-\011\013\014\016-\177]*\z/ ? $_
: &$encoder_func($_,$qb,$chset) }
split(/\n/, $field_body_octets, -1));
- } else { # supposed to be in plain ASCII, let's make sure it is
- $field_body = safe_encode_ascii($field_body);
+ } else { # should have been all-ASCII, or UTF-8 with SMTPUTF8 - but anyway:
+ $field_body = safe_encode_utf8($field_body) if $field_body_is_utf8;
+ ll(5) && do_log(5,'header encoded (utf8:%s) to UTF-8: %s: %s',
+ $field_body_is_utf8?'Y':'N', $field_name, $field_body);
}
- $field_name = safe_encode_ascii($field_name);
my $str = $field_name . ':';
- $str .= ' ' if $field_body =~ /^[^ \t]/; # looks nicer
+ $str .= ' ' if $field_body =~ /^[^ \t]/; # insert space, looks nicer
$str .= $field_body;
if ($structured == 2) { # already folded, keep it that way, sanitize
1 while $str =~ s/^([ \t]*)\n/$1/; # prefixed by whitespace lines?
@@ -8673,7 +9229,7 @@ sub write_header($$$$) {
} else { # count, edit, or delete
# obsolete RFC 822 syntax allowed whitespace before colon
my($field_name, $field_body) = ($1, $2);
- my $field_name_lc = lc($field_name);
+ my $field_name_lc = lc $field_name;
$received_cnt++ if $field_name_lc eq 'received';
if (exists($self->{edit}{$field_name_lc})) {
chomp($field_body);
@@ -8688,7 +9244,8 @@ sub write_header($$$$) {
$curr_head = undef; last;
}
$curr_head = $verbatim ? ($field_name . ':' . $new_fbody)
- : hdr($field_name, $new_fbody, 0);
+ : hdr($field_name, $new_fbody, 0, undef,
+ $msginfo->smtputf8);
chomp($curr_head); $curr_head .= "\n";
$curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s;
$field_body = $2; chomp($field_body); # carry to next iteration
@@ -8703,7 +9260,7 @@ sub write_header($$$$) {
$curr_head =~ s/\n(?=[ \t]*\n)//g and $ill_white_cnt++;
}
if ($fix_long_header_lines) { # truncate long header lines to 998 ch
- $curr_head =~ s{^(.{995}).{4,}$}{$1...}mg and $ill_long_cnt++;
+ $curr_head =~ s{^(.{995}).{4,}$}{$1...}gm and $ill_long_cnt++;
}
# use buffering to reduce number of calls to datasend()
if (length($str) > 16384) {
@@ -8739,7 +9296,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT = qw(&mail_dispatch);
import Amavis::Conf qw(:platform :confvars c cr ca);
@@ -8893,7 +9450,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&first_received_from &oldest_public_ip_addr_from_received);
import Amavis::Conf qw(:platform c cr ca);
@@ -8945,7 +9502,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&consumed_bytes);
import Amavis::Conf qw(c cr ca
@@ -9034,7 +9591,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Util qw(ll do_log);
}
@@ -9080,7 +9637,7 @@ sub type_declared
{ @_<2 ? shift->{ty_decl} : ($_[0]->{ty_decl} = $_[1]) };
sub name_declared # string or a ref to a list of strings
{ @_<2 ? shift->{nm_decl} : ($_[0]->{nm_decl} = $_[1]) };
-sub report_type # a string, e.g. 'delivery-status', RFC 3462
+sub report_type # a string, e.g. 'delivery-status', RFC 6522
{ @_<2 ? shift->{rep_typ} : ($_[0]->{rep_typ} = $_[1]) };
sub size
{ @_<2 ? shift->{size} : ($_[0]->{size} = $_[1]) };
@@ -9122,7 +9679,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter MIME::Parser::Filer); # subclass of MIME::Parser::Filer
}
# This package will be used by mime_decode().
@@ -9164,11 +9721,11 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&check_header_validity &check_for_banned_names);
import Amavis::Util qw(ll do_log min max minmax untaint untaint_inplace
- sanitize_str);
+ is_valid_utf_8 truncate_utf_8);
import Amavis::Conf qw(:platform %banned_rules c cr ca);
import Amavis::Lookup qw(lookup lookup2);
}
@@ -9179,55 +9736,82 @@ sub check_header_validity($) {
my(%field_head_counts, @bad);
my $minor_badh_category = 0;
my $allowed_tests = cr('allowed_header_tests');
- my($t_syntax,$t_empty,$t_long,$t_control,$t_8bit,$t_missing,$t_multiple) =
- !$allowed_tests ? () : @$allowed_tests{'syntax','empty','long','control',
- '8bit','missing','multiple'};
+ my($t_syntax, $t_empty, $t_long, $t_control, $t_8bit, $t_utf8,
+ $t_missing, $t_multiple) =
+ !$allowed_tests ? () : @$allowed_tests{qw(syntax empty long control
+ 8bit utf8 missing multiple)};
# minor category: 2: 8-bit char, 3: NUL/CR control, 4: empty line, 5: long,
# 6: syntax, 7: missing, 8: multiple
local($1,$2,$3);
for my $curr_head (@{$msginfo->orig_header}) {#array of hdr fields, not lines
my($field_name,$msg1,$msg2,$pre,$mid,$post);
# obsolete RFC 822 syntax allowed whitespace before colon
- $field_name = $1 if $curr_head =~ /^([!-9;-\176]+)[ \t]*:/s;
+ $field_name = $1 if $curr_head =~ /^([!-9;-\x7E\x80-\xFF]+)[ \t]*:/s;
$field_head_counts{lc($field_name)}++ if defined $field_name;
if (!defined($field_name) || substr($field_name,0,2) eq '--') {
if ($t_syntax) {
- $msg1 = "Invalid header field syntax";
- $pre = ''; $mid = ''; $post = $curr_head;
+ $msg1 = "Invalid header field syntax"; $msg2 = $curr_head;
$minor_badh_category = max(6, $minor_badh_category);
}
+ } elsif ($t_syntax && $field_name =~ /([^\x00-\x7F])/gs) {
+ $mid = $1; $msg1 = "Invalid header field name, contains non-ASCII char";
+ $minor_badh_category = max(6, $minor_badh_category);
} elsif ($t_empty && $curr_head =~ /^([ \t]+)(?=\n|\z)/gms) {
$mid = $1;
$msg1 ="Improper folded header field made up entirely of whitespace";
# note: using //g and pos to avoid deep recursion in regexp
$minor_badh_category = max(4, $minor_badh_category);
} elsif ($t_long && $curr_head =~ /^([^\n]{999,})(?=\n|\z)/gms) {
- $mid = $1; $msg1 = "Header line longer than 998 characters";
+ $msg1 = "Header line longer than 998 characters"; $msg2 = $1;
+ substr($msg2, 50) = '[...]' if length($msg2) > 55;
$minor_badh_category = max(5, $minor_badh_category);
} elsif ($t_control && $curr_head =~ /([\000\015])/gs) {
$mid = $1; $msg1 = "Improper use of control character";
$minor_badh_category = max(3, $minor_badh_category);
- } elsif ($t_8bit && $curr_head =~ /([\200-\377])/gs) {
- $mid = $1; $msg1 = "Non-encoded 8-bit data";
- $minor_badh_category = max(2, $minor_badh_category);
- } elsif ($t_8bit && $curr_head =~ /([^\000-\377])/gs) {
- $mid = $1; $msg1 = "Non-encoded Unicode character"; # should not happen
- $minor_badh_category = max(2, $minor_badh_category);
+ } elsif ($t_8bit && $curr_head =~ /([^\x00-\x7F])/gs) { # non-ASCII
+ $mid = $1;
+ if (!is_valid_utf_8($curr_head)) {
+ $msg1 = 'Non-encoded non-ASCII data (and not UTF-8)';
+ } elsif ($curr_head =~ /^([\x00-\x08\x0B-\x1F\x7F])/xgs) { # but TAB,NL
+ $mid = $1; $msg1 = 'UTF-8 string contains C0 Controls';
+ } elsif ($curr_head =~
+ /( (?: \xC2 | \xE0 \x82 | \xF0 \x80 \x82 ) [\x80-\x9F] )/xgs) {
+ # RFC 5198 prohibits "C1 Controls" (U+0080..U+009F) for Net-Unicode
+ $mid = $1; $msg1 = 'UTF-8 string contains C1 Controls';
+ } elsif ($msginfo->smtputf8) {
+ # UTF-8 header bodies (but not field names) are valid with SMTPUTF8
+ } elsif ($t_utf8) {
+ $msg1 = 'Non-encoded UTF-8 string in non-EAI mail';
+ if ($curr_head =~ /( [\xC0-\xDF][\x80-\xBF] |
+ [\xE0-\xEF][\x80-\xBF]{2} |
+ [\xF0-\xF4][\x80-\xBF]{3} )/xgs ) {
+ $mid = $1; # capture the entire first non-ASCII UTF-8 character
+ }
+ }
+ $minor_badh_category = max(2, $minor_badh_category) if defined $msg1;
}
if (defined $msg1) {
- $pre = substr($curr_head,0,pos($curr_head)-length($mid)) if !defined $pre;
- $post = substr($curr_head,pos($curr_head)) if !defined $post;
- chomp($post);
- substr($mid, 15) = '[...]' if length($mid) > 20;
- substr($post,15) = '[...]' if length($post) > 20;
- if (length($pre)-length($field_name)-2 > 50-length($post)) {
- $pre = $field_name . ': ...'
- . substr($pre, length($pre) - (45-length($post)));
- }
- $msg1 .= sprintf(" (char %02X hex)", ord($mid)) if length($mid)==1;
- $msg2 = sanitize_str($pre . $mid . $post);
+ $mid = '' if !defined $mid;
+ if (!defined $msg2) {
+ $pre = substr($curr_head, 0, pos($curr_head)-length($mid))
+ if !defined $pre;
+ $post = substr($curr_head,pos($curr_head)) if !defined $post;
+ chomp($post);
+ $mid = truncate_utf_8($mid, 15).'[...]' if length($mid) > 20;
+ $post = truncate_utf_8($post,15).'[...]' if length($post) > 20;
+ if (length($pre)-length($field_name)-2 > 50-length($post)) {
+ $pre = $field_name . ': ...'
+ . substr($pre, length($pre) - (45-length($post)));
+ }
+ $msg2 = $pre . $mid . $post;
+ }
+ if ($mid ne '' && length($mid) <= 4) {
+ $msg1 .= " (char ";
+ $msg1 .= join(' ', map(sprintf('%02X',ord($_)), split(//,$mid)));
+ $msg1 .= " hex)";
+ }
push(@bad, "$msg1: $msg2");
- last if @bad >= 100; # some sanity limit
+ last if @bad >= 100; # some sanity limit
}
}
# RFC 5322 (ex RFC 2822), RFC 2045, RFC 2183
@@ -9248,10 +9832,15 @@ sub check_header_validity($) {
$minor_badh_category = max(8, $minor_badh_category);
}
}
- if (!@bad)
- { do_log(5,"check_header: %d, OK", $minor_badh_category) }
- elsif (ll(2))
- { do_log(2,"check_header: %d, %s", $minor_badh_category, $_) for @bad }
+ for (@bad) { # sanitize C0 controls and non-ASCII
+ s{ ( [^\x20-\x7E] | \\ (?= x \{ ) ) }
+ { sprintf('\\x{%02X}', ord($1)) }xgse if tr/\x00-\x7F//c;
+ }
+ if (!@bad) {
+ do_log(5,"check_header: %d, OK", $minor_badh_category);
+ } elsif (ll(2)) {
+ do_log(2,"check_header: %d, %s", $minor_badh_category, $_) for @bad;
+ }
(\@bad, $minor_badh_category);
}
@@ -9282,7 +9871,7 @@ sub check_for_banned_names($) {
my(@names);
my(@rawnames) = grep(!/^[, ]*\z/,
($t =~ /\G (?: " (?: \\. | [^"\\] ){0,999} "
- | [^, ] )+ | [, ]+/gsx));
+ | [^, ] )+ | [, ]+/xgs));
# in principle quoted strings could be used
# to construct lookup tables on-the-fly (not implemented)
for my $n (@rawnames) { # collect only valid names
@@ -9424,7 +10013,7 @@ sub check_for_banned_names($) {
if (ll($ll)) { # only bother with logging when needed
local($1);
my $mk = defined $matchingkey ? $matchingkey : ''; # pretty-print
- $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx;
+ $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }xgse;
do_log($result?1:3, 'p.path%s %s: "%s"%s',
!$result?'':" BANNED:$result", $recip, $key_val_str,
!defined $result ? '' : ", matching_key=\"$mk\"");
@@ -9440,7 +10029,7 @@ sub check_for_banned_names($) {
$a = $r->banning_rule_key || [];
$matchingkey = "$matchingkey"; # make a plain string out of a qr
push(@$a,$matchingkey); $r->banning_rule_key($a);
- my(@comments) = $matchingkey =~ / \( \? \# \s* (.*?) \s* \) /gsx;
+ my(@comments) = $matchingkey =~ / \( \? \# \s* (.*?) \s* \) /xgs;
$a = $r->banning_rule_comment || [];
push(@$a, @comments ? join(' ', at comments) : $matchingkey);
$r->banning_rule_comment($a);
@@ -9467,14 +10056,14 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&mime_decode);
import Amavis::Conf qw(:platform c cr ca $TEMPBASE $MAXFILES);
import Amavis::Timing qw(section_time);
import Amavis::Util qw(snmp_count untaint ll do_log
safe_decode safe_decode_latin1
- safe_encode safe_encode_ascii safe_encode_utf8);
+ safe_encode safe_encode_utf8);
import Amavis::Unpackers::NewFilename qw(consumed_bytes);
}
use subs @EXPORT_OK;
@@ -9584,7 +10173,7 @@ sub mime_traverse($$$$$) {
for my $pair (@chunks) {
my($data,$encoding) = @$pair;
if (!defined $encoding || $encoding eq '') {
- $val_dec .= safe_decode_latin1($data);
+ $val_dec .= safe_decode_latin1($data); # assumes ISO-8859-1
} else {
$encoding =~ s/\*[^*]*\z//s; # strip RFC 2231 language suffix
$val_dec .= safe_decode($encoding,$data);
@@ -9603,11 +10192,7 @@ sub mime_traverse($$$$$) {
}
$part->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
my $val = $head->mime_attr('content-type.report-type');
- if (defined $val && $val ne '') {
- # $val = safe_encode_utf8($val) if $enc_is_utf8_buggy ||
- # Encode::is_utf8($val);
- $part->report_type($val);
- }
+ $part->report_type(safe_encode_utf8($val)) if defined $val && $val ne '';
}
mime_decode_pre_epi('epilogue', $entity->epilogue,
$tempdir, $parent_obj, $placement);
@@ -9631,7 +10216,7 @@ sub mime_decode($$$) {
Amavis::Unpackers::OurFiler->new("$tempdir/parts", $parent_obj) );
$parser->ignore_errors(1); # also is the default
# if bounce killer is enabled, extract_nested_messages must be off,
- # otherwise we lose headers of attached message/rfc822 messages
+ # otherwise we lose headers of attached message/rfc822 or message/global
$parser->extract_nested_messages(0);
# $parser->extract_nested_messages("NEST"); # parse embedded message/rfc822
# "NEST" complains with "part did not end with expected boundary" when
@@ -9696,7 +10281,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter MIME::Body); # subclass of MIME::Body
import Amavis::Util qw(ll do_log);
}
@@ -9761,16 +10346,17 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&delivery_status_notification &delivery_short_report
&build_mime_entity &defanged_mime_entity
&msg_from_quarantine &expand_variables);
import Amavis::Util qw(ll do_log sanitize_str min max minmax
- safe_encode safe_encode_ascii safe_encode_utf8
- untaint untaint_inplace make_password
- orcpt_decode xtext_decode ccat_split ccat_maj
- generate_mail_id);
+ untaint untaint_inplace
+ idn_to_ascii idn_to_utf8 mail_addr_idn_to_ascii
+ is_valid_utf_8 safe_encode safe_encode_utf8 safe_decode_utf8
+ orcpt_encode orcpt_decode xtext_decode safe_decode_mime
+ make_password ccat_split ccat_maj generate_mail_id);
import Amavis::Timing qw(section_time);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::ProcControl qw(exit_status_str proc_status_ok
@@ -9789,9 +10375,14 @@ use Time::HiRes ();
# replace substring ${myhostname} with a value of a corresponding variable
sub expand_variables($) {
my $str = $_[0]; local($1,$2);
+ my $myhost = idn_to_utf8(c('myhostname'));
$str =~ s{ \$ (?: \{ ([^\}]+) \} |
- ([a-zA-Z](?:[a-zA-Z0-9_]*[a-zA-Z0-9])?\b) ) }
- { { 'myhostname' => c('myhostname') }->{lc($1.$2)} }egx;
+ ([a-zA-Z](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?\b) ) }
+ { { 'myhostname' => $myhost,
+ 'myhostname_utf8' => $myhost,
+ 'myhostname_ascii' => idn_to_ascii($myhost),
+ }->{lc($1.$2)}
+ }xgse;
$str;
}
@@ -9820,7 +10411,7 @@ sub wrap_message_into_archive($$) {
: $1 eq 'm' ? $mail_id
: $1 eq 'n' ? $msginfo->log_id
: $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1) #,'-')
- : $1 eq '%' ? '%' : '%'.$1 }egs;
+ : $1 eq '%' ? '%' : '%'.$1 }gse;
$_ = $msginfo->mail_tempdir . '/' . $_;
}
my $eval_stat;
@@ -9920,7 +10511,7 @@ sub build_mime_entity($$$$$$$) {
$attach_orig_headers, $attach_orig_message) = @_;
$msg_format = '' if !defined $msg_format;
- if (!defined $mime_type || $mime_type !~ m{^multipart(/|\z)}i) {
+ if (!defined $mime_type || $mime_type !~ m{^ multipart (?: / | \z)}xsi) {
my $multipart_cnt = 0;
$multipart_cnt++ if $mail_as_string_ref;
$multipart_cnt++ if defined $msginfo &&
@@ -9936,7 +10527,7 @@ sub build_mime_entity($$$$$$$) {
$m_hdr = ''; $m_body = substr($$mail_as_string_ref,1);
} else {
# calling index and substr is much faster than an equiv. split into $1,$2
- # by a regular expression: /^( (?!\n) .*? (?:\n|\z))? (?: \n (.*) )? \z/sx
+ # by a regular expression: /^( (?!\n) .*? (?:\n|\z))? (?: \n (.*) )? \z/xs
my $ind = index($$mail_as_string_ref,"\n\n"); # find header/body separator
if ($ind < 0) { # no body
$m_hdr = $$mail_as_string_ref; $m_body = '';
@@ -9945,19 +10536,22 @@ sub build_mime_entity($$$$$$$) {
$m_body = substr($$mail_as_string_ref, $ind+2);
}
}
+ $m_hdr = safe_encode_utf8($m_hdr) if defined $m_hdr;
$m_body = safe_encode(c('bdy_encoding'), $m_body) if defined $m_body;
# make sure _our_ source line number is reported in case of failure
my $multipart_cnt = 0;
+ $mime_type = 'multipart/mixed' if !defined $mime_type;
eval {
+ # RFC 6522: 7bit should always be adequate for multipart/report encoding
$entity = MIME::Entity->build(
- Type => defined $mime_type ? $mime_type : 'multipart/mixed',
- Encoding => '7bit', 'X-Mailer' => undef);
+ Type => $mime_type, Encoding => '8bit',
+ 'X-Mailer' => undef);
1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
die $eval_stat;
};
- if (defined $m_hdr) { # insert header fields into MIME::Head entity
+ if (defined $m_hdr) { # insert header fields into MIME::Head entity;
# Mail::Header::modify allows all-or-nothing control over automatic header
# fields folding by Mail::Header, which is too bad - we would prefer
# to have full control on folding of header fields that are explicitly
@@ -9972,7 +10566,9 @@ sub build_mime_entity($$$$$$$) {
for my $hdr_line (split(/\r?\n/, $m_hdr)) {
if ($hdr_line =~ /^([^:]*?)[ \t]*:[ \t]*(.*)\z/s) {
my($fhead,$fbody) = ($1,$2);
- my $str = hdr($fhead,$fbody,0,' '); # encode, wrap, ...
+ $fbody = safe_decode_mime($fbody); # to logical characters
+ # encode, wrap, ...
+ my $str = hdr($fhead, $fbody, 0, ' ', $msginfo->smtputf8);
# re-split the result
($fhead,$fbody) = ($1,$2) if $str =~ /^([^:]*):[ \t]*(.*)\z/s;
chomp($fbody);
@@ -9992,15 +10588,21 @@ sub build_mime_entity($$$$$$$) {
if (defined $m_body) {
if ($flat && $attach_orig_message) {
my($pos,$j); # split $m_body into lines, retaining each \n
- for ($pos=0; ($j=index($m_body,"\n",$pos)) >= 0; $pos = $j+1)
- { push(@prefix_lines, substr($m_body,$pos,$j-$pos+1)) }
+ for ($pos=0; ($j=index($m_body,"\n",$pos)) >= 0; $pos = $j+1) {
+ push(@prefix_lines, substr($m_body,$pos,$j-$pos+1));
+ }
push(@prefix_lines, substr($m_body,$pos)) if $pos < length($m_body);
} else {
+ my $cnt_8bit = $m_body =~ tr/\x00-\x7F//c;
eval { # make sure _our_ source line number is reported on failure
$entity->attach(
Type => 'text/plain', Data => $m_body,
- Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
- ); $multipart_cnt++; 1;
+ Charset => !$cnt_8bit ? 'us-ascii' : c('bdy_encoding'),
+ Encoding => !$cnt_8bit ? '7bit'
+ : $cnt_8bit < 0.2 * length($m_body) ? 'quoted-printable'
+ : 'base64',
+ );
+ $multipart_cnt++; 1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
die $eval_stat;
@@ -10013,15 +10615,65 @@ sub build_mime_entity($$$$$$$) {
if (defined $msginfo && $attach_orig_headers && !$attach_orig_message) {
# attach a header section only
- do_log(4, "build_mime_entity: attaching just original header section");
+ my $hdr_8bit =
+ $msginfo->header_8bit || grep(tr/\x00-\x7F//c, @prefix_lines);
+ my $hdr_utf8 = 1;
+ if ($hdr_8bit) {
+ for (@prefix_lines, @{$msginfo->orig_header}) {
+ if (tr/\x00-\x7F//c && !is_valid_utf_8($_)) { $hdr_utf8 = 0; last }
+ }
+ }
+
+ # RFC 6522 Encoding considerations for text/rfc822-headers:
+ # 7-bit is sufficient for normal mail headers, however, if the
+ # headers are broken or extended and require encoding to make them
+ # legal 7-bit content, they MAY be encoded with quoted-printable
+ # as defined in [MIME].
+
+ # RFC 6532 section 3.5: allows newly defined MIME types to permit
+ # content-transfer-encoding, and it allows content-transfer-encoding
+ # for message/global.
+
+ # RFC 6533: Note that [RFC6532] relaxed a restriction from MIME [RFC2046]
+ # regarding the use of Content-Transfer-Encoding in new "message"
+ # subtypes. This specification (RFC 6533) explicitly allows the use
+ # of Content-Transfer-Encoding in message/global-headers and
+ # message/global-delivery-status.
+
+ my $headers_mime_type =
+ $flat ? 'text/plain' :
+ $hdr_8bit && $hdr_utf8 ? 'message/global-headers' # RFC 6533
+ : 'text/rfc822-headers'; # RFC 6522
+
+ # [rt.cpan.org #98737] MIME::Tools 5.505 prohibits quoted-printable
+ # for message/global-headers. Fixed by a later release.
+ # my $headers_mime_encoding =
+ # !$hdr_8bit ? '7bit' :
+ # $headers_mime_type =~ m{^text/}i || MIME::Entity->VERSION > 5.505
+ # ? 'quoted-printable' : '8bit';
+
+ my $headers_mime_encoding = $hdr_8bit ? '8bit' : '7bit';
+
+ ll(4) && do_log(4,"build_mime_entity: attaching original ".
+ "header section, MIME type: %s, encoding: %s",
+ $headers_mime_type, $headers_mime_encoding);
+
+ # RFC 6533 section 6.3. Interoperability considerations:
+ # It is important that message/global-headers media type is not
+ # converted to a charset other than UTF-8. As a result, implementations
+ # MUST NOT include a charset parameter with this media type.
+
eval { # make sure _our_ source line number is reported on failure
$entity->attach(
- Type => $flat ? 'text/plain' : 'text/rfc822-headers', # RFC 3462
- Encoding => $msginfo->header_8bit ? '8bit' : '7bit',
Data => [@prefix_lines, @{$msginfo->orig_header}],
- Disposition => 'inline', Filename => 'header',
+ Type => $headers_mime_type,
+ Encoding => $headers_mime_encoding,
+ Filename => $headers_mime_type eq 'message/global-headers' ?
+ 'header.u8hdr' : 'header.hdr',
+ Disposition => 'inline',
Description => 'Message header section',
- ); $multipart_cnt++; 1;
+ );
+ $multipart_cnt++; 1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
die $eval_stat;
@@ -10055,6 +10707,7 @@ sub build_mime_entity($$$$$$$) {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
die $eval_stat;
};
+
} else {
# attach as a normal message
do_log(4, "build_mime_entity: attaching entire original message, plain");
@@ -10074,17 +10727,34 @@ sub build_mime_entity($$$$$$$) {
\@prefix_lines, $msginfo->skip_bytes);
$orig_mail_as_body or die "Can't create Amavis::MIME::Body object: $!";
}
+
+ # RFC 6532 section 3.7: Internationalized messages in message/global
+ # format MUST only be transmitted as authorized by [RFC6531]
+ # or within a non-SMTP environment that supports these messages.
+ my $message_mime_type =
+ $flat ? 'text/plain' :
+ $msginfo->smtputf8 && $msginfo->header_8bit
+ ? 'message/global' # RFC 6532
+ : 'message/rfc822';
+
+ # [rt.cpan.org #98737] MIME::Tools 5.505 prohibits quoted-printable
+ # for message/global. Fixed by a later release.
+ my $message_mime_encoding =
+ !$msginfo->header_8bit && !$msginfo->body_8bit ? '7bit' :
+ $message_mime_type =~ m{^text/}i || MIME::Entity->VERSION > 5.505
+ ? 'quoted-printable' : '8bit';
+
eval { # make sure _our_ source line number is reported on failure
- my $att = $entity->attach( # RFC 2046
- Type => $flat ? 'text/plain' : 'message/rfc822',
- Encoding => ($msginfo->header_8bit || $msginfo->body_8bit) ?
- '8bit' : '7bit',
+ my $att = $entity->attach( # RFC 2046, RFC 6532
+ Type => $message_mime_type,
+ Encoding => $message_mime_encoding,
Data => defined $orig_mail_as_body ? []
: !$msginfo->skip_bytes ? $msg
: substr($$msg, $msginfo->skip_bytes),
# Path => $msginfo->mail_text_fn,
$flat ? () : (Disposition => 'attachment', Filename => 'message',
Description => 'Original message'),
+ # RFC 6532: File extension ".u8msg" is suggested for message/global
);
# direct access to tempfile handle
$att->bodyhandle($orig_mail_as_body) if defined $orig_mail_as_body;
@@ -10100,7 +10770,8 @@ sub build_mime_entity($$$$$$$) {
}
# If $msg_format is 'dsn' generate a delivery status notification according
-# to RFC 3462 (ex RFC 1892), RFC 3464 (ex RFC 1894) and RFC 3461 (ex RFC 1891).
+# to RFC 6522 (ex RFC 3462, RFC 1892), RFC 3464 (ex RFC 1894) and RFC 3461
+# (ex RFC 1891).
# If $msg_format is 'arf' generate an abuse report according to RFC 5965
# - "An Extensible Format for Email Feedback Reports". If $msg_format is
# 'attach', generate a report message and attach the original message.
@@ -10117,6 +10788,7 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
my($msginfo,$dsn_per_recip_capable,$builtins_ref,
$notif_recips,$request_type,$feedback_type,$msg_format) = @_;
my $notification; my $suppressed = 0;
+ my $is_smtputf8 = $msginfo->smtputf8; # UTF-8 allowed
if (!defined($msg_format)) {
$msg_format = $request_type eq 'dsn' ? 'dsn'
: $request_type eq 'report' ? c('report_format')
@@ -10129,12 +10801,11 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
else { $is_plain = 1 } # 'plain'
my $dsn_time = $msginfo->rx_time; # time of dsn creation - same as message
# use a reception time for consistency and to be resilient to clock jumps
- $dsn_time = Time::HiRes::time if !$dsn_time; # now
+ $dsn_time = Time::HiRes::time if !$dsn_time; # now, if missing
my $rfc2822_dsn_time = rfc2822_timestamp($dsn_time);
my $sender = $msginfo->sender;
my $dsn_passed_on = $msginfo->dsn_passed_on; # NOTIFY=SUCCESS passed to MTA
my $per_recip_data = $msginfo->per_recip_data;
- my $txt_recip = ''; # per-recipient part of dsn text according to RFC 3464
my $all_rejected = 0;
if (@$per_recip_data) {
$all_rejected = 1;
@@ -10168,6 +10839,8 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
}
$dsn_cutoff_level_bysender = lookup2(0,$sender,$cutoff_bysender_maps);
}
+
+ my $txt_recip = ''; # per-recipient part of dsn text according to RFC 3464
my($any_succ,$any_fail,$any_delayed) = (0,0,0); local($1);
for my $r (!$is_dsn ? () : @$per_recip_data) { # prepare per-recip fields
my $recip = $r->recip_addr;
@@ -10204,8 +10877,9 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
elsif ($_ eq 'NEVER') { $notify_never = 1 }
}
}
- if ($notify_never || $sender eq '')
- { $notify_on_failure = $notify_on_success = $notify_on_delay = 0 }
+ if ($notify_never || $sender eq '') {
+ $notify_on_failure = $notify_on_success = $notify_on_delay = 0;
+ }
my $dest = $r->recip_destiny;
my $remote_or_local = $recip_done==2 ? 'from MTA' :
$recip_done==1 ? '.' : # this agent
@@ -10305,24 +10979,39 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
# to the rules above, the DSN SHOULD NOT contain information for
# recipients for whom DSNs would not otherwise have been issued."
$txt_recip .= "\n"; # empty line between groups of per-recipient fields
+
my $dsn_orcpt = $r->dsn_orcpt;
if (defined $dsn_orcpt) {
- my($addr_type,$orcpt) = orcpt_decode($dsn_orcpt);
- $txt_recip .= "Original-Recipient: " .
- sanitize_str($addr_type.';'.$orcpt) . "\n";
+ # RFC 6533: systems generating a message/global-delivery-status
+ # body part SHOULD use the utf-8-address form of the UTF-8 address
+ # type for all addresses containing characters outside the ASCII
+ # repertoire. These systems SHOULD upconvert the utf-8-addr-xtext
+ # or the utf-8-addr-unitext form of a UTF-8 address type in the
+ # ORCPT parameter to the utf-8-address form of a UTF-8 address type
+ # in the "Original-Recipient:" field.
+ my($addr_type, $addr) = orcpt_encode($dsn_orcpt, $is_smtputf8);
+ $txt_recip .= "Original-Recipient: $addr_type;$addr\n"; # as octets
}
my $remote_mta = $r->recip_remote_mta;
- if (!defined($dsn_orcpt) && $remote_mta ne '' &&
- $r->recip_final_addr ne $recip) {
- $txt_recip .= "X-NextToLast-Final-Recipient: rfc822;" .
- quote_rfc2821_local($recip) . "\n";
- $txt_recip .= "Final-Recipient: rfc822;" .
- quote_rfc2821_local($r->recip_final_addr) . "\n";
+ my $final_recip_encoded;
+ { # normalize recipient address (like UTF-8 decoding)
+ my($addr_type, $addr) = orcpt_decode(';'.quote_rfc2821_local($recip));
+ ($addr_type, $addr) = orcpt_encode($addr_type.';'.$addr, $is_smtputf8);
+ $final_recip_encoded = $addr_type.';'.$addr;
+ }
+ if (defined $dsn_orcpt || $remote_mta eq '' ||
+ $r->recip_final_addr eq $recip) {
+ $txt_recip .= "Final-Recipient: $final_recip_encoded\n";
} else {
- $txt_recip .= "Final-Recipient: rfc822;" .
- quote_rfc2821_local($recip) . "\n";
- }
- local($1,$2,$3); my($smtp_resp_code,$smtp_resp_enhcode,$smtp_resp_msg);
+ $txt_recip .= "X-NextToLast-Final-Recipient: $final_recip_encoded\n";
+ # normalize final recipient address (e.g. UTF-8 decoding)
+ my($addr_type, $addr) =
+ orcpt_decode(';'.quote_rfc2821_local($r->recip_final_addr));
+ ($addr_type, $addr) = orcpt_encode($addr_type.';'.$addr, $is_smtputf8);
+ $txt_recip .= "Final-Recipient: $addr_type;$addr\n";
+ }
+ my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg);
+ local($1,$2,$3);
if ($smtp_resp =~ /^ (\d{3}) [ \t-] [ \t]* ([245] \. \d{1,3} \. \d{1,3})?
\s* (.*) \z/xs) {
($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3);
@@ -10339,7 +11028,8 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
: !$dsn_passed_on ? 'relayed' # relayed to non-conforming MTA
: $warn_sender ? 'delayed' # disguised as a DELAY notification
: undef; # shouldn't happen
- } elsif ($recip_done == 1) { # faked delivery to bit bucket or quarantine
+ } elsif ($recip_done == 1) {
+ # a faked delivery to bit bucket or to a quarantine
$action = $smtp_resp_class eq '5' ? 'failed' # local reject
: $smtp_resp_class eq '2' ? 'delivered' # discard / bit bucket
: undef; # shouldn't happen
@@ -10370,6 +11060,7 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
}
$smtp_resp =~ s/\n(?![ \t])/\n /gs;
$txt_recip .= "Diagnostic-Code: smtp; $smtp_resp\n";
+ # RFC 6533 adds optional field Localized-Diagnostic
$txt_recip .= "Last-Attempt-Date: $rfc2822_dsn_time\n";
my $final_log_id = $msginfo->log_id;
$final_log_id .= '/' . $msginfo->mail_id if defined $msginfo->mail_id;
@@ -10380,19 +11071,109 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
$smtp_resp_code, $ccat_name, $spam_level, $sender, $recip);
}
} # endfor per_recip_data
+
+ # prepare a per-message part of a report
+ my $txt_msg = '';
+ my $myhost = c('myhostname'); # my FQDN (DNS) name, UTF-8 octets
+ $myhost = $is_smtputf8 ? idn_to_utf8($myhost) : idn_to_ascii($myhost);
+ my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461
+
+ if ($is_dsn) { # DSN - per-msg part of dsn text according to RFC 3464
+ my $conn = $msginfo->conn_obj;
+ my $from_mta = $conn->smtp_helo;
+ my $client_ip = $conn->client_ip;
+ $txt_msg .= "Reporting-MTA: dns; $myhost\n";
+ $txt_msg .= "Received-From-MTA: dns; $from_mta ([$client_ip])\n"
+ if $from_mta ne '';
+ $txt_msg .= "Arrival-Date: ". rfc2822_timestamp($msginfo->rx_time) ."\n";
+ my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461
+ if (defined $dsn_envid) {
+ $dsn_envid = sanitize_str(xtext_decode($dsn_envid));
+ $txt_msg .= "Original-Envelope-Id: $dsn_envid\n";
+ }
+
+ } elsif ($is_arf) { # abuse report format - RFC 5965
+ # abuse, dkim, fraud, miscategorized, not-spam, opt-out, virus, other
+ $txt_msg .= "Version: 1\n"; # required
+ $txt_msg .= "Feedback-Type: $feedback_type\n"; # required
+ # User-Agent must comply with RFC 2616, section 14.43
+ my $ua_version = "$myproduct_name/$myversion_id ($myversion_date)";
+ $txt_msg .= "User-Agent: $ua_version\n"; # required
+ $txt_msg .= "Reporting-MTA: dns; $myhost\n";
+ # optional fields:
+
+ # RFC 6692: Report generators that include an Arrival-Date report field
+ # MAY choose to express the value of that date in Universal Coordinated
+ # Time (UTC) to enable simpler correlation with local records at sites
+ # that are following the provisions of RFC 6302.
+ $txt_msg .= 'Arrival-Date: ';
+ $txt_msg .= rfc2822_utc_timestamp($msginfo->rx_time) . "\n";
+ # $txt_msg .= rfc2822_timestamp($msginfo->rx_time) . "\n";
+
+ my $cl_ip_addr = $msginfo->client_addr;
+ if (defined $cl_ip_addr) {
+ $cl_ip_addr = 'IPv6:'.$cl_ip_addr if $cl_ip_addr =~ /:[0-9a-f]*:/i &&
+ $cl_ip_addr !~ /^IPv6:/i;
+ $txt_msg .= "Source-IP: $cl_ip_addr\n";
+ }
+ # RFC 6692 (was: draft-kucherawy-marf-source-ports):
+ my $cl_ip_port = $msginfo->client_port;
+ $txt_msg .= "Source-Port: $cl_ip_port\n" if defined $cl_ip_port;
+ my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461
+ if (defined $dsn_envid) {
+ $dsn_envid = sanitize_str(xtext_decode($dsn_envid));
+ $txt_msg .= "Original-Envelope-Id: $dsn_envid\n";
+ }
+ $txt_msg .= "Original-Mail-From: " . $msginfo->sender_smtp . "\n";
+ for my $r (@$per_recip_data) {
+ $txt_msg .= "Original-Rcpt-To: " . $r->recip_addr_smtp . "\n";
+ }
+ my $sigs_ref = $msginfo->dkim_signatures_valid;
+ if ($sigs_ref) {
+ for my $sig (@$sigs_ref) {
+ my $type = $sig->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM';
+ $txt_msg .= sprintf("Reported-Domain: %s (valid %s signature by)\n",
+ $sig->domain, $type);
+ }
+ }
+ if (c('enable_dkim_verification')) {
+ for (Amavis::DKIM::generate_authentication_results($msginfo,0)) {
+ my $h = $_; $h =~ tr/\n//d; # remove potential folding points
+ $txt_msg .= "Authentication-Results: $h\n";
+ }
+ }
+ $txt_msg .= "Incidents: 1\n";
+ # Reported-URI
+ }
+
+ my($txt_8bit, $txt_utf8);
+ my($delivery_status_mime_type, $delivery_status_mime_subtype);
+ if ($is_dsn || $is_arf) {
+ $txt_8bit = ($txt_msg=~tr/\x00-\x7F//c) + ($txt_recip=~tr/\x00-\x7F//c);
+ $txt_utf8 = !$txt_8bit ||
+ (is_valid_utf_8($txt_msg) && is_valid_utf_8($txt_recip));
+ $delivery_status_mime_subtype =
+ $is_arf ? 'feedback-report'
+ : $txt_utf8 && ($is_smtputf8 || $txt_8bit) ? 'global-delivery-status'
+ : 'delivery-status';
+ $delivery_status_mime_type = 'message/' . $delivery_status_mime_subtype;
+ }
+
if ( $is_arf || $is_plain || $is_attach ||
($is_dsn && ($any_succ || $any_fail || $any_delayed)) ) {
- my(@hdr_to) = defined $notif_recips ? qquote_rfc2821_local(@$notif_recips)
+ my(@hdr_to) = $notif_recips ? qquote_rfc2821_local(@$notif_recips)
: map($_->recip_addr_smtp, @$per_recip_data);
+ $_ = mail_addr_idn_to_ascii($_) for @hdr_to;
my $hdr_from = $msginfo->setting_by_contents_category(
$is_dsn ? cr('hdrfrom_notify_sender_by_ccat') :
$request_type eq 'report' ? cr('hdrfrom_notify_report_by_ccat') :
cr('hdrfrom_notify_release_by_ccat') );
- $hdr_from = expand_variables($hdr_from);
+ # make sure it's in octets
+ $hdr_from = expand_variables(safe_encode_utf8($hdr_from));
# use the provided template text
my(%mybuiltins) = %$builtins_ref; # make a local copy
# not really needed, these header fields are overridden later
- $mybuiltins{'f'} = $hdr_from;
+ $mybuiltins{'f'} = safe_decode_utf8($hdr_from);
$mybuiltins{'T'} = \@hdr_to;
$mybuiltins{'d'} = $rfc2822_dsn_time;
$mybuiltins{'report_format'} = $msg_format;
@@ -10417,11 +11198,15 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
$request_type eq 'report' ? cr('notify_report_templ_by_ccat') :
cr('notify_release_templ_by_ccat') );
my $report_str_ref = expand($template_ref, \%mybuiltins);
+
+ # 'multipart/report' MIME type is defined in RFC 6522. The report-type
+ # parameter identifies the type of report. The parameter is the MIME
+ # subtype of the second body part of the multipart/report.
my $report_entity = build_mime_entity($report_str_ref, $msginfo,
- $is_dsn ? 'multipart/report; report-type=delivery-status' :
- $is_arf ? 'multipart/report; report-type=feedback-report' :
- 'multipart/mixed',
- $msg_format, $is_plain, 1, $attach_full_msg);
+ !$is_dsn && !$is_arf ? 'multipart/mixed'
+ : "multipart/report; report-type=$delivery_status_mime_subtype",
+ $msg_format, $is_plain, 1, $attach_full_msg);
+
my $head = $report_entity->head;
# RFC 3464: The From field of the message header section of the DSN SHOULD
# contain the address of a human who is responsible for maintaining the
@@ -10435,86 +11220,48 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
eval { $head->replace('Date', $rfc2822_dsn_time); 1 }
or do { chomp $@; die $@ };
- my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461
- $dsn_envid = sanitize_str(xtext_decode($dsn_envid)) if defined $dsn_envid;
- my $txt_msg = ''; # per-message part of a report
- if ($is_arf) { # abuse report format - RFC 5965
- # abuse, dkim, fraud, miscategorized, not-spam, opt-out, virus, other
- $txt_msg .= "Version: 1\n"; # required
- $txt_msg .= "Feedback-Type: $feedback_type\n"; # required
- # User-Agent must comply with RFC 2616, section 14.43
- my $ua_version = "$myproduct_name/$myversion_id ($myversion_date)";
- $txt_msg .= "User-Agent: $ua_version\n"; # required
- $txt_msg .= "Reporting-MTA: dns; " . c('myhostname') . "\n";
- # optional fields:
-
- # RFC 6692: Report generators that include an Arrival-Date report field
- # MAY choose to express the value of that date in Universal Coordinated
- # Time (UTC) to enable simpler correlation with local records at sites
- # that are following the provisions of RFC 6302.
- $txt_msg .= 'Arrival-Date: ';
- $txt_msg .= rfc2822_utc_timestamp($msginfo->rx_time) . "\n";
- # $txt_msg .= rfc2822_timestamp($msginfo->rx_time) . "\n";
-
- my $cl_ip_addr = $msginfo->client_addr;
- $cl_ip_addr = 'IPv6:'.$cl_ip_addr if $cl_ip_addr =~ /:[0-9a-f]*:/i &&
- $cl_ip_addr !~ /^IPv6:/i;
- $txt_msg .= "Source-IP: $cl_ip_addr\n" if defined $cl_ip_addr;
- # RFC 6692 (was: draft-kucherawy-marf-source-ports):
- my $cl_ip_port = $msginfo->client_port;
- $txt_msg .= "Source-Port: $cl_ip_port\n" if defined $cl_ip_port;
- $txt_msg .= "Original-Envelope-Id: $dsn_envid\n" if defined $dsn_envid;
- $txt_msg .= "Original-Mail-From: " . $msginfo->sender_smtp . "\n";
- for my $r (@$per_recip_data)
- { $txt_msg .= "Original-Rcpt-To: " . $r->recip_addr_smtp . "\n" }
- my $sigs_ref = $msginfo->dkim_signatures_valid;
- if ($sigs_ref) {
- for my $sig (@$sigs_ref) {
- my $type = $sig->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM';
- $txt_msg .= sprintf("Reported-Domain: %s (valid %s signature by)\n",
- $sig->domain, $type);
- }
- }
- if (c('enable_dkim_verification')) {
- for (Amavis::DKIM::generate_authentication_results($msginfo,0)) {
- my $h = $_; $h =~ tr/\n//d; # remove potential folding points
- $txt_msg .= "Authentication-Results: $h\n";
- }
- }
- $txt_msg .= "Incidents: 1\n";
- # Reported-URI
- } elsif ($is_dsn) { # DSN - per-msg part of dsn text according to RFC 3464
- my $conn = $msginfo->conn_obj;
- my $from_mta = $conn->smtp_helo;
- my $client_ip = $conn->client_ip;
- $txt_msg .= "Reporting-MTA: dns; " . c('myhostname') . "\n";
- $txt_msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n"
- if $from_mta ne '';
- $txt_msg .= "Arrival-Date: ". rfc2822_timestamp($msginfo->rx_time) ."\n";
- $txt_msg .= "Original-Envelope-Id: $dsn_envid\n" if defined $dsn_envid;
- }
if ($is_dsn || $is_arf) { # attach a delivery-status or a feedback-report
+ ll(4) && do_log(4,"dsn: creating mime part %s, %s",
+ $delivery_status_mime_type,
+ !$txt_8bit ? 'us-ascii' : $txt_utf8 ? 'valid UTF-8'
+ : '8bit but *not* UTF-8');
eval { # make sure our source line number is reported in case of failure
+ # RFC 6533: Note that [RFC6532] relaxed a restriction from MIME
+ # [RFC2046] regarding the use of Content-Transfer-Encoding in new
+ # "message" subtypes. This specification explicitly allows the
+ # use of Content-Transfer-Encoding in message/global-headers and
+ # message/global-delivery-status.
+ # RFC 5965: Encoding considerations for message/feedback-report:
+ # "7bit" encoding is sufficient and MUST be used to maintain
+ # readability when viewed by non-MIME mail readers.
$report_entity->add_part(
- MIME::Entity->build(Top => 0,
- Type => $is_dsn ? 'message/delivery-status'
- : 'message/feedback-report',
- Encoding => '7bit', Disposition => 'inline',
- Filename => $is_arf ? 'arf_status' : 'dsn_status',
- Description => $is_arf ? "\u$feedback_type report" :
- $any_fail ? 'Delivery error report' :
- $any_delayed ? 'Delivery delay report' :
- 'Delivery report',
- Data => $txt_msg.$txt_recip),
- 1); # insert as a second mime part (at offset 1)
+ MIME::Entity->build(
+ Top => 0,
+ Type => $delivery_status_mime_type,
+ Data => $txt_msg . $txt_recip,
+ $delivery_status_mime_subtype ne 'global-delivery-status' ? ()
+ : (Charset => 'UTF-8'),
+ Encoding => $txt_8bit ? '8bit' : '7bit',
+ Disposition => 'inline',
+ Filename => $is_arf ? 'arf_status'
+ : $delivery_status_mime_subtype eq
+ 'global-delivery-status' ? 'dsn_status.u8dsn'
+ : 'dsn_status.dsn',
+ Description => $is_arf ? "\u$feedback_type report"
+ : $any_fail ? 'Delivery error report'
+ : $any_delayed ? 'Delivery delay report'
+ : 'Delivery report',
+ ), 1); # insert as a second mime part (at offset 1)
1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
die $eval_stat;
};
}
- my $mailfrom = $is_dsn ? '' # DSN envelope sender must be empty
- : unquote_rfc2821_local( (parse_address_list($hdr_from))[0] );
+ my $mailfrom =
+ $is_dsn ? '' # DSN envelope sender must be empty
+ : mail_addr_idn_to_ascii(
+ unquote_rfc2821_local( (parse_address_list($hdr_from))[0] ));
$notification = Amavis::In::Message->new;
$notification->rx_time($dsn_time);
$notification->log_id($msginfo->log_id);
@@ -10524,26 +11271,34 @@ sub delivery_status_notification($$$;$$$$) { # ..._or_report
$notification->conn_obj($msginfo->conn_obj);
$notification->originating(
($request_type eq 'dsn' || $request_type eq 'report') ? 1 : 0);
- # $notification->body_type('7BIT');
$notification->mail_text($report_entity);
+ $notification->body_type($txt_8bit ? '8BITMIME' : '7BIT');
$notification->add_contents_category(CC_CLEAN,0);
+ my(@recips) = $notif_recips ? @$notif_recips
+ : map($_->recip_addr, @$per_recip_data);
+ if ($request_type eq 'dsn' || $request_type eq 'report') {
+ my $bcc = $msginfo->setting_by_contents_category(cr('dsn_bcc_by_ccat'));
+ push(@recips, $bcc) if defined $bcc && $bcc ne '';
+ }
+ if (grep( / [^\x00-\x7F] .*? \@ [^@]* \z/sx && is_valid_utf_8($_),
+ ($mailfrom, @recips) )) {
+ # localpart is non-ASCII UTF-8, we must use SMTPUTF8
+ do_log(2, 'DSN notification requires SMTPUTF8');
+ $notification->smtputf8(1);
+ } else {
+ $_ = mail_addr_idn_to_ascii($_) for ($mailfrom, @recips);
+ }
$notification->sender($mailfrom);
$notification->sender_smtp(qquote_rfc2821_local($mailfrom));
$notification->auth_submitter('<>');
$notification->auth_user(c('amavis_auth_user'));
$notification->auth_pass(c('amavis_auth_pass'));
+ $notification->recips(\@recips, 1);
if (defined $hdr_from) {
- my(@rfc2822_from) = map(unquote_rfc2821_local($_),
- parse_address_list($hdr_from));
+ my(@rfc2822_from) =
+ map(unquote_rfc2821_local($_), parse_address_list($hdr_from));
$notification->rfc2822_from($rfc2822_from[0]);
}
- my $bcc;
- if ($request_type eq 'dsn' || $request_type eq 'report') {
- $bcc = $msginfo->setting_by_contents_category(cr('dsn_bcc_by_ccat'));
- }
- $notification->recips([($notif_recips ? @$notif_recips
- : map($_->recip_addr, @$per_recip_data)),
- defined $bcc && $bcc ne '' ? $bcc : () ], 1);
my $notif_m = c('notify_method');
$_->delivery_method($notif_m) for @{$notification->per_recip_data};
}
@@ -10583,9 +11338,10 @@ sub delivery_short_report($) {
# Build a new MIME::Entity object based on the original mail, but hopefully
# safer to mail readers: conventional mail header fields are retained,
-# original mail becomes an attachment of type 'message/rfc822'.
-# Text in $first_part becomes the first MIME part of type 'text/plain',
-# $first_part may be a scalar string or a ref to a list of lines
+# original mail becomes an attachment of type 'message/rfc822' or
+# 'message/global'. Text in $first_part becomes the first MIME part
+# of type 'text/plain', $first_part may be a scalar string or a ref
+# to a list of lines
#
sub defanged_mime_entity($$) {
my($msginfo,$first_part) = @_;
@@ -10620,13 +11376,13 @@ sub defanged_mime_entity($$) {
$curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s
? ($1, $2) : (undef, $curr_head);
if ($desired_field{lc($field_name)}) { # only desired header fields
- # protect NUL, CR, and characters with codes above \177
- $field_body =~ s{ ( [^\001-\014\016-\177] ) }
- { sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o',
- ord($1)) }gsxe;
+ # protect NUL, CR, and characters with codes above \377
+ $field_body =~ s{ ( [^\001-\014\016-\377] ) }
+ { sprintf(ord($1)>255 ? '\\x{%04x}' : '\\x{%02x}',
+ ord($1)) }xgse;
# protect NL in illegal all-whitespace continuation lines
$field_body =~ s{\n([ \t]*)(?=\n)}{\\012$1}gs;
- $field_body =~ s{^(.{995}).{4,}$}{$1...}mg; # truncate lines to 998
+ $field_body =~ s{^(.{995}).{4,}$}{$1...}gm; # truncate lines to 998
chomp($field_body); # note that field body is already folded
if (lc($field_name) eq 'subject') {
# needs to be inserted directly into new header section so that it
@@ -10638,11 +11394,16 @@ sub defanged_mime_entity($$) {
}
}
}
+
eval {
+ my $cnt_8bit = $first_part =~ tr/\x00-\x7F//c;
$new_entity->attach(
- Type => 'text/plain',
- Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
- Data => $first_part);
+ Type => 'text/plain', Data => $first_part,
+ Charset => c('bdy_encoding'),
+ Encoding => !$cnt_8bit ? '7bit'
+ : $cnt_8bit > 0.2 * length($first_part) ? 'base64'
+ : 'quoted-printable',
+ );
1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
@@ -10668,8 +11429,9 @@ sub defanged_mime_entity($$) {
}
eval {
my $att = $new_entity->attach( # RFC 2046
- Type => 'message/rfc822; x-spam-type=original',
- Encoding =>($msginfo->header_8bit || $msginfo->body_8bit) ?'8bit':'7bit',
+ Type => ($msginfo->smtputf8 && $msginfo->header_8bit ? 'message/global'
+ : 'message/rfc822') . '; x-spam-type=original',
+ Encoding => $msginfo->header_8bit || $msginfo->body_8bit ? '8bit':'7bit',
Data => defined $orig_mail_as_body ? []
: !$msginfo->skip_bytes ? $msg
: substr($$msg, $msginfo->skip_bytes),
@@ -10745,21 +11507,25 @@ sub msg_from_quarantine($$$) {
} elsif ( $bsmtp && /^RSET$/i) {
$sender = undef; @recips_all = (); @recips_blocked = (); $qid = undef;
} elsif ( $bsmtp && /^QUIT$/i) { last;
- } elsif (!$bsmtp && /^Return-Path:/si) {
} elsif (!$bsmtp && /^Delivered-To:/si) {
- } elsif (!$bsmtp && /^X-Envelope-From:[ \t]*(.*)$/si) {
+ } elsif (!$bsmtp && /^(Return-Path|X-Envelope-From):[ \t]*(.*)$/si) {
if (!defined $sender) {
- my(@addr_list) = parse_address_list($1);
- @addr_list >= 1 or die "Address missing in X-Envelope-From";
- @addr_list <= 1 or die "More than one address in X-Envelope-From";
- $sender = unquote_rfc2821_local($addr_list[0]);
+ my(@addr_list) = parse_address_list($2);
+ @addr_list >= 1 or die "Address missing in $1";
+ @addr_list <= 1 or die "More than one address in $1";
+ $sender =
+ mail_addr_idn_to_ascii(unquote_rfc2821_local($addr_list[0]));
}
} elsif (!$bsmtp && /^X-Envelope-To:[ \t]*(.*)$/si) {
my(@addr_list) = parse_address_list($1);
- push(@recips_all, map(unquote_rfc2821_local($_), @addr_list));
+ push(@recips_all,
+ map(mail_addr_idn_to_ascii(unquote_rfc2821_local($_)),
+ @addr_list));
} elsif (!$bsmtp && /^X-Envelope-To-Blocked:[ \t]*(.*)$/si) {
my(@addr_list) = parse_address_list($1);
- push(@recips_blocked, map(unquote_rfc2821_local($_), @addr_list));
+ push(@recips_blocked,
+ map(mail_addr_idn_to_ascii(unquote_rfc2821_local($_)),
+ @addr_list));
$have_recips_blocked = 1;
} elsif (/^X-Quarantine-ID:[ \t]*(.*)$/si) {
$qid = $1; $qid = $1 if $qid =~ /^<(.*)>\z/s;
@@ -10856,7 +11622,8 @@ sub msg_from_quarantine($$$) {
# "Resent-From:" and "Resent-Date:" are required fields!
my $hdrfrom_recip = $msginfo->setting_by_contents_category(
cr('hdrfrom_notify_recip_by_ccat'));
- $hdrfrom_recip = expand_variables($hdrfrom_recip);
+ # make sure it's in octets
+ $hdrfrom_recip = expand_variables(safe_encode_utf8($hdrfrom_recip));
if ($msginfo->requested_by eq '') {
$hdr_edits->add_header('Resent-From', $hdrfrom_recip);
} else {
@@ -10871,10 +11638,12 @@ sub msg_from_quarantine($$$) {
: 'undisclosed-recipients:;');
$hdr_edits->add_header('Resent-Date', # time of the release
rfc2822_timestamp($msginfo->rx_time));
+ my $myhost = c('myhostname'); # my FQDN (DNS) name, UTF-8 octets
+ $myhost = $msginfo->smtputf8 ? idn_to_utf8($myhost) :idn_to_ascii($myhost);
$hdr_edits->add_header('Resent-Message-ID',
sprintf('<%s-%s@%s>',
$msginfo->parent_mail_id||'', $msginfo->mail_id||'',
- c('myhostname')) );
+ $myhost) );
}
$hdr_edits->add_header('Received', make_received_header_field($msginfo,1),1);
my $bcc = $msginfo->setting_by_contents_category(cr('always_bcc_by_ccat'));
@@ -10926,21 +11695,26 @@ sub mail_done { my($self,$conn,$msginfo) = @_; undef }
#
package Amavis;
-require 5.005; # need qr operator and \z in regexps
+require 5.005; # need qr operator and \z in regexp
+require 5.008; # need basic Unicode support
use strict;
use re 'taint';
BEGIN {
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
import Amavis::Conf qw(:platform :sa :confvars c cr ca);
import Amavis::Util qw(untaint untaint_inplace
min max minmax unique_list unique_ref
ll do_log do_log_safe update_current_log_level
dump_captured_log log_capture_enabled am_id
- sanitize_str sanitize_str_inplace debug_oneshot
- safe_encode safe_encode_ascii safe_encode_utf8
- safe_decode safe_decode_latin1 proto_decode
+ sanitize_str debug_oneshot proto_decode
+ truncate_utf_8 is_valid_utf_8
+ safe_encode safe_encode_utf8 safe_decode_mime
+ safe_decode safe_decode_utf8 safe_decode_latin1
+ clear_idn_cache idn_to_utf8 idn_to_ascii
+ mail_addr_idn_to_ascii mail_addr_decode
+ orcpt_encode orcpt_decode
format_time_interval add_entropy stir_random
generate_mail_id make_password
prolong_timer get_deadline waiting_for_client
@@ -10948,8 +11722,7 @@ BEGIN {
snmp_counters_init snmp_count dynamic_destination
ccat_split ccat_maj cmp_ccat cmp_ccat_maj
setting_by_given_contents_category_all
- setting_by_given_contents_category
- orcpt_encode orcpt_decode);
+ setting_by_given_contents_category);
import Amavis::ProcControl qw(exit_status_str proc_status_ok
cloexec run_command collect_results);
import Amavis::Log qw(open_log close_log collect_log_stats);
@@ -11104,22 +11877,32 @@ sub macro_score {
$result;
};
-# implements macro header_field, providing a named header field from a message
+# implements macro 'header_field', providing a requested header field
+# from a message; attempts decoding UTF-8 to logical characters
+# unless a macro name is 'header_field_octets'; non-decodable UTF-8
+# is left unchanged as octets
#
sub macro_header_field {
my($msginfo,$name,$header_field_name,$limit,$hf_index) = @_;
undef $hf_index if $hf_index !~ /^[+-]?\d+\z/; # defaults to last
- local($_) = $msginfo->get_header_field_body($header_field_name,$hf_index);
- if (defined $_) { # unfold, trim, protect CR, LF, \000 and \200
- chomp; s/\n(?=[ \t])//gs; s/^[ \t]+//; s/[ \t\n]+\z//;
- if ($header_field_name =~
- /^(?:Message-ID|Resent-Message-ID|In-Reply-To|References)\z/i) {
- $_ = join(' ',parse_message_id($_)) if $_ ne ''; # strip CFWS
- }
- s{([\r\n\000\200])}{sprintf("\\%03o",ord($1))}eg;
- };
- !defined($limit) || $limit =~ /^\s+\z/ ||
- $limit < 6 || length($_) <= $limit ? $_ : substr($_,0,$limit-5) . '[...]';
+ my $s = $msginfo->get_header_field_body($header_field_name, $hf_index);
+ return undef if !defined($s);
+ # unfold, trim, protect any leftover CR and LF
+ chomp($s); $s=~s/\n(?=[ \t])//gs; $s=~s/^[ \t]+//; $s=~s/[ \t\n]+\z//;
+ if ($header_field_name =~
+ /^(?:Message-ID|Resent-Message-ID|In-Reply-To|References)\z/i) {
+ $s = join(' ',parse_message_id($s)) if $s ne ''; # strip CFWS
+ }
+ if ($name ne 'header_field_octets' &&
+ $s =~ tr/\x00-\x7F//c && is_valid_utf_8($s)) {
+ eval { $s = safe_decode_utf8($s, 1|8); 1 }
+ }
+ if (defined($limit) && $limit !~ /^\s+\z/ &&
+ $limit > 5 && length($s) > $limit) {
+ substr($s,$limit-5) = ''; $s .= '[...]';
+ }
+ $s =~ s{ ( [\r\n] ) }{ sprintf('\\x{%02X}',ord($1)) }xgse;
+ $s;
};
sub dkim_test {
@@ -11148,18 +11931,20 @@ sub dkim_acceptable_signing_domain($@) {
my $sigs_ref = $msginfo->dkim_signatures_valid;
if ($sigs_ref && @$sigs_ref) {
for my $sig (@$sigs_ref) {
- my $sdid = lc($sig->domain);
+ my $sdid_ace = idn_to_ascii($sig->domain);
for (@acceptable_sdid) {
- my $ad = !defined $_ ? '' : lc($_);
+ my $ad = !defined $_ ? '' : $_;
local($1);
$ad = $1 if $ad =~ /\@([^\@]*)\z/; # compatibility with pre-2.6.5
if ($ad eq '') { # checking for author domain signature
$matches = 1 if $msginfo->dkim_author_sig;
} elsif ($ad =~ /^\.(.*)\z/s) { # domain itself or its subdomain
- my $d = $1;
- if ($sdid eq $d || $sdid =~ /\.\Q$d\E\z/s) { $matches = 1; last }
+ my $d = idn_to_ascii($1);
+ if ($sdid_ace eq $d || $sdid_ace =~ /\.\Q$d\E\z/s) {
+ $matches = 1; last;
+ }
} else {
- if ($sdid eq $ad) { $matches = 1; last }
+ if ($sdid_ace eq idn_to_ascii($ad)) { $matches = 1; last }
}
}
last if $matches;
@@ -11207,8 +11992,10 @@ sub init_builtin_macros() {
week_iso8601 => sub {iso8601_week($MSGINFO->rx_time)},
weekday => sub {iso8601_weekday($MSGINFO->rx_time)},
y => sub {sprintf("%.0f", 1000*get_time_so_far())}, # elapsed time in ms
- h => sub {c('myhostname')}, # fqdn name of this host
- HOSTNAME => sub {c('myhostname')},
+ h => sub { $MSGINFO->smtputf8
+ ? safe_decode_utf8(idn_to_utf8(c('myhostname')))
+ : idn_to_ascii(c('myhostname')) },
+ HOSTNAME => sub {safe_decode_utf8(idn_to_utf8(c('myhostname')))},
l => sub {$MSGINFO->originating ? 1 : undef}, # our client (mynets/roaming)
s => sub {$MSGINFO->sender_smtp}, # orig. unmodified env. sender addr in <>
S => sub {$MSGINFO->sender_smtp}, # kept for compatibility, avoid!
@@ -11220,8 +12007,10 @@ sub init_builtin_macros() {
N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, #short dsn
actions_performed => sub {join(',',@{$MSGINFO->actions_performed||[]})},
Q => sub {$MSGINFO->queue_id}, # MTA queue ID of the message if known
- m => sub {macro_header_field($MSGINFO,'header','Message-ID')},
- r => sub {macro_header_field($MSGINFO,'header','Resent-Message-ID')},
+ m => sub {my $m_id = $MSGINFO->get_header_field_body('message-id');
+ defined $m_id ? (parse_message_id($m_id))[0] : undef },
+ r => sub {my $m_id = $MSGINFO->get_header_field_body('resent-message-id');
+ defined $m_id ? (parse_message_id($m_id))[0] : undef },
j => sub {macro_header_field($MSGINFO,'header','Subject')},
log_domains => sub {
my %domains;
@@ -11240,14 +12029,15 @@ sub init_builtin_macros() {
!defined($s) ? undef : qquote_rfc2821_local($s) },
rfc2822_from => sub {my $f = $MSGINFO->rfc2822_from;
!defined($f) ? undef :
- qquote_rfc2821_local(ref $f ? @$f : $f)},
+ qquote_rfc2821_local(ref $f ? @$f : $f)},
rfc2822_resent_sender => sub {my $rs = $MSGINFO->rfc2822_resent_sender;
!defined($rs) ? undef :
qquote_rfc2821_local(grep(defined $_, @$rs))},
rfc2822_resent_from => sub {my $rf = $MSGINFO->rfc2822_resent_from;
!defined($rf) ? undef :
qquote_rfc2821_local(grep(defined $_, @$rf))},
- header_field => sub {macro_header_field($MSGINFO, at _)},
+ header_field_octets => sub {macro_header_field($MSGINFO, at _)}, # as octets
+ header_field => sub {macro_header_field($MSGINFO, at _)}, # as characters
HEADER => sub {macro_header_field($MSGINFO, at _)},
useragent => # argument: 'name' or 'body', or empty to return entire field
sub { my($macro_name,$which_part) = @_; my($head,$body);
@@ -11411,15 +12201,36 @@ sub init_builtin_macros() {
TESTS => sub {macro_tests($MSGINFO,undef, at _)}, # tests without scores
z => sub {$MSGINFO->msg_size}, #mail size as defined by RFC 1870, or approx
ip_trace_all => sub { # all IP addresses in the Received trace, top-down
- my $ip_trace = $MSGINFO->ip_addr_trace;
- return if !$ip_trace;
- [ map(defined $_ ? sanitize_str($_) : 'x', @$ip_trace) ];
+ my $trace = $MSGINFO->trace; return if !$trace;
+ [ map(defined $_ ? sanitize_str($_) : 'x',
+ map($_->{ip}, @$trace)) ];
},
ip_trace_public => sub { # all public IP addresses in the Received trace
my $ip_trace = $MSGINFO->ip_addr_trace_public;
return if !$ip_trace;
[ map(defined $_ ? sanitize_str($_) : 'x', @$ip_trace) ];
},
+ ip_proto_trace_all => sub { # from a Received trace
+ # protocol type from the WITH clause and an IP address
+ my $trace_ref = $MSGINFO->trace; return if !$trace_ref;
+ my(@trace) = @$trace_ref;
+ shift(@trace); # chop off the last hop (MTA -> amavisd)
+ [ map(sanitize_str( (!$_->{with} ? '' : $_->{with}.'://') .
+ (!$_->{ip} ? 'x' : !$_->{port} ? $_->{ip}
+ : '['.$_->{ip}.']:'.$_->{port})), at trace)];
+ },
+ ip_proto_trace_public => sub { # from a Received trace
+ # protocol type from the WITH clause and an IP address
+ my $trace_ref = $MSGINFO->trace; return if !$trace_ref;
+ my(@trace) = @$trace_ref;
+ shift(@trace); # chop off the last hop (MTA -> amavisd)
+ [ map(sanitize_str( (!$_->{with} ? '' : $_->{with}.'://') .
+ (!$_->{ip} ? 'x' : !$_->{port} ? $_->{ip}
+ : '['.$_->{ip}.']:'.$_->{port}) ),
+ grep($_->{public}, @trace)) ];
+ },
+ protocol => # "WITH protocol type" as seen by amavisd (the last hop)
+ sub { my $c = $MSGINFO->conn_obj; !$c ? '' : $c->appl_proto },
t => sub { # first (oldest) entry in the Received trace
sanitize_str(first_received_from($MSGINFO)) },
e => sub { # first (oldest) valid public IP in the Received trace,
@@ -11434,8 +12245,9 @@ sub init_builtin_macros() {
},
g => sub { # original SMTP session client DNS name
sanitize_str($MSGINFO->client_name) },
- client_helo => sub { # original SMTP session EHLO/HELO name
- sanitize_str($MSGINFO->client_helo) },
+ client_helo => sub { # original SMTP session EHLO/HELO name
+ sanitize_str($MSGINFO->client_helo) },
+ client_protocol => sub { $MSGINFO->client_proto }, # XFORWARD PROTO, AM.PDP
remote_mta => sub { unique_ref(map($_->recip_remote_mta,
@{$MSGINFO->per_recip_data})) },
smtp_response => sub { unique_ref(map($_->recip_smtp_response,
@@ -11528,45 +12340,48 @@ sub init_builtin_macros() {
b64urlenc => sub {my $nm=shift;
join('', map { my $s=encode_base64($_,'');
$s=~s/=+\z//; $s=~tr{+/}{-_}; $s } @_)},
- mime2utf8 => sub { # convert to UTF-8 octets, truncate to $max_len if given
+ mail_addr_decode => sub {my($nm,$addr) = @_; mail_addr_decode($addr,0)},
+ mail_addr_decode_octets =>
+ sub {my($nm,$addr) = @_; mail_addr_decode($addr,1)},
+ mime_decode => sub {
+ # convert RFC 2047 encoded-words or UTF-8 octets to logical characters,
+ # truncate to $max_len characters if limit is provded
my($nm,$str,$max_len,$both_if_diff) = @_;
- if (!defined $str || $str eq '') {
- $str = '';
- } else {
- eval {
- my $chars = safe_decode('MIME-Header',$str); # logical characters
- my $octets = safe_encode_utf8($chars); # bytes, UTF-8 encoded
- if (defined $max_len && $max_len > 0 && length($octets) > $max_len) {
- local($1);
- if ($octets =~ /^(.{0,$max_len})(?=[\x00-\x7F\xC0-\xFF]|\z)/s) {
- $octets = $1; # cleanly chop a UTF-8 byte sequence, RFC 3629
- }
- }
- if (!$both_if_diff) {
- $str = $octets;
- } else {
- # only compare the visible part
- if (defined $max_len && $max_len > 0 && length($str) > $max_len) {
- substr($str,$max_len) = '';
- }
- $str = $octets . ' (raw: ' . $str . ')' if $octets ne $str;
- }
- 1;
- } or do {
- my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
- do_log(5, "macro mime2utf8: malformed string, keeping raw bytes: %s",
- $eval_stat);
- if (defined $max_len && $max_len > 0 && length($str) > $max_len) {
- substr($str,$max_len) = '';
- }
- };
- }
+ return '' if !defined $str || $str eq '';
+ my $chars = safe_decode_mime($str); # octets to logical characters
+ if (!defined $max_len || $max_len <= 0) { # no size limit
+ return $chars if !$both_if_diff;
+ $chars .= ' (raw: ' . $str . ')' if $chars ne $str;
+ } else { # truncate characters string at $max_len
+ substr($chars,$max_len) = '' if length($chars) > $max_len;
+ return $chars if !$both_if_diff;
+ # only compare the visible part
+ my $octets = safe_encode_utf8($chars);
+ substr($str,length($octets)) = '' if length($str) > length($octets);
+ $chars .= ' (raw: ' . $str . ')' if $str ne $chars;
+ }
+ $chars;
+ },
+ mime2utf8 => sub {
+ # convert RFC 2047 encoded-words or UTF-8 to UTF-8 octets,
+ # truncate to $max_len characters if limit is provded
+ my($nm,$str,$max_len,$both_if_diff) = @_;
+ return '' if !defined $str || $str eq '';
+ my $chars = safe_decode_mime($str); # to logical characters
+ my $octets = safe_encode_utf8($chars); # to bytes, UTF-8 encoded
+ $octets = truncate_utf_8($octets,$max_len);
+ return $octets if !$both_if_diff;
+ # only compare the visible part
+ if (defined $max_len && $max_len > 0 && length($str) > $max_len) {
+ substr($str,$max_len) = '';
+ }
+ $str = $octets . ' (raw: ' . $str . ')' if $octets ne $str;
$str;
},
report_json => sub {
return if !$report_ref; # ugly globals
structured_report_update_time($report_ref);
- safe_encode_utf8(Amavis::JSON::encode($report_ref));
+ return Amavis::JSON::encode($report_ref); # as a string of characters
},
# macros f, T, C, B will be defined for each notification as appropriate
# (representing From:, To:, Cc:, and Bcc: respectively)
@@ -11640,8 +12455,13 @@ sub init_tokenize_templates() {
notify_release_templ notify_report_templ notify_autoresp_templ);
for my $bank_name (keys %policy_bank) {
for my $n (@templ_names) { # tokenize templates to speed up macro expansion
- my $s = $policy_bank{$bank_name}{$n}; $s = $$s if ref($s) eq 'SCALAR';
- $policy_bank{$bank_name}{$n} = tokenize(\$s) if defined $s;
+ my $s = $policy_bank{$bank_name}{$n};
+ $s = $$s if ref($s) eq 'SCALAR';
+ if (defined $s) {
+ # encode log templates to UTF-8, leave the rest as character strings
+ $s = safe_encode_utf8($s) if $n eq 'log_templ' || 'log_recip_templ';
+ $policy_bank{$bank_name}{$n} = tokenize(\$s);
+ }
}
}
}
@@ -11737,15 +12557,15 @@ sub after_chroot_init() {
grep(/\.pm\z/, keys %INC)) {
next if !grep($_ eq $m, qw(Amavis::Conf
Archive::Tar Archive::Zip Compress::Zlib Compress::Raw::Zlib
- Convert::TNEF Convert::UUlib
+ Convert::TNEF Convert::UUlib File::LibMagic
MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet
Digest::MD5 Digest::SHA Digest::SHA1 Crypt::OpenSSL::RSA
Authen::SASL Authen::SASL::XS Authen::SASL::Cyrus Authen::SASL::Perl
Encode Scalar::Util Time::HiRes File::Temp Unix::Syslog Unix::Getrusage
Socket Socket6 IO::Socket::INET6 IO::Socket::IP IO::Socket::SSL
- Net::Server NetAddr::IP Net::DNS Net::SSLeay Net::Patricia Net::LDAP
- Mail::ClamAV Mail::SpamAssassin Mail::DKIM::Verifier Mail::DKIM::Signer
- Mail::SPF Mail::SPF::Query URI Razor2::Client::Version
+ Net::Server NetAddr::IP Net::DNS Net::LibIDN Net::SSLeay Net::Patricia
+ Net::LDAP Mail::SpamAssassin Mail::DKIM::Verifier Mail::DKIM::Signer
+ Mail::ClamAV Mail::SPF Mail::SPF::Query URI Razor2::Client::Version
DBI DBD::mysql DBD::Pg DBD::SQLite BerkeleyDB DB_File
ZMQ ZMQ::LibZMQ2 ZMQ::LibZMQ3 ZeroMQ SAVI Anomy::Sanitizer));
do_log(0, "Module %-19s %s", $m, eval{$m->VERSION} || '?');
@@ -11866,7 +12686,7 @@ sub set_sockets_access() {
if ($s =~ m{^(/.+)\|unix\z}si) {
my $path = $1;
chmod($unix_socket_mode,$path)
- or do_log(-1, "Error setting mode 0%o on a socket %s: %s",
+ or do_log(-1, "Error setting mode 0%03o on a socket %s: %s",
$unix_socket_mode, $path, $!);
}
}
@@ -12103,6 +12923,7 @@ sub child_init_hook {
$redis_storage = Amavis::Redis->new(@storage_redis_dsn);
}
$spamcontrol_obj->init_child if $spamcontrol_obj;
+ # Amavis::Util::dump_subs();
1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
@@ -12136,6 +12957,8 @@ sub post_accept_hook {
Amavis::Timing::init(); snmp_counters_init();
$zmq_obj->register_proc(1,1,'A') if $zmq_obj; # enter 'accept' state
$snmp_db->register_proc(1,1,'A') if $snmp_db;
+ if ($child_invocation_count % 13 == 0) # every now and then
+ { clear_idn_cache(); clear_query_keys_cache() }
load_policy_bank(''); # start with a builtin baseline policy bank
}
@@ -12388,8 +13211,9 @@ sub process_request {
$peer_addr = normalize_ip_addr($peer_addr);
}
$conn->socket_port($prop->{sockport});
- $conn->socket_ip($sock_addr);
- $conn->client_ip($peer_addr);
+ $conn->client_port($prop->{peerport});
+ $conn->socket_ip(untaint($sock_addr));
+ $conn->client_ip(untaint($peer_addr)); # untaint just in case
}
if ($suggested_protocol eq 'SMTP' || $suggested_protocol eq 'LMTP' ||
($suggested_protocol eq '' && $ns_proto =~ /^(?:TCP|SSLEAY|SSL)\z/)) {
@@ -12595,7 +13419,7 @@ sub process_tcp_lookup_request($$) {
sub tcp_lookup_encode($) {
my $str = $_[0]; local($1);
- $str =~ s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/egs;
+ $str =~ s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/gse;
$str;
}
@@ -12616,7 +13440,7 @@ sub check_mail_begin_task() {
undef $av_output; @detecting_scanners = (); @av_scanners_results = ();
@virusname = (); @bad_headers = ();
$banned_filename_any = $banned_filename_all = 0;
- undef $MSGINFO; # just in case
+ undef $MSGINFO; undef $report_ref;
}
# create a mail_id unique to a database and save preliminary info to SQL;
@@ -12663,19 +13487,18 @@ sub generate_unique_mail_id($) {
$mail_id;
}
-sub extract_ip_addresses_from_received_trace($) {
+sub extract_info_from_received_trace($) {
my($msginfo) = @_;
- my(@ip_trace);
+ my(@trace);
for (my $j=0; ; $j++) { # walk through Received header fields, top-down
my $r = $msginfo->get_header_field_body('received',$j);
last if !defined $r;
- my $ip = fish_out_ip_from_received($r); # possibly undef
+ my $fields_ref = parse_received($r);
+ my $ip = fish_out_ip_from_received($r,$fields_ref); # possibly undef
$ip = normalize_ip_addr($ip) if defined $ip;
- push(@ip_trace, $ip); # possibly undef
+ push(@trace, { ip => $ip, %$fields_ref });
}
- ll(3) && do_log(3, "ip_trace: %s",
- join(' < ', map(defined $_ ? $_ : 'x', @ip_trace)));
- @ip_trace;
+ \@trace;
}
# Collects some information derived from the envelope and the message,
@@ -12699,6 +13522,7 @@ sub collect_some_info($) {
my $rfc2822_sender = $msginfo->get_header_field_body('sender');
my $rfc2822_from_field = $msginfo->get_header_field_body('from');
my(@rfc2822_from); # RFC 5322 (ex RFC 2822) allows multiple author's addr
+ local($1);
if (defined $rfc2822_sender) {
my(@sender_parsed) = map(unquote_rfc2821_local($_),
parse_address_list($rfc2822_sender));
@@ -12784,22 +13608,59 @@ sub collect_some_info($) {
do_log(4,"message size unknown, size set to %d", $mail_size);
}
- my @ip_trace = extract_ip_addresses_from_received_trace($msginfo);
+ my $trace_ref = extract_info_from_received_trace($msginfo);
my $cl_ip = $msginfo->client_addr;
if (defined $cl_ip) {
- my $last_hop_ip = $ip_trace[0];
+ my $last_hop = $trace_ref->[0];
+ my $last_hop_ip = $last_hop && $last_hop->{ip};
if (!defined $last_hop_ip || lc($cl_ip) ne lc($last_hop_ip)) { # milter?
do_log(5,"prepending client's IP address to trace: %s", $cl_ip);
- unshift(@ip_trace, $cl_ip);
- }
- }
- my @ip_trace_public =
- map(do { my($public,$key,$err) = lookup_ip_acl($_, @public_networks_maps);
- $public && !$err ? $_ : () },
- grep(defined $_ && $_ ne '', @ip_trace) );
- $msginfo->ip_addr_trace(\@ip_trace);
+ unshift(@$trace_ref, {
+ ip => $msginfo->client_addr,
+ port => $msginfo->client_port,
+ with => $msginfo->client_proto,
+ });
+ } elsif ($last_hop->{ip} && !$last_hop->{port}) {
+ # add a missing information, not available in a Received trace
+ $last_hop->{port} = $msginfo->client_port;
+ }
+ }
+ { # add the last hop (ours, currently underway) to the trace
+ my $conn = $msginfo->conn_obj; # the connection between MTA and amavisd
+ my $recips = $msginfo->recips;
+ my $myhelo = c('localhost_name'); # my EHLO/HELO/LHLO name, UTF-8 octets
+ $myhelo = 'localhost' if $myhelo eq '';
+ $myhelo = $msginfo->smtputf8 ? idn_to_utf8($myhelo) : idn_to_ascii($myhelo);
+ unshift(@$trace_ref, {
+ ip => $conn->client_ip,
+ port => $conn->client_port,
+ from => $conn->smtp_helo,
+ by => $myhelo,
+ with => $conn->appl_proto,
+ # id => $msginfo->mail_id, # not yet known
+ $recips && @$recips==1 ? (for => qquote_rfc2821_local(@$recips)) : (),
+ # ";" => rfc2822_timestamp($msginfo->rx_time), # not needed
+ });
+ }
+
+ my(@ip_trace_public);
+ for my $hop (@$trace_ref) {
+ next if !$hop;
+ my $ip = $hop->{ip};
+ if ($ip) {
+ my($public,$key,$err) = lookup_ip_acl($ip, @public_networks_maps);
+ if ($public && !$err) { $hop->{public} = 1; push(@ip_trace_public,$ip) }
+ }
+ my $with = $hop->{with};
+ $hop->{with} = $with if defined $with && $with =~ tr/A-Za-z0-9.+-/_/c;
+ }
+ $msginfo->trace($trace_ref);
$msginfo->ip_addr_trace_public(\@ip_trace_public);
-
+# ll(5) && do_log(5, "trace: %s", Amavis::JSON::encode($trace_ref));
+ ll(3) && do_log(3, "trace: %s",
+ join(' < ', map( (!$_->{with} ? '' : $_->{with}.'://') .
+ (!$_->{ip} ? 'x' : !$_->{port} ? $_->{ip}
+ : '['.$_->{ip}.']:'.$_->{port}), @$trace_ref ) ));
# check for mailing lists, bulk mail and auto-responses
my $is_mlist; # mail from a mailing list
my $is_auto; # bounce, auto-response, challenge-response, ...
@@ -12946,15 +13807,12 @@ sub check_mail($$) {
collect_some_info($msginfo);
if (!defined($msginfo->client_addr)) { # fetch missing IP addr from header
- my $ip_trace = $msginfo->ip_addr_trace; # 'Received' trace, top-down
- if ($ip_trace) {
- for my $cl_ip (@$ip_trace) {
- if (defined $cl_ip && $cl_ip ne '') {
- do_log(3,"client IP address unknown, fetched from Received: %s",
- $cl_ip);
- $msginfo->client_addr($cl_ip);
- last;
- }
+ my $trace_ref = $msginfo->trace; # 'Received' trace info, top-down
+ for my $hop ($trace_ref ? @$trace_ref : ()) {
+ my $ip = $hop && $hop->{ip};
+ if (defined $ip && $ip ne '') {
+ do_log(3,"client IP address unknown, fetched from Received: %s",$ip);
+ $msginfo->client_addr($ip); last;
}
}
}
@@ -13028,7 +13886,7 @@ sub check_mail($$) {
($dst_ip,$dst_port) = ($1.$2, $3) if defined($dst) &&
$dst =~ m{^(?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) }six;
$os_fingerprint_obj = Amavis::OS_Fingerprint->new(
- dynamic_destination($os_fingerprint_method,$conn),
+ untaint(dynamic_destination($os_fingerprint_method,$conn)),
0.050, $cl_ip, $msginfo->client_port, $dst_ip, $dst_port,
defined $mail_id ? $mail_id : sprintf("%08x",rand(0x7fffffff)) );
}
@@ -13225,7 +14083,7 @@ sub check_mail($$) {
$which_section = "check_header";
my $allowed_tests = cr('allowed_header_tests');
my($badh_ref,$minor_badh_cc);
- if ($allowed_tests && %$allowed_tests) {
+ if ($allowed_tests && %$allowed_tests) { # any test enabled?
($badh_ref,$minor_badh_cc) = check_header_validity($msginfo);
$msginfo->checks_performed->{H} = 1;
if (@$badh_ref) {
@@ -13319,7 +14177,14 @@ sub check_mail($$) {
or die sprintf("Can't create hard link %s to %s: %s",
$newpart, $msginfo->mail_text_fn, $!);
$newpart_obj->type_short('MAIL'); # case sensitive
- $newpart_obj->type_declared('message/rfc822');
+ if ($msginfo->smtputf8 && $msginfo->header_8bit) {
+ # RFC 6532 section 3.7
+ $newpart_obj->type_declared('message/global');
+ $newpart_obj->name_declared('message.u8msg');
+ } else {
+ $newpart_obj->type_declared('message/rfc822');
+ $newpart_obj->name_declared('message.msg');
+ }
}
}
@@ -14254,7 +15119,7 @@ sub check_mail($$) {
}
$which_section = "delivery-notification"; $t0_sect = Time::HiRes::time;
- # generate a delivery status notification according to RFC 3462 & RFC 3464
+ # generate a delivery status notification according to RFC 6522 & RFC 3464
my($notification,$suppressed) = delivery_status_notification(
$msginfo, $dsn_per_recip_capable, \%builtins,
[$sender], 'dsn', undef, undef);
@@ -14653,13 +15518,18 @@ sub check_mail($$) {
my $msg = "$which_section FAILED: $eval_stat";
if ($point_of_no_return) {
do_log(-2, "TROUBLE in check_mail, but must continue (%s): %s",
- $point_of_no_return,$msg);
+ $point_of_no_return, $msg);
} else {
do_log(-2, "TROUBLE in check_mail: %s", $msg);
+ undef $smtp_resp; # to be provided below
+ }
+ if (!defined($smtp_resp)) {
$smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg";
$exit_code = EX_TEMPFAIL;
- for my $r (@{$msginfo->per_recip_data})
- { $r->recip_smtp_response($smtp_resp); $r->recip_done(1) }
+ for my $r (@{$msginfo->per_recip_data}) {
+ next if $r->recip_done;
+ $r->recip_smtp_response($smtp_resp); $r->recip_done(1);
+ }
}
};
@@ -14696,7 +15566,7 @@ sub check_mail($$) {
do_log(-1, "signal: %s", join(', ',keys %got_signals)) if %got_signals;
undef $MSGINFO; # release global reference
($smtp_resp, $exit_code, $preserve_evidence);
-}
+} # end check_mail
# ROT13 obfuscation (Caesar cipher)
# (possibly useful as a weak privacy measure when analyzing logs)
@@ -14707,14 +15577,14 @@ sub rot13 {
$str;
}
-# Assemble a structured report, suitable for JSON serialization,
-# useful in save_info_final()
+# Assemble a structured report, suitable for JSON serialization, useful
+# in save_info_final(). Resulting string is in Perl logical characters
+# (not necessarily with UTF8 flag set if all-ASCII).
#
sub structured_report($;$) {
my($msginfo, $notification_type) = @_;
my(@recipients); # per-recipient records
- my(@rcpt_to_list); # list of recipient addresses
my(@queued_as_list); # list of unique MTA queue IDs of forwarded mail
my(@smtp_status_code_list); # list of unique SMTP responses
my(@destiny_list); # list of destiny names
@@ -14723,20 +15593,38 @@ sub structured_report($;$) {
my $true = Amavis::JSON::boolean(1);
local($1,$2);
+ my $sender_smtp = $msginfo->sender_smtp;
+ $sender_smtp =~ s/^<(.*)>\z/$1/s;
+ my(@rcpt_smtp) = map($_->recip_addr_smtp, @{$msginfo->per_recip_data});
+ s/^<(.*)>\z/$1/s for @rcpt_smtp;
+
+ my $h_sender = $msginfo->rfc2822_sender; # undef or scalar
+ my $h_from = $msginfo->rfc2822_from; # undef, scalar or listref
+ my $h_to = $msginfo->rfc2822_to; # undef, scalar or listref
+ my $h_cc = $msginfo->rfc2822_cc; # undef, scalar or listref
+ my(@arr_h_from, @arr_h_to, @arr_h_cc);
+ @arr_h_from = ref $h_from ? @$h_from : $h_from if defined $h_from;
+ @arr_h_to = ref $h_to ? @$h_to : $h_to if defined $h_to;
+ @arr_h_cc = ref $h_cc ? @$h_cc : $h_cc if defined $h_cc;
+
+ # Message-ID can contain an international domain name with A-labels
+ my(@arr_m_id, @arr_refs);
+ my $m_id = $msginfo->get_header_field_body('message-id');
+ @arr_m_id = parse_message_id($m_id) if defined $m_id && $m_id ne '';
+ my $h_refs = $msginfo->references;
+ @arr_refs = @$h_refs if $h_refs;
+ $_ = mail_addr_decode($_) for (@arr_m_id, @arr_refs,
+ $sender_smtp, @rcpt_smtp, $h_sender,
+ @arr_h_from, @arr_h_to, @arr_h_cc);
+ my $j = 0;
for my $r (@{$msginfo->per_recip_data}) {
- my $recip_smtp = $r->recip_addr_smtp;
- if (defined $recip_smtp) {
- $recip_smtp =~ s/^<(.*)>\z/$1/s;
- $recip_smtp =~ s/(\@[^@]+)\z/lc $1/se; # lowercase a domain part
- }
- push(@rcpt_to_list, $recip_smtp);
- my $orig_addr = $r->dsn_orcpt; # RCPT command ORCPT option, RFC 3461
- if (defined $orig_addr) {
- $orig_addr = orcpt_decode($orig_addr);
- $orig_addr =~ s/(\@[^@]+)\z/lc $1/se; # lowercase a domain part
- if (defined $recip_smtp && $orig_addr eq $recip_smtp) {
- undef $orig_addr; # is redundant
- }
+ my $recip_smtp = $rcpt_smtp[$j++]; # already processed for UTF-8
+ my $orig_rcpt = $r->dsn_orcpt; # RCPT command ORCPT option, RFC 3461
+ if (defined $orig_rcpt) {
+ my($addr_type, $addr) = orcpt_encode($orig_rcpt,1); # to octets
+ # is orcpt redundant?
+ $orig_rcpt = defined $recip_smtp && $addr eq $recip_smtp ? undef
+ : safe_decode_utf8($addr); # to characters
}
my $dest = $r->recip_destiny;
my $resp = $r->recip_smtp_response;
@@ -14783,13 +15671,13 @@ sub structured_report($;$) {
my(%recip) = (
rcpt_to => $recip_smtp,
- defined $orig_addr ? (rcpt_to_orig => $orig_addr) : (),
+ defined $orig_rcpt ? (rcpt_to_orig => $orig_rcpt) : (),
defined $rid ? (rid => $rid) : (),
defined $o_rid ? (rid_orig => Amavis::JSON::numeric($o_rid)) : (),
rcpt_is_local => Amavis::JSON::boolean($r->recip_is_local),
defined $user_policy_id ? (sql_user_policy_id => $user_policy_id) : (),
action => $d, # i.e. destiny
- defined $resp ? (smtp_response => $resp) : (),
+ defined $resp ? (smtp_response => $resp) : (),
defined $resp_code ? (smtp_code => $resp_code) : (),
# defined $resp_code_enh ? (smtp_code_enh => $resp_code_enh) : (),
defined $queued_as ? (queued_as => $queued_as) : (),
@@ -14808,31 +15696,15 @@ sub structured_report($;$) {
: (penpals_age => Amavis::JSON::numeric(int($penpals_age))),
# recip_tagged # was tagged by address extension or Subject or X-Spam
);
- for my $key (keys %recip) {
- next if ref $recip{$key};
- next if $recip{$key} !~ /[\x{a0}-\x{ff}]/s; # not upper half codepoints
- # garbage-in/garbage-out, but at least ensure characters are valid
- $recip{$key} = safe_decode_latin1($recip{$key}); # assumes ISO-8859-1
- }
push(@recipients, \%recip);
}
- my $sender_smtp = $msginfo->sender_smtp;
- $sender_smtp =~ s/^<(.*)>\z/$1/s;
- $sender_smtp =~ s/(\@[^@]+)\z/lc $1/se; # lowercase a domain part
- $sender_smtp = '<>' if $sender_smtp eq ''; # looks more natural in the log
-
- my $rfc2822_from = $msginfo->rfc2822_from; # undef, scalar or listref
- my $rfc2822_sender = $msginfo->rfc2822_sender; # undef or scalar
- my $rfc2822_to = $msginfo->rfc2822_to; # undef, scalar or listref
- my $rfc2822_cc = $msginfo->rfc2822_cc; # undef, scalar or listref
-
my $q_type = $msginfo->quar_type;
# only keep the first quarantine type used (e.g. ignore archival quar.)
$q_type = $q_type->[0] if ref $q_type;
my $q_to = $msginfo->quarantined_to; # ref to a list of quar. locations
- if (!defined($q_to) || !@$q_to) { $q_to = undef }
+ if (!$q_to || !@$q_to) { undef $q_to }
else {
$q_to = $q_to->[0]; # keep only the first quarantine location
$q_to =~ s{^\Q$QUARANTINEDIR\E/}{}; # strip directory name
@@ -14852,33 +15724,25 @@ sub structured_report($;$) {
my $useragent = $msginfo->get_header_field_body('user-agent');
$useragent = $msginfo->get_header_field_body('x-mailer') if !$useragent;
$useragent =~ s/^\s*(.*?)\s*\z/$1/s if $useragent;
- my $m_id = $msginfo->get_header_field_body('message-id');
- $m_id = join(' ', parse_message_id($m_id))
- if defined $m_id && $m_id ne ''; # strip CFWS
- my $refs = $msginfo->references;
my $subj = $msginfo->get_header_field_body('subject');
my $from = $msginfo->get_header_field_body('from'); # raw full field
for ($subj,$from) { # character set decoding, unfolding
chomp; s/\n(?=[ \t])//gs; s/^[ \t]+//s; s/[ \t]+\z//s; # unfold, trim
- eval { # convert to UTF-8 octets
- $_ = safe_decode('MIME-Header',$_); 1; # to characters
- } or do {
- my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
- do_log(1,"structured_report INFO: header field ".
- "not decodable, assuming Latin1: %s", $eval_stat);
- };
+ $_ = safe_decode_mime($_); # to logical characters
}
- my($conn,$src_ip,$dst_ip,$dst_port);
+ my($conn, $src_ip, $dst_ip, $dst_port, $appl_proto);
$conn = $msginfo->conn_obj;
if ($conn) { # MTA -> amavisd
$src_ip = $conn->client_ip; # immediate client IP addr, i.e. our MTA
$dst_ip = $conn->socket_ip; # IP address of our receiving socket
$dst_port = $conn->socket_port; # port number of our receiving socket
+ $appl_proto = $conn->appl_proto; # protocol - the 'WITH' field
}
my $client_addr = $msginfo->client_addr; # SMTP client -> MTA
my $client_port = $msginfo->client_port; # SMTP client -> MTA
- my $ip_trace_public = $msginfo->ip_addr_trace_public; # "Received" trace
+ my $trace_ref = $msginfo->trace; # "Received" trace entries (hashrefs)
+ my $ip_trace_public = $msginfo->ip_addr_trace_public; # "Received" IP trace
my $checks_performed = $msginfo->checks_performed;
$checks_performed = join(' ', grep($checks_performed->{$_},
qw(V S H B F P D))) if $checks_performed;
@@ -14899,11 +15763,11 @@ sub structured_report($;$) {
my $dkim_author_sig = $msginfo->dkim_author_sig;
my $dkim_sigs_new_ref = $msginfo->dkim_signatures_new;
my $dkim_sigs_ref = $msginfo->dkim_signatures_valid;
- my(@dkim_sigs_valid, @dkim_sigs_new);
- @dkim_sigs_valid =
- unique_list(map($_->domain, @$dkim_sigs_ref)) if $dkim_sigs_ref;
- @dkim_sigs_new =
- unique_list(map($_->domain, @$dkim_sigs_new_ref)) if $dkim_sigs_new_ref;
+ my(@dkim_sigs_valid, @dkim_sigs_new); # domain names, IDN-decoded
+ @dkim_sigs_valid = unique_list(map(idn_to_utf8($_->domain),
+ @$dkim_sigs_ref)) if $dkim_sigs_ref;
+ @dkim_sigs_new = unique_list(map(idn_to_utf8($_->domain),
+ @$dkim_sigs_new_ref)) if $dkim_sigs_new_ref;
my $vn = $msginfo->virusnames;
undef $vn if $vn && !@$vn;
@@ -14914,7 +15778,7 @@ sub structured_report($;$) {
my $scanner = $av && $av->[0];
if ($status && defined $scanner) {
$scanner =~ tr/"/'/; # sanitize scanner name for json
- $scanner =~ tr/\x00-\x1f\x7f\\/ /;
+ $scanner =~ tr/\x00-\x1F\x7F\x80-\x9F\\/ /;
$scanners_report{$scanner} = \@virus_names;
}
}
@@ -14938,7 +15802,7 @@ sub structured_report($;$) {
my(%result) = (
type => 'amavis',
- host => c('myhostname'),
+ host => safe_decode_utf8(idn_to_utf8(c('myhostname'))),
log_id => $msginfo->log_id,
# secret_id => $msginfo->secret_id,
mail_id => $msginfo->mail_id,
@@ -14953,24 +15817,35 @@ sub structured_report($;$) {
defined $partition_tag ? (partition => $partition_tag) : (),
defined $queue_id && $queue_id ne '' ? (queue_id => $queue_id) : (),
defined $sid ? (sid => $sid) : (),
+ defined $appl_proto ? (protocol => $appl_proto) : (),
+
+ # addresses from SMTP envelope:
mail_from => $sender_smtp,
- !defined $rfc2822_sender ? ()
- : (sender => $rfc2822_sender),
- !defined $rfc2822_from ? ()
- : (author => [ ref $rfc2822_from ? @$rfc2822_from : $rfc2822_from ]),
- !defined $rfc2822_to ? ()
- : (to_addr => [ ref $rfc2822_to ? @$rfc2822_to : $rfc2822_to ]),
- !defined $rfc2822_cc ? ()
- : (cc_addr => [ ref $rfc2822_cc ? @$rfc2822_cc : $rfc2822_cc ]),
+ rcpt_to => \@rcpt_smtp, # list of recipient addresses
+ rcpt_num => Amavis::JSON::numeric(scalar @rcpt_smtp), # num. of recips
+ recipients => \@recipients, # list of hashes
+
+ # addresses from mail header:
+ !defined $h_sender ? () : (sender => $h_sender),
+ $h_from ? (author => \@arr_h_from) : (),
+ $h_to ? (to_addr => \@arr_h_to) : (),
+ $h_cc ? (cc_addr => \@arr_h_cc) : (),
# defined $from ? (from_raw => $from) : (),
defined $subj ? (subject => $subj) : (),
defined $subj ? (subject_rot13 => rot13($subj)) : (),
- defined $m_id ? (message_id => $m_id) : (),
- $refs && @$refs ? (references => [ @$refs ]) : (),
+
+ defined $m_id ? (message_id => join(' ', at arr_m_id)) : (),
+ @arr_refs ? (references => \@arr_refs) : (),
+
defined $useragent ? (user_agent => $useragent) : (),
!defined $policy_bank_path ? ()
: (policy_banks => [ split(m{/}, $policy_bank_path) ]),
- ref $ip_trace_public ? (ip_trace => [ @$ip_trace_public ]) : (),
+ $ip_trace_public ? (ip_trace => [ @$ip_trace_public ]) : (),
+ !$trace_ref || !@$trace_ref ? ()
+ : (ip_proto_trace => [ map( (!$_->{with} ? '' : $_->{with}.'://') .
+ (!$_->{ip} ? 'x' : !$_->{port} ? $_->{ip}
+ : '['.$_->{ip}.']:'.$_->{port}),
+ @$trace_ref) ]),
!$msginfo->msg_size ? ()
: (size => Amavis::JSON::numeric(0+$msginfo->msg_size)),
!$msginfo->body_digest ? ()
@@ -15003,16 +15878,11 @@ sub structured_report($;$) {
!@smtp_status_code_list ? () : (smtp_code => \@smtp_status_code_list),
!@queued_as_list ? () : (queued_as => \@queued_as_list),
action => \@destiny_list,
- rcpt_num => Amavis::JSON::numeric(scalar @rcpt_to_list), # num. of recips
- rcpt_to => \@rcpt_to_list, # list of recipient addresses
- recipients => \@recipients, # list of hashes
message => # a brief report
sprintf("%s %s %s %s -> %s",
$msginfo->log_id, join(',', @destiny_list),
$msginfo->setting_by_contents_category(\%ccat_display_names),
- $msginfo->sender_smtp,
- join(',', map($_->recip_addr_smtp,
- @{$msginfo->per_recip_data}))),
+ $sender_smtp, join(',', @rcpt_smtp)),
time_unix => # UNIX time to millisecond precision
Amavis::JSON::numeric(sprintf("%.3f", $rx_time)),
# time_mjd => # Modified Julian Day to millisecond precision
@@ -15024,14 +15894,6 @@ sub structured_report($;$) {
iso8601_weekday($rx_time)), # 1..7, Mo=1, localtime
!%elapsed ? () : (elapsed => \%elapsed),
);
- for my $key (keys %result) {
- next if ref $result{$key};
- # already decoded characters in the following fields
- next if $key eq 'subject' || $key eq 'subject_rot13' || $key eq 'from';
- next if $result{$key} !~ /[\x{a0}-\x{ff}]/s; # not upper half codepoints
- # garbage-in/garbage-out, but at least ensure characters are valid
- $result{$key} = safe_decode_latin1($result{$key}); # assumes ISO-8859-1
- }
if (%elapsed) {
# last-minute update of total elapsed time, cast to numeric
my $el = $result{elapsed};
@@ -15054,10 +15916,11 @@ sub structured_report_update_time($) {
$report_ref;
}
-sub build_and_save_structured_report($;$) {
+sub build_and_save_structured_report($$) {
my($msginfo, $notification_type) = @_;
if ($redis_storage &&
$redis_logging_queue_size_limit && c('redis_logging_key') ) {
+ do_log(5,'build_and_save_structured_report on %s', $notification_type);
eval { # protect the new code just in case
$redis_storage->save_structured_report(
structured_report($msginfo, $notification_type),
@@ -15106,11 +15969,11 @@ sub inspect_a_bounce_message($) {
# take a main message component, ignoring preamble/epilogue MIME parts
# and pseudo components such as a fabricated 'MAIL' (i.e. a copy of
# entire message for the benefit of some virus scanners)
- my $name = $e->name_declared;
- next if !defined($e->type_declared) && defined($name) &&
+ my($name, $type) = ($e->name_declared, $e->type_declared);
+ next if !defined $type && defined $name &&
($name eq 'preamble' || $name eq 'epilogue');
- next if $e->type_short eq 'MAIL' &&
- lc($e->type_declared) eq 'message/rfc822';
+ next if $e->type_short eq 'MAIL' && defined $type &&
+ $type =~ m{^message/(?:rfc822|global)\z}si;
$top_main = $e; last;
}
my(@parts); my $fname_ind; my $plaintext = 0;
@@ -15128,17 +15991,18 @@ sub inspect_a_bounce_message($) {
$p0_report_type = lc $p0_report_type if defined $p0_report_type;
if ( @parts >= 2 && @parts <= 4 &&
- $t[0] eq 'multipart/report' &&
+ $t[0] eq 'multipart/report' && # RFC 6522
( $t[2] eq 'message/delivery-status' || # RFC 3464
- $t[2] eq 'message/global-delivery-status' || # RFC 5337
+ $t[2] eq 'message/global-delivery-status' || # RFC 6533
$t[2] eq 'message/disposition-notification' || # RFC 3798
- $t[2] eq 'message/global-disposition-notification' || # RFC 5337
+ $t[2] eq 'message/global-disposition-notification' || # RFC 6533
$t[2] eq 'message/feedback-report' # RFC 5965
) &&
defined $p0_report_type && $t[2] eq 'message/'.$p0_report_type &&
- ( $t[3] eq 'text/rfc822-headers' || $t[3] eq 'message/rfc822' ||
- $t[3] eq 'message/rfc822-headers' || # nonstandard
- $t[3] eq 'message/partial' ) # nonstandard
+ $t[3] =~ m{^ (?: text/rfc822-headers | # RFC 6522
+ message/(?: rfc822-headers | global-headers |
+ rfc822 | global | partial )) \z}xs
+ # message/rfc822-headers and message/partial are nonstandard
)
{ # standard DSN or MDN or feedback-report
$bounce_type = $t[2] eq 'message/disposition-notification' ? 'MDN'
@@ -15151,7 +16015,8 @@ sub inspect_a_bounce_message($) {
$t[0] eq 'multipart/report' &&
$t[-2] eq 'message/delivery-status' &&
defined $p0_report_type && $t[-2] eq 'message/'.$p0_report_type &&
- ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' )
+ $t[-1] =~ m{^ (?: text/rfc822-headers |
+ message/(?: global-headers|rfc822|global )) \z}xs
) { # almost standard DSN, has two leading plain text parts
$bounce_type = 'DSN'; # BorderWare Security Platform
$structure_type = 'standard ' . $bounce_type;
@@ -15169,15 +16034,17 @@ sub inspect_a_bounce_message($) {
} elsif (@parts >= 3 && @parts <= 4 && # a root with 2 or 3 leaves
$t[0] eq 'multipart/report' &&
defined $p0_report_type && $p0_report_type eq 'delivery-status' &&
- ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' )) {
- # not quite std. DSN (missing message/delivery-status), but recognizable
+ $t[-1] =~ m{^ (?: text/rfc822-headers |
+ message/(?: global-headers|rfc822|global )) \z}xs)
+ { # not quite std. DSN (missing message/delivery-status), but recognizable
$fname_ind = -1; $is_true_bounce = 1; $bounce_type = 'DSN';
$structure_type = 'DSN, missing delivery-status part';
} elsif (@parts >= 3 && @parts <= 5 &&
$t[0] eq 'multipart/mixed' &&
- ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' ||
- $t[-1] eq 'message/rfc822-headers') && # nonstandard - Gordano M.S.
+ $t[-1] =~ m{^ (?: text/rfc822-headers |
+ message/(?: global-headers|rfc822|global|
+ rfc822-headers )) \z}xs &&
( $rfc2822_from[0] =~ /^MAILER-DAEMON(?:\@|\z)/si ||
$msginfo->get_header_field_body('subject') =~
/\b(?:Delivery Failure Notification|failure notice)\b/
@@ -15191,8 +16058,9 @@ sub inspect_a_bounce_message($) {
$rfc2822_from[0] =~ /^notify\@yahoo/si &&
@parts >= 3 && @parts <= 5 &&
$t[0] eq 'multipart/mixed' &&
- ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' )
- ) {
+ $t[-1] =~ m{^ (?: text/rfc822-headers |
+ message/(?: global-headers|rfc822|global ))
+ \z}xs ) {
$fname_ind = -1;
$structure_type = 'multipart/mixed(yahoogroups)';
@@ -15382,12 +16250,13 @@ sub add_forwarding_header_edits_common($$$$$$) {
# a header if the [SMTP] connection relaying the message is not from a
# trusted internal MTA.
my $authservid = c('myauthservid');
- $authservid = c('myhostname') if !defined $authservid || $authservid eq '';
+ $authservid = c('myhostname') if !defined $authservid || $authservid eq '';
+ $authservid = idn_to_ascii($authservid);
# delete header field if its authserv-id matches ours or is unparseable
$hdr_edits->edit_header('Authentication-Results',
sub { my($h,$b) = @_;
my $aid = parse_authentication_results($b);
- if (defined $aid) { $aid =~ s{/.*}{}; $authservid =~ s{/.*}{} };
+ if (defined $aid) { $aid =~ s{/.*}{}s; $authservid =~ s{/.*}{}s };
!defined $aid || lc($aid) eq lc($authservid) ? (undef,0) : ($b,1);
} );
# [...] Border MTA MAY elect simply to remove all instances of this
@@ -15407,14 +16276,17 @@ sub add_forwarding_header_edits_common($$$$$$) {
defined $am_hdr_fld_body && $am_hdr_fld_body ne '' &&
defined $am_hdr_fld_head && $am_hdr_fld_head =~ /^[!-9;-\176]+\z/;
}
+ my $myhost = c('myhostname');
+ $myhost = $msginfo->smtputf8 ? idn_to_utf8($myhost) : idn_to_ascii($myhost);
for ('X-Spam-Checker-Version') {
if ($extra_code_antispam_sa &&
$allowed_hdrs && $allowed_hdrs->{lc $_} &&
$use_our_hdrs && $use_our_hdrs->{lc $_}) {
no warnings 'once';
$hdr_edits->add_header($_,
- sprintf("SpamAssassin %s (%s) on %s", Mail::SpamAssassin::Version(),
- $Mail::SpamAssassin::SUB_VERSION, c('myhostname')));
+ sprintf("SpamAssassin %s (%s) on %s",
+ Mail::SpamAssassin::Version(),
+ $Mail::SpamAssassin::SUB_VERSION, $myhost));
}
}
$hdr_edits;
@@ -15494,6 +16366,8 @@ sub add_forwarding_header_edits_per_recip($$$$$$$) {
last if defined $subject_tag && $subject_tag ne '';
}
}
+ my $myhost = c('myhostname');
+ $myhost = $msginfo->smtputf8 ? idn_to_utf8($myhost) :idn_to_ascii($myhost);
$subject_tag = '' if !defined $subject_tag;
if ($subject_tag ne '') { # expand subject template
# just implement a small subset of macro-lookalikes, not true macro calls
@@ -15504,12 +16378,12 @@ sub add_forwarding_header_edits_per_recip($$$$$$$) {
0+sprintf("%.3f",$tag2_level))
: $1 eq 'YESNO' ? ($do_tag2 ? 'Yes' : 'No')
: $1 eq 'YESNOCAPS' ? ($do_tag2 ? 'YES' : 'NO')
- : $1 eq 'HOSTNAME' ? c('myhostname')
+ : $1 eq 'HOSTNAME' ? $myhost #** characters or octets?
: $1 eq 'DATE' ? rfc2822_timestamp($msginfo->rx_time)
: $1 eq 'U' ? iso8601_utc_timestamp($msginfo->rx_time)
: $1 eq 'LOGID' ? $msginfo->log_id
: $1 eq 'MAILID' ? $mail_id||''
- : '_'.$1.'_' }egsx;
+ : '_'.$1.'_' }xgse;
}
# normalize
@@ -15585,7 +16459,7 @@ sub add_forwarding_header_edits_per_recip($$$$$$$) {
$hdr_edits->add_header('X-Amavis-Modified',
sprintf("Mail body modified (%s) - %s",
length($mail_mangle) > 1 ? "using $mail_mangle" : "defanged",
- c('myhostname') ));
+ $myhost ));
}
}
if ($do_tag_virus_checked) {
@@ -15729,9 +16603,13 @@ sub add_forwarding_header_edits_per_recip($$$$$$$) {
$hdr_edits->edit_header('Subject',
sub { local($1,$2);
$_[1] =~ /^([ \t]?)(.*)\z/s; my $subj = $2;
- if (length($subject_tag) >= 3) # precaution
- { $subj =~ s/\Q$subject_tag\E//sg }
- ' ' . $subject_tag . $subj });
+ $subj = safe_decode_mime($subj); # to characters
+ $subj =~ s/\Q$subject_tag\E//sg
+ if length($subject_tag) >= 3; # precaution
+ safe_decode_utf8(
+ ' ' . safe_encode_utf8($subject_tag) .
+ safe_encode_utf8($subj));
+ } );
} else { # no Subject header field present, insert one
$subject_tag =~ s/[ \t]+\z//; # trim
$hdr_edits->add_header('Subject', $subject_tag);
@@ -15756,7 +16634,7 @@ sub add_forwarding_header_edits_per_recip($$$$$$$) {
my $rewrite = !ref $rewrite_map ? undef : lookup2(0,$recip,$rewrite_map);
if ($rewrite ne '') {
my(@replacements) = grep($_ ne '',
- map { /^ [ \t]* (.*?) [ \t]* \z/sx; $1 } split(/,/, $rewrite, -1));
+ map { /^ [ \t]* (.*?) [ \t]* \z/xs; $1 } split(/,/, $rewrite, -1));
if (@replacements) {
my $repl_addr = shift @replacements;
my $modif_addr = replace_addr_fields($recip,$repl_addr,$delim);
@@ -15771,8 +16649,8 @@ sub add_forwarding_header_edits_per_recip($$$$$$$) {
# $clone->recip_addr_modified($new_addr);
}
}
- $r->dsn_orcpt(orcpt_encode($r->recip_addr_smtp))
- if !defined($r->dsn_orcpt);
+ $r->dsn_orcpt(join(';', orcpt_decode(';'.$r->recip_addr_smtp)))
+ if !defined $r->dsn_orcpt;
}
}
if ($is_local && defined $delim && $delim ne '') {
@@ -15799,8 +16677,8 @@ sub add_forwarding_header_edits_per_recip($$$$$$$) {
# RCPT command when the message is relayed. If an ORCPT parameter is
# added by the relaying MTA, it MUST contain the recipient address
# from the RCPT command used when the message was received by that MTA.
- $r->dsn_orcpt(orcpt_encode($r->recip_addr_smtp))
- if !defined($r->dsn_orcpt);
+ $r->dsn_orcpt(join(';', orcpt_decode(';'.$r->recip_addr_smtp)))
+ if !defined $r->dsn_orcpt;
$r->recip_addr_modified($new_addr);
$r->recip_tagged(1);
}
@@ -16105,6 +16983,7 @@ sub do_quarantine($$$$;@) {
$quar_msg->body_digest($msginfo->body_digest); # copy original digest
$quar_msg->dsn_ret($msginfo->dsn_ret);
$quar_msg->dsn_envid($msginfo->dsn_envid);
+ $quar_msg->smtputf8($msginfo->smtputf8);
$quar_msg->auth_submitter($msginfo->sender_smtp);
$quar_msg->auth_user(c('amavis_auth_user'));
$quar_msg->auth_pass(c('amavis_auth_pass'));
@@ -16463,9 +17342,11 @@ sub prepare_header_edits_for_quarantine($) {
sub do_notify_and_quarantine($$) {
my($msginfo, $virus_dejavu) = @_;
my($mailfrom_admin, $hdrfrom_admin, $notify_admin_templ_ref) =
- map { scalar($msginfo->setting_by_contents_category(cr($_))) }
+ map(scalar $msginfo->setting_by_contents_category(cr($_)),
qw(mailfrom_notify_admin_by_ccat hdrfrom_notify_admin_by_ccat
- notify_admin_templ_by_ccat);
+ notify_admin_templ_by_ccat));
+ $mailfrom_admin = safe_encode_utf8($mailfrom_admin); # make sure it's octets
+ $hdrfrom_admin = safe_encode_utf8($hdrfrom_admin); # make sure it's octets
my $qar_method = c('archive_quarantine_method');
my(@ccat_names_pairs) =
$msginfo->setting_by_main_contents_category_all(\%ccat_display_names);
@@ -16496,7 +17377,7 @@ sub do_notify_and_quarantine($$) {
last if $archive_any && !$archive_transparent;
}
}
- my(@q_tuples, at a_addr); # per-recip quarantine address(es) and admins
+ my(@q_tuples, @a_addr); # per-recip quarantine address(es) and admins
for my $r (@{$msginfo->per_recip_data}) {
my $rec = $r->recip_addr;
my $blacklisted = $r->recip_blacklisted_sender;
@@ -16666,13 +17547,11 @@ sub do_notify_and_quarantine($$) {
join(',',qquote_rfc2821_local(@a_addr)),
$msginfo->sender_smtp);
$hdrfrom_admin = expand_variables($hdrfrom_admin);
- my $mailfrom_admin_q;
- if (!defined($mailfrom_admin)) {
+ if (!defined $mailfrom_admin) {
# defaults to email address in hdrfrom_notify_admin
- $mailfrom_admin_q = (parse_address_list($hdrfrom_admin))[0];
- $mailfrom_admin = unquote_rfc2821_local($mailfrom_admin_q);
+ $mailfrom_admin =
+ unquote_rfc2821_local( (parse_address_list($hdrfrom_admin))[0] );
}
- $mailfrom_admin_q = qquote_rfc2821_local($mailfrom_admin);
my $notification = Amavis::In::Message->new;
$notification->rx_time($msginfo->rx_time); # copy the reception time
$notification->log_id($msginfo->log_id); # copy log id
@@ -16682,26 +17561,36 @@ sub do_notify_and_quarantine($$) {
$notification->conn_obj($msginfo->conn_obj);
$notification->originating(1);
$notification->add_contents_category(CC_CLEAN,0);
+ $_ = safe_encode_utf8($_) for @a_addr; # make sure addresses are in octets
+ if (grep( / [^\x00-\x7F] .*? \@ [^@]* \z/sx && is_valid_utf_8($_),
+ ($mailfrom_admin, @a_addr) )) {
+ # localpart is non-ASCII UTF-8, we must use SMTPUTF8
+ $notification->smtputf8(1);
+ do_log(2, 'admin notification requires SMTPUTF8');
+ } else {
+ $_ = mail_addr_idn_to_ascii($_) for ($mailfrom_admin, @a_addr);
+ }
$notification->sender($mailfrom_admin);
- $notification->sender_smtp($mailfrom_admin_q);
- $notification->auth_submitter($mailfrom_admin_q);
+ $notification->sender_smtp(qquote_rfc2821_local($mailfrom_admin));
+ $notification->auth_submitter($notification->sender_smtp);
$notification->auth_user(c('amavis_auth_user'));
$notification->auth_pass(c('amavis_auth_pass'));
$notification->recips([@a_addr]);
my $notif_m = c('notify_method');
$_->delivery_method($notif_m) for @{$notification->per_recip_data};
- my(@rfc2822_from_admin) = map(unquote_rfc2821_local($_),
- parse_address_list($hdrfrom_admin));
+ my(@rfc2822_from_admin) =
+ map(unquote_rfc2821_local($_), parse_address_list($hdrfrom_admin));
$notification->rfc2822_from($rfc2822_from_admin[0]);
# if ($mailfrom_admin ne '')
# { $_->dsn_notify(['NEVER']) for @{$notification->per_recip_data} }
my(%mybuiltins) = %builtins; # make a local copy
- $mybuiltins{'T'} = [qquote_rfc2821_local(@a_addr)]; # used in To:
- $mybuiltins{'f'} = $hdrfrom_admin; # From:
+ $mybuiltins{'f'} = safe_decode_utf8($hdrfrom_admin); # From:
+ $mybuiltins{'T'} = # To:
+ [ map(mail_addr_idn_to_ascii(qquote_rfc2821_local($_)), @a_addr) ];
$notification->mail_text(
build_mime_entity(expand($notify_admin_templ_ref,\%mybuiltins),
$msginfo, undef,undef,0, 1,0) );
-# $notification->body_type('7BIT');
+# $notification->body_type('7BIT'); # '8BITMIME'
my $hdr_edits = Amavis::Out::EditHeader->new;
$notification->header_edits($hdr_edits);
mail_dispatch($notification, 'Notif', 0);
@@ -16754,14 +17643,14 @@ sub do_notify_and_quarantine($$) {
$r->setting_by_contents_category(cr('mailfrom_notify_recip_by_ccat'));
my $hdrfrom_recip =
$r->setting_by_contents_category(cr('hdrfrom_notify_recip_by_ccat'));
- $hdrfrom_recip = expand_variables($hdrfrom_recip);
- my $mailfrom_recip_q;
- if (!defined($mailfrom_recip)) {
+ # make sure it's in octets
+ $mailfrom_recip = safe_encode_utf8($mailfrom_recip);
+ $hdrfrom_recip = expand_variables(safe_encode_utf8($hdrfrom_recip));
+ if (!defined $mailfrom_recip) {
# defaults to email address in hdrfrom_notify_recip
- $mailfrom_recip_q = (parse_address_list($hdrfrom_recip))[0];
- $mailfrom_recip = unquote_rfc2821_local($mailfrom_recip_q);
+ $mailfrom_recip =
+ unquote_rfc2821_local( (parse_address_list($hdrfrom_recip))[0] );
}
- $mailfrom_recip_q = qquote_rfc2821_local($mailfrom_recip);
my $notification = Amavis::In::Message->new;
$notification->rx_time($msginfo->rx_time); # copy the reception time
$notification->log_id($msginfo->log_id); # copy log id
@@ -16771,16 +17660,24 @@ sub do_notify_and_quarantine($$) {
$notification->conn_obj($msginfo->conn_obj);
$notification->originating(1);
$notification->add_contents_category(CC_CLEAN,0);
+ if (grep( / [^\x00-\x7F] .*? \@ [^@]* \z/sx && is_valid_utf_8($_),
+ ($mailfrom_recip, $rec) )) {
+ # localpart is non-ASCII UTF-8, we must use SMTPUTF8
+ do_log(2, 'recipient notification requires SMTPUTF8');
+ $notification->smtputf8(1);
+ } else {
+ $_ = mail_addr_idn_to_ascii($_) for ($mailfrom_recip, $rec);
+ }
$notification->sender($mailfrom_recip);
- $notification->sender_smtp($mailfrom_recip_q);
- $notification->auth_submitter($mailfrom_recip_q);
+ $notification->sender_smtp(qquote_rfc2821_local($mailfrom_recip));
+ $notification->auth_submitter($notification->sender_smtp);
$notification->auth_user(c('amavis_auth_user'));
$notification->auth_pass(c('amavis_auth_pass'));
$notification->recips([$rec]);
my $notif_m = c('notify_method');
$_->delivery_method($notif_m) for @{$notification->per_recip_data};
- my(@rfc2822_from_recip) = map(unquote_rfc2821_local($_),
- parse_address_list($hdrfrom_recip));
+ my(@rfc2822_from_recip) =
+ map(unquote_rfc2821_local($_), parse_address_list($hdrfrom_recip));
$notification->rfc2822_from($rfc2822_from_recip[0]);
# if ($mailfrom_recip ne '')
# { $_->dsn_notify(['NEVER']) for @{$notification->per_recip_data} }
@@ -16797,12 +17694,12 @@ sub do_notify_and_quarantine($$) {
$mybuiltins{'banning_rule_rhs'} =
!defined($r->banning_rule_rhs) ? undef
: unique_ref($r->banning_rule_rhs);
- $mybuiltins{'f'} = $hdrfrom_recip; # From:
- $mybuiltins{'T'} = qquote_rfc2821_local($rec); # To:
+ $mybuiltins{'f'} = safe_decode_utf8($hdrfrom_recip); # From:
+ $mybuiltins{'T'} = mail_addr_idn_to_ascii(qquote_rfc2821_local($rec));
$notification->mail_text(
build_mime_entity(expand($notify_recips_templ_ref,\%mybuiltins),
$msginfo, undef,undef,0, 0,0) );
-# $notification->body_type('7BIT');
+# $notification->body_type('7BIT'); # '8BITMIME'
my $hdr_edits = Amavis::Out::EditHeader->new;
$notification->header_edits($hdr_edits);
mail_dispatch($notification, 'Notif', 0);
@@ -16840,8 +17737,11 @@ sub get_body_digest($$) {
if (!defined $dns_resolver && Mail::DKIM::Verifier->VERSION >= 0.40) {
# Create a persistent DNS resolver object for the benefit
# of Mail::DKIM::Verifier; this avoids repeating initializations
- # with each request, and allows us to turn on EDNS
+ # with each request, and allows us to turn on EDNS.
+ # The controversial need for 'config_file' option was debated in
+ # [rt.cpan.org #96608] https://rt.cpan.org/Ticket/Display.html?id=96608
$dns_resolver = Net::DNS::Resolver->new(
+ config_file => '/etc/resolv.conf',
force_v4 => !$have_inet6,
defnames => 0,
retry => 2, # number of times to try the query (not REtries)
@@ -16862,7 +17762,7 @@ sub get_body_digest($$) {
ll(5) && do_log(5, "DNS resolver created, UDP payload size %s, NS: %s",
$dns_resolver->udppacketsize,
join(', ',$dns_resolver->nameservers) );
- Mail::DKIM::DNS::resolver($dns_resolver);
+ Mail::DKIM::DNS::resolver($dns_resolver);
}
}
$dkim_verifier = Mail::DKIM::Verifier->new;
@@ -16896,7 +17796,7 @@ sub get_body_digest($$) {
my $ind = index($$msg, "\n\n", $pos); # find header/body separator
$header = $ind < 0 ? substr($$msg, $pos)
: substr($$msg, $pos, $ind+1-$pos);
- $h_8bit = 1 if $header =~ tr/\000-\177//c;
+ $h_8bit = 1 if $header =~ tr/\x00-\x7F//c;
$hctx->add($header);
$pos = $ind < 0 ? length($$msg) : $ind+2;
}
@@ -16929,6 +17829,7 @@ sub get_body_digest($$) {
$header =~ s{\n}{\015\012}gs; # needed for DKIM and for size
$header_size = length($header); # size includes CRLF (RFC 1870)
if (defined $dkim_verifier) {
+ do_log(5, "get_body_digest: feeding header section to DKIM verifier");
eval {
$dkim_verifier->PRINT($header)
or die "Error writing mail header to DKIM: $!";
@@ -16954,7 +17855,7 @@ sub get_body_digest($$) {
$pos += length($ln);
last if $ln eq "\n";
$hctx->add($ln);
- $h_8bit = 1 if !$h_8bit && $ln =~ tr/\000-\177//c;
+ $h_8bit = 1 if !$h_8bit && ($ln =~ tr/\x00-\x7F//c);
if ($ln =~ /^[ \t]/) { # header field continuation
$$orig_header[-1] .= $ln; # including NL
} else { # starts a new header field
@@ -17018,6 +17919,7 @@ sub get_body_digest($$) {
undef $dkim_verifier;
};
}
+
$header_size += 2; # include a separator CRLF line in a header section size
untaint_inplace($header_size); # length(tainted) stays tainted too
section_time('digest_hdr');
@@ -17039,7 +17941,8 @@ sub get_body_digest($$) {
# empty mail
} elsif (ref $msg eq 'SCALAR') {
- do_log(5, "get_body_digest: reading mail body from memory");
+ ll(5) && do_log(5, "get_body_digest: reading mail body from memory, ".
+ "%d DKIM signatures", scalar @dkim_signatures);
my($buff, $buff_l);
while ($pos < length($$msg)) {
# do it in chunks to avoid unnecessarily large memory use
@@ -17047,7 +17950,7 @@ sub get_body_digest($$) {
$buff = substr($$msg,$pos,32768); $buff_l = length($buff);
$pos += $buff_l;
$bctx->add($buff);
- $b_8bit = 1 if !$b_8bit && ($buff =~ tr/\000-\177//c);
+ $b_8bit = 1 if !$b_8bit && ($buff =~ tr/\x00-\x7F//c);
if (!$feed_dkim) {
# count \n, compensating for CRLF (RFC 1870)
$body_size += $buff_l + ($buff =~ tr/\n//);
@@ -17071,11 +17974,12 @@ sub get_body_digest($$) {
} else {
#*** # only read further if not already at end-of-file
- do_log(5, "get_body_digest: reading mail body from a file");
+ ll(5) && do_log(5, "get_body_digest: reading mail body from a file, ".
+ "%d DKIM signatures", scalar @dkim_signatures);
my($buff, $buff_l);
while (($buff_l = $msg->read($buff,65536)) > 0) {
$bctx->add($buff);
- $b_8bit = 1 if !$b_8bit && ($buff =~ tr/\000-\177//c);
+ $b_8bit = 1 if !$b_8bit && ($buff =~ tr/\x00-\x7F//c);
if (!$feed_dkim) {
# count \n, compensating for CRLF (RFC 1870)
$body_size += $buff_l + ($buff =~ tr/\n//);
@@ -17147,27 +18051,28 @@ sub get_body_digest($$) {
$msginfo->body_digest($body_digest_hex);
$msginfo->header_8bit($h_8bit ? 1 : 0);
$msginfo->body_8bit($b_8bit ? 1 : 0);
- # check for 8-bit characters and adjust body type if necessary (RFC 1652)
+ # check for 8-bit characters and adjust body type if necessary (RFC 6152)
my $bt_orig = $msginfo->body_type;
- $bt_orig = !defined($bt_orig) ? '' : uc($bt_orig);
+ $bt_orig = defined $bt_orig ? uc $bt_orig : '';
if ($h_8bit || $b_8bit) {
# just keep original label whatever it is (garbage-in - garbage-out);
# keeping 8-bit mail unlabeled might avoid breaking DKIM in transport
# (labeling as 8-bit may invoke 8>7 downgrades in MTA, breaking signatures)
} elsif ($bt_orig eq '') { # unlabeled on reception
- $msginfo->body_type('7BIT'); # safe to label
+ $msginfo->body_type('7BIT'); # safe to label as all-ASCII
} elsif ($bt_orig eq '8BITMIME') { # redundant (quite common)
$msginfo->body_type('7BIT'); # turn a redundant 8BITMIME into 7BIT
}
if (ll(4)) {
- my $msg_fmt =
- ($bt_orig eq '' && $b_8bit) ? "%s, but 8-bit body"
- : ($bt_orig eq '' && $h_8bit) ? "%s, but 8-bit header"
- : ($bt_orig eq '7BIT' && ($h_8bit || $b_8bit)) ? "%s inappropriately"
- : ($bt_orig eq '8BITMIME' && !($h_8bit || $b_8bit)) ? "%s unnecessarily"
- : "%s, good";
- do_log(4, "body type (ESMTP BODY): $msg_fmt (h=%s, b=%s)",
- $bt_orig eq '' ? 'unlabeled' : "labeled $bt_orig", $h_8bit,$b_8bit);
+ my $remark =
+ ($bt_orig eq '' && $b_8bit) ? ", but 8-bit body"
+ : ($bt_orig eq '' && $h_8bit) ? ", but 8-bit header"
+ : ($bt_orig eq '7BIT' && ($h_8bit || $b_8bit)) ? " inappropriately"
+ : ($bt_orig eq '8BITMIME' && !($h_8bit || $b_8bit)) ? " unnecessarily"
+ : ", good";
+ do_log(4, "body type (8bit-MIMEtransport): %s%s (h=%s, b=%s)",
+ $bt_orig eq '' ? 'unlabeled' : "labeled $bt_orig",
+ $remark, $h_8bit, $b_8bit);
}
do_log(3, "body hash: %s", $body_digest_hex);
section_time(defined $dkim_verifier ? 'digest_body_dkim' : 'digest_body');
@@ -18021,8 +18926,8 @@ do_log(2, 'logging initialized, log level %s, %s%s', c('log_level'),
do_log(2, 'ZMQ enabled: %s', Amavis::ZMQ::zmq_version()) if $zmq_obj;
# insist on a FQDN in $myhostname
-my $myhn = c('myhostname');
-$myhn =~ /[^.]\.[a-zA-Z0-9-]+\z/s || lc($myhn) eq 'localhost'
+my $myhn = idn_to_utf8(c('myhostname'));
+$myhn =~ /[^.]\.[^.]+\.?\z/s || lc($myhn) eq 'localhost'
or die <<"EOD";
The value of variable \$myhostname is \"$myhn\", but should have been
a fully qualified domain name; perhaps uname(3) did not provide such.
@@ -18212,12 +19117,13 @@ $ENV{HOME} = $helpers_home if defined $helpers_home && $helpers_home ne '';
$ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100';
{ my $msg = '';
$msg .= ", instance=$instance_name" if $instance_name ne '';
- $msg .= ", nl=".sprintf("\\%03o",ord("\n")) if "\n" ne "\012";
- $msg .= ", Unicode aware" if $unicode_aware;
+ $msg .= ", nl=".sprintf('\\x%02X',ord("\n")) if "\n" ne "\012";
+ $msg .= ", Unicode aware"; # ensured by 'require 5.008'
for (qw(PERLIO LC_ALL LC_TYPE LC_CTYPE LANG))
{ $msg .= sprintf(', %s="%s"', $_,$ENV{$_}) if $ENV{$_} ne '' }
do_log(0,"starting.%s %s at %s %s%s",
- !$warm_restart?'':' (warm)', $0, c('myhostname'), $myversion, $msg);
+ !$warm_restart?'':' (warm)', $0,
+ idn_to_utf8(c('myhostname')), $myversion, $msg);
}
# report version of Perl and process UID/GID
do_log(1, "perl=%s, user=%s, EUID: %s (%s); group=%s, EGID: %s (%s)",
@@ -18389,10 +19295,9 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
- import Amavis::Conf qw(:platform $myversion $myhostname
- $nanny_details_level);
+ import Amavis::Conf qw(:platform $myversion $nanny_details_level);
import Amavis::Util qw(ll do_log do_log_safe
snmp_initial_oids snmp_counters_get);
}
@@ -18651,10 +19556,9 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
- import Amavis::Conf qw(:platform $myversion $myhostname
- $nanny_details_level);
+ import Amavis::Conf qw(:platform $myversion $nanny_details_level);
import Amavis::Util qw(ll do_log do_log_safe
snmp_initial_oids snmp_counters_get
add_entropy fetch_entropy_bytes);
@@ -18909,7 +19813,7 @@ use warnings FATAL => qw(utf8 void);
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw($db_home $daemon_chroot_dir);
import Amavis::Util qw(untaint ll do_log);
@@ -18998,7 +19902,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Util qw(ll do_log);
import Amavis::Conf qw($trim_trailing_space_in_lookup_result_fields);
@@ -19123,7 +20027,7 @@ use warnings FATAL => qw(utf8 void);
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Timing qw(section_time);
@@ -19174,6 +20078,12 @@ sub clear_cache {
# to a hash containing values of requested fields), otherwise returns undef.
# A match aborts further fetching sequence, unless $get_all is true.
#
+# The $addr may be a string of octets (assumed to be UTF-8 encoded)
+# or a string of characters which gets first encoded to UTF-8 octets.
+# International domain name (IDN) in $addr will be converted to ACE
+# and lowercased. International domain names in SQL are expected to be
+# encoded in ASCII-compatible encoding (ACE).
+#
# SQL lookups (e.g. for user+foo at example.com) are performed in order
# which can be requested by 'ORDER BY' in the SELECT statement, otherwise
# the order is unspecified, which is only useful if only specific entries
@@ -19240,7 +20150,7 @@ sub lookup_sql($$$%) {
@{ca('local_domains_maps')}));
my($keys_ref,$rhs_ref) = make_query_keys($addr,
$sql_lookups_no_at_means_domain,$is_local);
- if (!$sql_allow_8bit_address) { s/[^\040-\176]/?/g for @$keys_ref }
+ if (!$sql_allow_8bit_address) { s/[^\040-\176]/?/gs for @$keys_ref }
my $n = scalar(@$keys_ref); # number of keys
my(@extras_tmp, at pos_args); local($1);
@extras_tmp = @$extra_args if $extra_args;
@@ -19262,7 +20172,7 @@ sub lookup_sql($$$%) {
#*** (%L is experimental, incomplete)
: $1 eq '%L' ? [($is_local?'1':'0'), SQL_BOOLEAN] #is local
: shift @extras_tmp),
- $1 eq '%k' ? join(',', ('?') x $n) : '?' }gxe;
+ $1 eq '%k' ? join(',', ('?') x $n) : '?' }xgse;
$sel = untaint($sel) . $sel_taint; # keep original clause taintedness
ll(4) && do_log(4,"lookup_sql %s \"%s\", query args: %s",
$clause_name, $addr,
@@ -19344,7 +20254,7 @@ BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
$have_sasl $ldap_sys_default);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
$have_sasl = eval { require Authen::SASL };
import Amavis::Conf qw(:platform :confvars c cr ca);
@@ -19566,7 +20476,7 @@ use re 'taint';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Util qw(ll do_log);
import Amavis::Conf qw($trim_trailing_space_in_lookup_result_fields);
@@ -19691,12 +20601,12 @@ BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
$ldap_sys_default @ldap_attrs @mv_ldap_attrs);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::Util qw(untaint untaint_inplace snmp_count
- ll do_log do_log_safe);
+ ll do_log do_log_safe idn_to_ascii);
import Amavis::rfc2821_2822_Tools qw(make_query_keys split_address);
import Amavis::LDAP::Connection ();
@@ -19761,6 +20671,12 @@ sub clear_cache {
delete $self->{cache};
}
+# The $addr may be a string of octets (assumed to be UTF-8 encoded)
+# or a string of characters which gets first encoded to UTF-8 octets.
+# International domain name (IDN) in $addr will be converted to ACE
+# and lowercased. International domain names in LDAP are expected to be
+# encoded in ASCII-compatible encoding (ACE).
+#
sub lookup_ldap($$$%) {
my($self,$addr,$get_all,%options) = @_;
my(@result, at matchingkey, at tmp_result, at tmp_matchingkey);
@@ -19802,8 +20718,8 @@ sub lookup_ldap($$$%) {
# process %m
my $filter = $self->{query_filter};
my @filter_attr; my $expanded_filter = '';
- for my $t ($filter =~ /\G( \( [^(=]+ = %m \) | [ \t0-9A-Za-z]+ | . )/gsx) {
- if ($t !~ m{ \( ([^(=]+) = %m \) }sx) { $expanded_filter .= $t }
+ for my $t ($filter =~ /\G( \( [^(=]+ = %m \) | [ \t0-9A-Za-z]+ | . )/xgs) {
+ if ($t !~ m{ \( ([^(=]+) = %m \) }xs) { $expanded_filter .= $t }
else {
push(@filter_attr, $1);
$expanded_filter .= '(|' . join('', map("($1=$_)", @keys)) . ')';
@@ -19815,9 +20731,10 @@ sub lookup_ldap($$$%) {
if ($base =~ /%d/) {
my($localpart,$domain) = split_address($addr);
if ($domain) {
- untaint_inplace($domain); $domain = lc($domain); local($1);
+ untaint_inplace($domain); local($1);
$domain =~ s/^\@?(.*?)\.*\z/$1/s;
- $base =~ s/%d/&Net::LDAP::Util::escape_dn_value($domain)/ge;
+ $domain = idn_to_ascii($domain);
+ $base =~ s/%d/&Net::LDAP::Util::escape_dn_value($domain)/gse;
}
}
# build hash of keys and array position
@@ -19904,12 +20821,12 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Util qw(ll do_log debug_oneshot dump_captured_log
untaint snmp_counters_init read_file
- snmp_count proto_encode proto_decode orcpt_encode
+ snmp_count proto_encode proto_decode
switch_to_my_time switch_to_client_time
am_id new_am_id add_entropy rmdir_recursively
generate_mail_id);
@@ -20133,7 +21050,7 @@ sub preprocess_policy_query($$) {
my $recip_obj = Amavis::In::Message::PerRecip->new;
$recip_obj->recip_addr($addr_unq);
$recip_obj->recip_addr_smtp($addr_quo);
- $recip_obj->dsn_orcpt(orcpt_encode($addr_quo));
+ $recip_obj->dsn_orcpt($addr_quo);
$recip_obj->recip_destiny(D_PASS); # default is Pass
$recip_obj->delivery_method('') if !defined($d_co) ||
lc($d_co) eq 'client';
@@ -20162,18 +21079,20 @@ sub preprocess_policy_query($$) {
$msginfo->mail_tempdir(untaint($tempdir));
}
my $quar_type;
+ my $p_mail_id;
if (!$ampdp) {
# don't bother with filenames
} elsif ($attr_ref->{'request'} =~ /^(?:release|requeue|report)\z/i) {
exists $attr_ref->{'mail_id'} or die "Missing 'mail_id' field";
$msginfo->partition_tag($attr_ref->{'partition_tag'}); # may be undef
- my $mail_id = $attr_ref->{'mail_id'};
+ $p_mail_id = $attr_ref->{'mail_id'};
# amavisd almost-base64: 62 +, 63 - (in use up to 2.6.4, dropped in 2.7.0)
# RFC 4648 base64: 62 +, 63 / (not used here)
# RFC 4648 base64url: 62 -, 63 _
- $mail_id =~ m{^ [A-Za-z0-9] [A-Za-z0-9_+-]* ={0,2} \z}xs
- or die "Invalid mail_id '$mail_id'";
- $msginfo->parent_mail_id(untaint($mail_id));
+ $p_mail_id =~ m{^ [A-Za-z0-9] [A-Za-z0-9_+-]* ={0,2} \z}xs
+ or die "Invalid mail_id '$p_mail_id'";
+ $p_mail_id = untaint($p_mail_id);
+ $msginfo->parent_mail_id($p_mail_id);
$msginfo->mail_id(scalar generate_mail_id());
if (!exists($attr_ref->{'secret_id'}) || $attr_ref->{'secret_id'} eq '') {
die "Secret_id is required, but missing" if c('auth_required_release');
@@ -20189,15 +21108,15 @@ sub preprocess_policy_query($$) {
my $id_new_b64 = Digest::MD5->new->add($secret_bin)->b64digest;
substr($id_new_b64, 12) = '';
$id_new_b64 =~ tr{+/}{-_}; # base64 -> RFC 4648 base64url
- last if $id_new_b64 eq $mail_id; # exit enclosing block
+ last if $id_new_b64 eq $p_mail_id; # exit enclosing block
}
if (index($secret_b64,'_') < 0) { # old or undetermined format
my $id_old_b64 = Digest::MD5->new->add($secret_b64)->b64digest;
substr($id_old_b64, 12) = '';
$id_old_b64 =~ tr{/}{-}; # base64 -> almost-base64
- last if $id_old_b64 eq $mail_id; # exit enclosing block
+ last if $id_old_b64 eq $p_mail_id; # exit enclosing block
}
- die "Secret_id $secret_b64 does not match mail_id $mail_id";
+ die "Secret_id $secret_b64 does not match mail_id $p_mail_id";
}; # end block, 'last' arrives here
}
$quar_type = $attr_ref->{'quar_type'};
@@ -20205,7 +21124,7 @@ sub preprocess_policy_query($$) {
# choose some reasonable default (simpleminded)
$quar_type = c('spam_quarantine_method') =~ /^sql:/i ? 'Q' : 'F';
}
- my $fn = $mail_id;
+ my $fn = $p_mail_id;
if ($quar_type eq 'F' || $quar_type eq 'Z') {
$QUARANTINEDIR ne '' or die "Config variable \$QUARANTINEDIR is empty";
if ($attr_ref->{'mail_file'} ne '') {
@@ -20232,8 +21151,7 @@ sub preprocess_policy_query($$) {
my $releasing = $attr_ref->{'request'}=~ /^(?:release|requeue|report)\z/i;
new_am_id('rel-'.$msginfo->mail_id) if $releasing;
if ($releasing && $quar_type eq 'Q') { # releasing from SQL
- do_log(5, "preprocess_policy_query: opening in sql: %s",
- $msginfo->mail_id);
+ do_log(5, "preprocess_policy_query: opening in sql: %s", $p_mail_id);
my $obj = $Amavis::sql_storage;
$Amavis::extra_code_sql_quar && $obj
or die "SQL quarantine code not enabled (3)";
@@ -20243,31 +21161,31 @@ sub preprocess_policy_query($$) {
if (!defined($msginfo->partition_tag) &&
defined($sel_msg) && $sel_msg ne '') {
do_log(5, "preprocess_policy_query: missing partition_tag in request,".
- " fetching msgs record for mail_id=%s", $msginfo->mail_id);
+ " fetching msgs record for mail_id=%s", $p_mail_id);
# find a corresponding partition_tag if missing from a release request
$conn_h->begin_work_nontransaction; #(re)connect if necessary
- $conn_h->execute($sel_msg, untaint($msginfo->mail_id));
+ $conn_h->execute($sel_msg, $p_mail_id);
my $a_ref; my $cnt = 0; my $partition_tag;
while ( defined($a_ref=$conn_h->fetchrow_arrayref($sel_msg)) ) {
$cnt++;
$partition_tag = $a_ref->[0] if !defined $partition_tag;
ll(5) && do_log(5, "release: got msgs record for mail_id=%s: %s",
- $msginfo->mail_id, join(', ',@$a_ref));
+ $p_mail_id, join(', ',@$a_ref));
}
$conn_h->finish($sel_msg) if defined $a_ref; # only if not all read
$cnt <= 1 or die "Multiple ($cnt) records with same mail_id exist, ".
"specify a partition_tag in the AM.PDP request";
if ($cnt < 1) {
do_log(0, "release: no records with msgs.mail_id=%s in a database, ".
- "trying to read from a quar. anyway", $msginfo->mail_id);
+ "trying to read from a quar. anyway", $p_mail_id);
}
$msginfo->partition_tag($partition_tag); # could still be undef/NULL !
}
ll(5) && do_log(5, "release: opening mail_id=%s, partition_tag=%s",
- $msginfo->mail_id, $msginfo->partition_tag);
+ $p_mail_id, $msginfo->partition_tag);
$conn_h->begin_work_nontransaction; # (re)connect if not connected
$fh = Amavis::IO::SQL->new;
- $fh->open($conn_h, $sel_quar, untaint($msginfo->mail_id),
+ $fh->open($conn_h, $sel_quar, $p_mail_id,
'r', untaint($msginfo->partition_tag))
or die "Can't open sql obj for reading: $!"; 1;
} else { # mail checking or releasing from a file
@@ -20467,7 +21385,8 @@ sub check_ampdp_policy($$$$) {
my($new_fbody,$verbatim) = &$e($field_name,$field_body);
if (!defined($new_fbody)) { $field_body = undef; last } # delete
my $curr_head = $verbatim ? ($field_name . ':' . $new_fbody)
- : hdr($field_name, $new_fbody, 0);
+ : hdr($field_name, $new_fbody, 0,
+ $msginfo->smtputf8);
chomp($curr_head); $curr_head .= "\n";
$curr_head =~ /^([^:]*?)[ \t]*:(.*)\z/s;
$field_body = $2; chomp($field_body); # carry to next iteration
@@ -20581,16 +21500,16 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Util qw(ll do_log do_log_safe untaint
dump_captured_log log_capture_enabled
am_id new_am_id snmp_counters_init
- orcpt_encode xtext_decode debug_oneshot
- waiting_for_client prolong_timer
+ orcpt_decode xtext_decode safe_encode_utf8
+ idn_to_ascii sanitize_str add_entropy
+ debug_oneshot waiting_for_client prolong_timer
switch_to_my_time switch_to_client_time
- sanitize_str add_entropy
setting_by_given_contents_category);
import Amavis::Lookup qw(lookup lookup2);
import Amavis::Lookup::IP qw(lookup_ip_acl normalize_ip_addr);
@@ -20865,7 +21784,8 @@ sub process_smtp_request($$$$) {
# $conn: information about client connection
# $check_mail: subroutine ref to be called with file handle
- my($msginfo,$authenticated,$auth_user,$auth_pass);
+ my($msginfo, $authenticated, $auth_user, $auth_pass);
+ my(%announced_ehlo_keywords);
$self->{sock} = $sock;
$self->{pipelining} = 0; # may we buffer responses?
$self->{smtp_outbuf} = []; # SMTP responses buffer for PIPELINING
@@ -20875,14 +21795,15 @@ sub process_smtp_request($$$$) {
$tls_security_level = 0 if !defined($tls_security_level) ||
lc($tls_security_level) eq 'none';
my $myheloname;
-# $myheloname = c('myhostname');
+# $myheloname = idn_to_ascii(c('myhostname'));
# $myheloname = 'localhost';
# $myheloname = '[127.0.0.1]';
my $sock_ip = $conn->socket_ip;
$myheloname = defined $sock_ip && $sock_ip ne '' ? "[$sock_ip]"
: '[localhost]';
new_am_id(undef, $Amavis::child_invocation_count, undef);
- my $initial_am_id = 1; my($sender_unq,$sender_quo, at recips,$got_rcpt);
+ my $initial_am_id = 1;
+ my($sender_unq, $sender_quo, @recips, $got_rcpt);
my $max_recip_size_limit; # maximum of per-recipient message size limits
my($terminating,$aborting,$eof,$voluntary_exit); my(%xforward_args);
my $seq = 0;
@@ -20907,13 +21828,13 @@ sub process_smtp_request($$$$) {
s{ \$ (?: \{ ([^\}]+) \} |
([a-zA-Z](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?\b) ) }
{ { 'helo-name' => $myheloname,
- 'myhostname' => c('myhostname'),
+ 'myhostname' => idn_to_ascii(c('myhostname')),
'version' => $myversion,
'version-id' => $myversion_id,
'version-date' => $myversion_date,
'product' => $myproduct_name,
'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
- }egx;
+ }xgse;
$self->smtp_resp(1,"220 $smtpd_greeting_banner_tmp");
section_time('SMTP greeting');
# each call to smtp_resp starts a $smtpd_timeout timeout to tame slow clients
@@ -20975,13 +21896,13 @@ sub process_smtp_request($$$$) {
s{ \$ (?: \{ ([^\}]+) \} |
([a-zA-Z](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?\b) ) }
{ { 'helo-name' => $myheloname,
- 'myhostname' => c('myhostname'),
+ 'myhostname' => idn_to_ascii(c('myhostname')),
'version' => $myversion,
'version-id' => $myversion_id,
'version-date' => $myversion_date,
'product' => $myproduct_name,
'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
- }egx;
+ }xgse;
$self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp"); #flush!
$terminating = 1;
}
@@ -21005,8 +21926,12 @@ sub process_smtp_request($$$$) {
$self->smtp_resp(1,"554 5.5.1 Error: TLS already active");
} elsif (!$tls_security_level) {
$self->smtp_resp(1,"502 5.5.1 Error: command not available");
+ # } elsif (!$announced_ehlo_keywords{'STARTTLS'}) {
+ # $self->smtp_resp(1,"502 5.5.1 Error: ".
+ # "service extension STARTTLS was not announced");
} else {
$self->smtp_resp(1,"220 2.0.0 Ready to start TLS"); #flush!
+ %announced_ehlo_keywords = ();
IO::Socket::SSL->start_SSL($sock,
SSL_server => 1, SSL_session_cache => 2,
SSL_error_trap => sub { my($sock,$msg)=@_;
@@ -21030,7 +21955,7 @@ sub process_smtp_request($$$$) {
$self->smtp_resp(0,"250 $myheloname");
$conn->smtp_helo($args); section_time('SMTP HELO');
} elsif (/^(?:EHLO|LHLO)\z/) {
- $self->{pipelining} = 1; $lmtp = /^LHLO\z/ ? 1 : 0;
+ $self->{pipelining} = 1; $lmtp = $_ eq 'LHLO' ? 1 : 0;
$conn->appl_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP');
my(@ehlo_keywords) = (
'VRFY',
@@ -21038,7 +21963,8 @@ sub process_smtp_request($$$$) {
!defined($message_size_limit) ? 'SIZE' # RFC 1870
: sprintf('SIZE %d',$message_size_limit),
'ENHANCEDSTATUSCODES', # RFC 2034, RFC 3463, RFC 5248
- '8BITMIME', # RFC 1652
+ '8BITMIME', # RFC 6152
+ 'SMTPUTF8', # RFC 6531
'DSN', # RFC 3461
!$tls_security_level || $self->{ssl_active} ? ()
: 'STARTTLS', # RFC 3207 (ex RFC 2487)
@@ -21047,11 +21973,17 @@ sub process_smtp_request($$$$) {
'XFORWARD NAME ADDR PORT PROTO HELO IDENT SOURCE' );
my(%smtpd_discard_ehlo_keywords) =
map((uc($_),1), @{ca('smtpd_discard_ehlo_keywords')});
+ # RFC 6531: Servers offering this extension MUST provide
+ # support for, and announce, the 8BITMIME extension
+ $smtpd_discard_ehlo_keywords{'SMTPUTF8'} = 1
+ if $smtpd_discard_ehlo_keywords{'8BITMIME'};
@ehlo_keywords =
grep(/^([A-Za-z0-9]+)/ &&
- !$smtpd_discard_ehlo_keywords{uc($1)}, @ehlo_keywords);
+ !$smtpd_discard_ehlo_keywords{uc $1}, @ehlo_keywords);
$self->smtp_resp(1,"250 $myheloname\n" .
join("\n", at ehlo_keywords)); #flush!
+ %announced_ehlo_keywords =
+ map( (/^([A-Za-z0-9]+)/ && uc $1, 1), @ehlo_keywords);
$conn->smtp_helo($args); section_time("SMTP $_");
};
last;
@@ -21065,7 +21997,8 @@ sub process_smtp_request($$$$) {
}
my $bad;
for (split(' ',$args)) {
- if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) {
+ if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
+ ( [\x21-\x7E\x80-\xFF]{0,255} )\z/xs) {
$self->smtp_resp(1,"501 5.5.4 Syntax error in XFORWARD parameters",
1, $cmd);
$bad = 1; last;
@@ -21098,6 +22031,11 @@ sub process_smtp_request($$$$) {
};
/^AUTH\z/ && @{ca('auth_mech_avail')} && do { # RFC 4954 (ex RFC 2554)
+ # if (!$announced_ehlo_keywords{'AUTH'}) {
+ # $self->smtp_resp(1,"502 5.5.1 Error: ".
+ # "service extension AUTH was not announced");
+ # last;
+ # } elsif
if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) {
$self->smtp_resp(1,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd);
last;
@@ -21241,20 +22179,25 @@ sub process_smtp_request($$$$) {
%xforward_args = (); # reset values for the next transaction
if ($self->{ssl_active}) {
$msginfo->tls_cipher($sock->get_cipher);
- $conn->appl_proto($self->{proto}.'S') # RFC 3848
- if $self->{proto} =~ /^(LMTP|ESMTP)\z/i;
+ if ($self->{proto} =~ /^(LMTP|ESMTP)\z/i) {
+ $self->{proto} .= 'S'; # RFC 3848
+ $conn->appl_proto($self->{proto});
+ }
}
my $submitter;
if ($authenticated) {
$msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass);
- $conn->appl_proto($self->{proto}.'A') # RFC 3848
- if $self->{proto} =~ /^(LMTP|ESMTP)S?\z/i;
+ if ($self->{proto} =~ /^(LMTP|ESMTP)S?\z/i) {
+ $self->{proto} .= 'A'; # RFC 3848
+ $conn->appl_proto($self->{proto});
+ }
} elsif (c('auth_reauthenticate_forwarded') &&
c('amavis_auth_user') ne '') {
$msginfo->auth_user(c('amavis_auth_user'));
$msginfo->auth_pass(c('amavis_auth_pass'));
# $submitter = quote_rfc2821_local(c('mailfrom_notify_recip'));
- # $submitter = expand_variables($submitter) if defined $submitter;
+ # $submitter = expand_variables(safe_encode_utf8($submitter))
+ # if defined $submitter;
}
local($1,$2);
if ($args !~ /^FROM: [ \t]*
@@ -21265,33 +22208,89 @@ sub process_smtp_request($$$$) {
$self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM:<address>",1,$cmd);
last;
}
- my($addr,$opt) = ($1,$2); my($size,$dsn_ret,$dsn_envid);
- my $msg ; my $msg_nopenalize = 0;
+ my($addr,$opt) = ($1,$2);
+ my($size,$dsn_ret,$dsn_envid,$smtputf8);
+ my $msg; my $msg_nopenalize = 0;
for (split(' ',$opt)) {
- if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
- ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP
+ if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* )
+ (?: = ( [^=\000-\040\177]+ ) )? \z/xs) {
+ # any CHAR excluding "=", SP, and control characters
$msg = "501 5.5.4 Syntax error in MAIL FROM parameters";
} else {
my($name,$val) = (uc($1),$2);
- if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) { # RFC 1870
- if (!defined($size)) { $size = untaint($val) }
- else { $msg = "501 5.5.4 Syntax error in MAIL parameter: $name" }
- } elsif ($name eq 'BODY' && $val=~/^(?:7BIT|8BITMIME)\z/i) {
- $msginfo->body_type(uc($val));
+ if (!defined($val) && $name =~ /^(?:BODY|RET|ENVID|AUTH)\z/) {
+ $msg = "501 5.5.4 Syntax error in MAIL parameter, ".
+ "value is required: $name";
+ } elsif ($name eq 'SIZE') { # RFC 1870
+ if (!$announced_ehlo_keywords{'SIZE'}) {
+ do_log(5,'service extension SIZE was not announced');
+ # "555 5.5.4 Service extension SIZE was not announced: $name"
+ }
+ if (!defined $val) {
+ # value not provided, ignore
+ } elsif ($val !~ /^\d{1,20}\z/) {
+ $msg = "501 5.5.4 Syntax error in MAIL parameter: $name";
+ } else {
+ $size = untaint($val) if !defined $size;
+ }
+ } elsif ($name eq 'SMTPUTF8') { # RFC 6531
+ if (!$announced_ehlo_keywords{'SMTPUTF8'}) {
+ do_log(5,'service extension SMTPUTF8 was not announced');
+ # "555 5.5.4 Service extension SMTPUTF8 not announced: $name"
+ }
+ if (defined $val) {
+ # RFC 6531: The parameter does not accept a value.
+ $msg = "501 5.5.4 Syntax error in MAIL parameter: $name";
+ } else {
+ $msginfo->smtputf8(1);
+ if ($self->{proto} =~ /^(LMTP|ESMTP)S?A?\z/si) {
+ $self->{proto} = 'UTF8' . $self->{proto}; # RFC 6531
+ $self->{proto} =~ s/^UTF8ESMTP/UTF8SMTP/s;
+ $conn->appl_proto($self->{proto});
+ }
+ }
+ } elsif ($name eq 'BODY') { # RFC 6152: 8bit-MIMEtransport
+ if (!$announced_ehlo_keywords{'8BITMIME'}) {
+ do_log(5,'service extension 8BITMIME was not announced: BODY');
+ # "555 5.5.4 Service extension 8BITMIME not announced: $name"
+ }
+ if (defined $val && $val =~ /^(?:7BIT|8BITMIME)\z/i) {
+ $msginfo->body_type(uc $val);
+ } else {
+ $msg = "501 5.5.4 Syntax error in MAIL parameter: $name";
+ }
} elsif ($name eq 'RET') { # RFC 3461
- if (!defined($dsn_ret)) { $dsn_ret = uc($val) }
- else { $msg = "501 5.5.4 Syntax error in MAIL parameter: $name" }
+ if (!$announced_ehlo_keywords{'DSN'}) {
+ do_log(5,'service extension DSN was not announced: RET');
+ # "555 5.5.4 Service extension DSN not announced: $name"
+ }
+ if (!defined($dsn_ret)) {
+ $dsn_ret = uc $val;
+ } else {
+ $msg = "501 5.5.4 Syntax error in MAIL parameter: $name";
+ }
} elsif ($name eq 'ENVID') { # RFC 3461, value encoded as xtext
- if (!defined($dsn_envid)) { $dsn_envid = $val }
- else { $msg = "501 5.5.4 Syntax error in MAIL parameter: $name" }
+ if (!$announced_ehlo_keywords{'DSN'}) {
+ do_log(5,'service extension DSN was not announced: ENVID');
+ # "555 5.5.4 Service extension DSN not announced: $name"
+ }
+ if (!defined($dsn_envid)) {
+ $dsn_envid = $val;
+ } else {
+ $msg = "501 5.5.4 Syntax error in MAIL parameter: $name";
+ }
} elsif ($name eq 'AUTH') { # RFC 4954 (ex RFC 2554)
+ if (!$announced_ehlo_keywords{'AUTH'}) {
+ do_log(5,'service extension AUTH was not announced');
+ # "555 5.5.4 Service extension AUTH not announced: $name"
+ }
my $s = xtext_decode($val); # encoded as xtext: RFC 3461
- do_log(5, "MAIL command, %s, submitter: %s", $authenticated,$s);
+ do_log(5,"MAIL command, %s, submitter: %s", $authenticated,$s);
if (defined $submitter) { # authorized identity
$msg = "504 5.5.4 MAIL command duplicate param.: $name=$val";
} elsif (!@{ca('auth_mech_avail')}) {
- do_log(3, "MAIL command parameter AUTH supplied, but ".
- "authentication capability not announced, ignored");
+ do_log(3,"MAIL command parameter AUTH supplied, but ".
+ "authentication capability not announced, ignored");
$submitter = '<>';
# mercifully ignore invalid parameter for the benefit of
# running amavisd as a Postfix pre-queue smtp proxy filter
@@ -21357,20 +22356,43 @@ sub process_smtp_request($$$$) {
$self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO:<address>",1,$cmd);
last;
}
- my($addr_smtp,$opt) = ($1,$2); my($notify,$orcpt);
+ my($addr_smtp,$opt) = ($1,$2);
+ my($notify,$orcpt);
my $msg; my $msg_nopenalize = 0;
for (split(' ',$opt)) {
- if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
- ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP
+ if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* )
+ (?: = ( [^=\000-\040\177]+ ) )? \z/xs) {
+ # any CHAR excluding "=", SP, and control characters
$msg = "501 5.5.4 Syntax error in RCPT parameters";
} else {
my($name,$val) = (uc($1),$2);
- if ($name eq 'NOTIFY') { # RFC 3461
- if (!defined($notify)) { $notify = $val }
- else { $msg = "501 5.5.4 Syntax error in RCPT parameter $name" }
- } elsif ($name eq 'ORCPT') { # RFC 3461, value encoded as xtext
- if (!defined($orcpt)) { $orcpt = $val }
- else { $msg = "501 5.5.4 Syntax error in RCPT parameter $name" }
+ if (!defined($val) && $name =~ /^(?:NOTIFY|ORCPT)\z/) {
+ $msg = "501 5.5.4 Syntax error in RCPT parameter, ".
+ "value is required: $name";
+ } elsif ($name eq 'NOTIFY') { # RFC 3461
+ if (!$announced_ehlo_keywords{'DSN'}) {
+ do_log(5,'service extension DSN was not announced: NOTIFY');
+ # "555 5.5.4 Service extension DSN not announced: $name"
+ }
+ if (!defined($notify)) {
+ $notify = $val;
+ } else {
+ $msg = "501 5.5.4 Syntax error in RCPT parameter $name";
+ }
+ } elsif ($name eq 'ORCPT') {
+ # RFC 3461: value encoded as xtext
+ # RFC 6533: utf-8-addr-xtext, utf-8-addr-unitext, utf-8-address
+ if (!$announced_ehlo_keywords{'DSN'}) {
+ do_log(5,'service extension DSN was not announced: ORCPT');
+ # "555 5.5.4 Service extension DSN not announced: $name"
+ }
+ if (defined $orcpt) { # duplicate
+ $msg = "501 5.5.4 Syntax error in RCPT parameter $name";
+ } else {
+ my($addr_type, $orcpt_dec) =
+ orcpt_decode($val, $msginfo->smtputf8);
+ $orcpt = $addr_type . ';' . $orcpt_dec;
+ }
} else {
$msg = "555 5.5.4 RCPT command parameter unrecognized: $name";
# 504 5.5.4 RCPT command parameter not implemented:
@@ -21383,15 +22405,20 @@ sub process_smtp_request($$$$) {
my $addr = unquote_rfc2821_local($addr_smtp);
my $requoted = qquote_rfc2821_local($addr);
if ($requoted ne $addr_smtp) { # check for valid canonical quoting
- do_log(0, "WARN: address modified (recip): %s -> %s",
- $addr_smtp, $requoted);
# RFC 3461: If no ORCPT parameter was present in the RCPT command
# when the message was received, an ORCPT parameter MAY be added
# to the RCPT command when the message is relayed. If an ORCPT
# parameter is added by the relaying MTA, it MUST contain the
# recipient address from the RCPT command used when the message
# was received by that MTA
- $orcpt = orcpt_encode($addr_smtp) if !defined $orcpt;
+ if (defined $orcpt) {
+ do_log(2, "address modified (recip): %s -> %s, orcpt retained: %s",
+ $addr_smtp, $requoted, $orcpt);
+ } else {
+ do_log(2, "address modified (recip): %s -> %s, setting orcpt",
+ $addr_smtp, $requoted);
+ $orcpt = ';' . $addr_smtp;
+ }
}
if (lookup2(0,$addr, ca('debug_recipient_maps'))) {
debug_oneshot(1, $self->{proto} . "< $cmd");
@@ -21512,7 +22539,7 @@ sub process_smtp_request($$$$) {
eval {
$msginfo->sender($sender_unq); $msginfo->sender_smtp($sender_quo);
$msginfo->per_recip_data(\@recips);
- ll(1) && do_log(1, "%s:%s:%s %s: %s -> %s%s Received: %s",
+ ll(1) && do_log(1, "%s %s:%s %s: %s -> %s%s Received: %s",
$conn->appl_proto,
!ref $inet_socket_bind && $conn->socket_ip eq $inet_socket_bind
? '' : '['.$conn->socket_ip.']',
@@ -21523,12 +22550,13 @@ sub process_smtp_request($$$$) {
!defined $msginfo->msg_size ? () : # RFC 1870
' SIZE='.$msginfo->msg_size,
!defined $msginfo->body_type ? () : ' BODY='.$msginfo->body_type,
- !defined $msginfo->auth_submitter ||
- $msginfo->auth_submitter eq '<>' ? () :
- ' AUTH='.$msginfo->auth_submitter,
+ !$msginfo->smtputf8 ? () : ' SMTPUTF8',
!defined $msginfo->dsn_ret ? () : ' RET='.$msginfo->dsn_ret,
!defined $msginfo->dsn_envid ? () :
' ENVID='.xtext_decode($msginfo->dsn_envid),
+ !defined $msginfo->auth_submitter ||
+ $msginfo->auth_submitter eq '<>' ? () :
+ ' AUTH='.$msginfo->auth_submitter,
),
make_received_header_field($msginfo,0) );
# pipelining checkpoint
@@ -21841,15 +22869,15 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform);
import Amavis::Util qw(ll do_log min max minmax);
}
use Errno qw(EIO EINTR EAGAIN ECONNRESET);
+use Encode ();
use Time::HiRes ();
-use Encode;
sub init {
my $self = $_[0];
@@ -21891,7 +22919,7 @@ sub ehlo_response_parse {
elsif (!defined($bad)) { $bad = $el }
$first = 0;
} elsif ($el =~ /^([A-Z0-9][A-Z0-9-]*)(?:[ =](.*))?\z/si) {
- $self->{supports}{uc($1)} = defined($2) ? $2 : '';
+ $self->{supports}{uc($1)} = defined $2 ? $2 : '';
} elsif ($el =~ /^[ \t]*\z/s) {
# don't bother (e.g. smtp-sink)
} elsif (!defined($bad)) {
@@ -21914,7 +22942,7 @@ sub supports
sub datasend {
my $self = shift;
my $buff = @_ == 1 ? $_[0] : join('', at _);
- do_log(-1,"WARN: Unicode string passed to datasend")
+ do_log(-1,"WARN: Unicode string passed to datasend: %s", $buff)
if Encode::is_utf8($buff); # always false on tainted, Perl 5.8 bug #32687
# ll(5) && do_log(5, 'smtp print %d bytes>', length($buff));
$buff =~ tr/\r//d if $self->{strip_cr}; # sanitize bare CR if necessary
@@ -22041,11 +23069,11 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&rundown_stale_sessions);
import Amavis::Conf qw(:platform c cr ca $smtp_connection_cache_enable);
- import Amavis::Util qw(min max minmax ll do_log snmp_count);
+ import Amavis::Util qw(min max minmax ll do_log snmp_count idn_to_ascii);
}
use subs @EXPORT_OK;
use vars qw(%sessions_cache);
@@ -22060,7 +23088,7 @@ sub new {
$cache_key = $proto_sockname;
local($1,$2,$3,$4);
if ($proto_sockname =~ # deal with dynamic destinations (wildcards)
- /^([a-z][a-z0-9.+-]*) : (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*)/sx) {
+ /^([a-z][a-z0-9.+-]*) : (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*)/xs) {
my $peeraddress = defined $2 ? $2 : $3; my $peerport = $4;
$peeraddress = $wildcard_implied_host if $peeraddress eq '*';
$peerport = $wildcard_implied_port if $peerport eq '*';
@@ -22145,12 +23173,12 @@ sub timeout {
sub supports {
my($self,$keyword) = @_;
- defined $self->{handle} ? $self->{handle}->supports($keyword) : undef;
+ $self->{handle} ? $self->{handle}->supports($keyword) : undef;
}
sub smtp_response {
my $self = $_[0];
- defined $self->{handle} ? $self->{handle}->smtp_response : undef;
+ $self->{handle} ? $self->{handle}->smtp_response : undef;
}
sub quit {
@@ -22328,14 +23356,15 @@ sub establish_or_refresh {
my $tls_security_level = c('tls_security_level_out');
$tls_security_level = 0 if !defined($tls_security_level) ||
lc($tls_security_level) eq 'none';
- my $heloname = c('localhost_name'); # host name used in EHLO/HELO/LHLO
- $heloname = 'localhost' if $heloname eq '';
+ my $myheloname = c('localhost_name'); # host name used in EHLO/HELO/LHLO
+ $myheloname = 'localhost' if $myheloname eq '';
+ $myheloname = idn_to_ascii($myheloname);
for (1..2) {
# send EHLO/LHLO/HELO
$self->timeout(max(60,min($smtp_helo_timeout,
$deadline - time)));
- if ($lmtp) { $smtp_handle->lhlo($heloname) } #flush!
- else { $smtp_handle->ehlo($heloname) } #flush!
+ if ($lmtp) { $smtp_handle->lhlo($myheloname) } #flush!
+ else { $smtp_handle->ehlo($myheloname) } #flush!
$smtp_resp = $self->smtp_response; # fetch response to EHLO/LHLO
if (!defined $smtp_resp || $smtp_resp eq '') {
die sprintf("%s response to %s, dt: %.3f s\n",
@@ -22348,7 +23377,7 @@ sub establish_or_refresh {
die "Negative SMTP resp. to LHLO: $smtp_resp\n";
} else { # failure, SMTP fallback to HELO
do_log(3,"Negative SMTP resp. to EHLO, will try HELO: %s", $smtp_resp);
- $smtp_handle->helo($heloname); #flush!
+ $smtp_handle->helo($myheloname); #flush!
$smtp_resp = $self->smtp_response; # fetch response to HELO
if (!defined $smtp_resp || $smtp_resp eq '') {
die sprintf("%s response to HELO, dt: %.3f s\n",
@@ -22362,7 +23391,7 @@ sub establish_or_refresh {
}
$self->session_state('ehlo');
$smtp_handle->ehlo_response_parse($smtp_resp);
- my $tls_capable = defined($self->supports('STARTTLS')); # RFC 3207
+ my $tls_capable = defined $self->supports('STARTTLS'); # RFC 3207
ll(5) && do_log(5, "tls active=%d, capable=%s, sec_level=%s",
$smtp_handle->ssl_active, $tls_capable, $tls_security_level);
if ($smtp_handle->ssl_active) {
@@ -22405,13 +23434,15 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT = qw(&mail_via_smtp);
import Amavis::Conf qw(:platform c cr ca $smtp_connection_cache_enable);
import Amavis::Util qw(untaint min max minmax ll do_log snmp_count
- xtext_encode xtext_decode prolong_timer
- get_deadline collect_equal_delivery_recips);
+ xtext_encode xtext_decode orcpt_encode orcpt_decode
+ idn_to_ascii mail_addr_idn_to_ascii
+ prolong_timer get_deadline
+ collect_equal_delivery_recips);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools;
import Amavis::Lookup qw(lookup lookup2);
@@ -22419,6 +23450,7 @@ BEGIN {
}
use Time::HiRes qw(time);
+use Encode ();
# use Authen::SASL;
# simple OO wrapper around Mail::DKIM::Signer to provide a method 'print'
@@ -22434,6 +23466,8 @@ sub close { 1 }
sub print {
my $self = shift;
my $buff = @_ == 1 ? $_[0] : join('', at _);
+ do_log(-1,"WARN: Unicode string passed to Amavis::Out::SMTP::print : %s",
+ $buff) if Encode::is_utf8($buff); # false on tainted, Perl 5.8 bug #32687
$buff =~ tr/\r//d if $self->{strip_cr};
$buff =~ s{\n}{\015\012}gs;
$self->{handle}->PRINT($buff);
@@ -22481,29 +23515,32 @@ sub mail_via_smtp(@) {
my $which_section = 'fwd_init';
my $id = $msginfo->parent_mail_id;
$id = $msginfo->mail_id . (defined $id ? "($id)" : "");
- my $logmsg = sprintf("%s %s from %s", $id,
- ($initial_submission ? 'SEND' : 'FWD'),
- $msginfo->sender_smtp);
+ my $sender_smtp = $msginfo->sender_smtp;
+ my $logmsg = sprintf("%s %s", $id, $initial_submission?'SEND':'FWD');
my($per_recip_data_ref, $proto_sockname) =
collect_equal_delivery_recips($msginfo, $filter, qr/^(?:smtp|lmtp):/i);
if (!$per_recip_data_ref || !@$per_recip_data_ref) {
- do_log(5, "%s, nothing to do", $logmsg); return 1;
+ do_log(5, "%s from %s, nothing to do", $logmsg, $sender_smtp);
+ return 1;
}
my $proto_sockname_displ = !ref $proto_sockname ? $proto_sockname
: '(' . join(', ',@$proto_sockname) . ')';
my(@per_recip_data) = @$per_recip_data_ref; undef $per_recip_data_ref;
- ll(4) && do_log(4, "about to connect to %s, %s -> %s",
- $proto_sockname_displ, $logmsg,
+ ll(4) && do_log(4, "about to connect to %s, %s from %s -> %s",
+ $proto_sockname_displ, $logmsg, $sender_smtp,
join(',', qquote_rfc2821_local(
map($_->recip_final_addr, @per_recip_data)) ));
my $am_id = $msginfo->log_id;
- my $dsn_envid = $msginfo->dsn_envid; my $dsn_ret = $msginfo->dsn_ret;
+ my $dsn_envid = $msginfo->dsn_envid;
+ my $dsn_ret = $msginfo->dsn_ret;
+ my $smtputf8 = $msginfo->smtputf8; # SMTPUTF8 requested
+ my $smtputf8_capable; # SMTPUTF8 offered by MTA, RFC 6531
my($relayhost, $protocol, $lmtp, $mta_id, @snmp_vars);
my($smtp_session, $smtp_handle, $smtp_resp, $smtp_response);
my($any_valid_recips, $any_tempfail_recips, $pipelining,
$any_valid_recips_and_data_sent, $recips_done_by_early_fail,
$in_datasend_mode, $dsn_capable, $auth_capable) = (0) x 8;
- my $mimetransport8bit_capable = 0; # RFC 1652
+ my $mimetransport8bit_capable = 0; # RFC 6152
my(%from_options);
# RFC 5321 (ex RFC 2821), section 4.5.3.2. Timeouts
my $smtp_connect_timeout = 35;
@@ -22554,18 +23591,96 @@ sub mail_via_smtp(@) {
}
$dsn_capable = c('propagate_dsn_if_possible') &&
defined($smtp_session->supports('DSN')); # RFC 3461
- $mimetransport8bit_capable =
- defined($smtp_session->supports('8BITMIME')); # RFC 1652
+ $mimetransport8bit_capable = # 8bit-MIMEtransport service extension
+ defined($smtp_session->supports('8BITMIME')); # RFC 6152
+ $smtputf8_capable = # "Internationalized Email" service extension
+ $mimetransport8bit_capable &&
+ defined($smtp_session->supports('SMTPUTF8')); # RFC 6531
$pipelining = defined($smtp_session->supports('PIPELINING')); # RFC 2920
do_log(3,"No announced PIPELINING support by MTA?") if !$pipelining;
- ll(5) && do_log(5,"Remote host presents itself as: %s%s%s",
+ ll(5) && do_log(5,"Remote host presents itself as: %s, handles %s",
$smtp_handle->domain,
- $dsn_capable ? ', handles DSN' : '',
- $pipelining ? ', handles PIPELINING' : '');
- if ($lmtp && !$pipelining) # RFC 2033 requirements
- { die "An LMTP server implementation MUST implement PIPELINING" }
- if ($lmtp && !defined($smtp_session->supports('ENHANCEDSTATUSCODES')))
- { die "An LMTP server implementation MUST implement ENHANCEDSTATUSCODES" }
+ join(', ', $dsn_capable ? 'DSN' : (),
+ $pipelining ? 'PIPELINING' : (),
+ $mimetransport8bit_capable ? '8BITMIME' : (),
+ $smtputf8_capable ? 'SMTPUTF8' : () ) );
+ if ($lmtp && !$pipelining) { # RFC 2033 requirements
+ die "An LMTP server implementation MUST implement PIPELINING";
+ }
+ if ($lmtp && !defined($smtp_session->supports('ENHANCEDSTATUSCODES'))) {
+ die "An LMTP server implementation MUST implement ENHANCEDSTATUSCODES";
+ }
+
+ if (!$smtputf8_capable || !$smtputf8) {
+ # if SMTPUTF8 is not requested or if MTA is unable to handle
+ # IDN with U-labels, and local part is all-ASCII, then we may
+ # still get this delivered by converting a domain name
+ # to ASCII-compatible encoding (ACE)
+ if ($sender_smtp =~ /^ [\x00-\x7F]* \@ [^\@]* [^\x00-\x7F] [^\@]*\z/xs) {
+ # localpart all-ASCII, domain is non-ASCII
+ my $idn_ascii = mail_addr_idn_to_ascii($sender_smtp);
+ do_log(2,'sender IDN encoded to ACE: %s -> %s',
+ $sender_smtp, $idn_ascii);
+ $sender_smtp = $idn_ascii;
+ }
+ for my $r (@per_recip_data) {
+ next if $r->recip_done;
+ my $rcpt_addr = $r->recip_final_addr;
+ if ($rcpt_addr =~ /^ [\x00-\x7F]* \@ [^\@]* [^\x00-\x7F] [^\@]*\z/xs) {
+ my $idn_ascii = mail_addr_idn_to_ascii($rcpt_addr);
+ do_log(2,'recipient IDN encoded to ACE: %s -> %s',
+ $rcpt_addr, $idn_ascii);
+ $rcpt_addr = $idn_ascii;
+ $r->dsn_orcpt(join(';', orcpt_decode(';'.$r->recip_addr_smtp)))
+ if !defined $r->dsn_orcpt;
+ # N.B.: change recip_addr_modified(), not recip_final_addr() !
+ $r->recip_addr_modified($rcpt_addr);
+ }
+ }
+ }
+
+ if ($smtputf8) { # SMTPUTF8 handling was requested, RFC 6531
+ #
+ # RFC 6531 section 3.4: If the SMTPUTF8-aware SMTP client is aware
+ # that neither the envelope nor the message being sent requires any
+ # of the SMTPUTF8 extension capabilities, it SHOULD NOT supply the
+ # SMTPUTF8 parameter with the MAIL command.
+ #
+ my($sender_8bit, $recips_8bit);
+ $sender_8bit = 1 if $msginfo->sender_smtp =~ tr/\x00-\x7F//c;
+ for my $r (@per_recip_data) {
+ next if $r->recip_done;
+ $recips_8bit = 1 if $r->recip_final_addr =~ tr/\x00-\x7F//c;
+ }
+
+ if (!ll(5)) {
+ # don't bother, just logging
+ } elsif ($sender_8bit || $recips_8bit || $msginfo->header_8bit) {
+ do_log(5,'SMTPUTF8 option requested and is needed, %s is non-ASCII',
+ join(' & ', $sender_8bit ? 'sender' : (),
+ $recips_8bit ? 'recip' : (),
+ $msginfo->header_8bit ? 'header' : () ));
+ } else {
+ do_log(5,'SMTPUTF8 option requested but not needed');
+ }
+
+ if (!$smtputf8_capable) {
+ # RFC 6531 sect 3.5: An SMTPUTF8-aware SMTP client MUST NOT send
+ # an internationalized message to an SMTP server that does not
+ # support SMTPUTF8.
+ # 550 5.6.7 Non-ASCII addresses not permitted for that sender
+ # 553 5.6.7 Non-ASCII addresses not permitted for that recipient
+ # after DATA-dot:
+ # 554 5.6.9 UTF-8 header message cannot be transmitted to one or more
+ # recipients, so the message must be rejected
+ #
+ if (!$sender_8bit && !$recips_8bit) {
+ # mail addresses are all-ASCII, don't care for an 8bit header
+ do_log(3,'SMTPUTF8 option requested but not offered, turning it off');
+ $smtputf8 = 0; # turn off if not needed
+ }
+ }
+ }
section_time($which_section);
$which_section = 'fwd-xforward';
@@ -22583,8 +23698,8 @@ sub mail_via_smtp(@) {
# versions expected plain text with neutered special characters;
# see README_FILES/XFORWARD_README
if (defined $v && $v ne '') {
- $v =~ s/[^\041-\176]/?/g; # isprint
- $v =~ s/[<>()\\";\@]/?/g; # other chars that are special in hdrs
+ $v =~ s/[^\041-\176]/?/gs; # isprint
+ $v =~ s/[<>()\\";\@]/?/gs; # other chars that are special in hdrs
# postfix/src/smtpd/smtpd.c NEUTER_CHARACTERS
$v = xtext_encode($v);
substr($v,255) = '' if length($v) > 255; # chop xtext, not nice
@@ -22641,45 +23756,76 @@ sub mail_via_smtp(@) {
# ENVID identifies transaction, not a message
$dsn_envid = xtext_encode(sprintf("AM.%s.%s\@%s",
$msginfo->mail_id || $msginfo->log_id,
- iso8601_utc_timestamp(time), c('myhostname')));
+ iso8601_utc_timestamp(time),
+ idn_to_ascii(c('myhostname')) ));
+ }
+ if ($smtputf8 && $smtputf8_capable) {
+ $from_options{'SMTPUTF8'} = undef; # turn option *on*, no value
}
- my $submitter = $msginfo->auth_submitter;
my $btype = $msginfo->body_type;
- $from_options{'BODY'} = uc($btype) if $mimetransport8bit_capable
- && defined($btype) && $btype ne '';
- $from_options{'RET'} = $dsn_ret if $dsn_capable && defined $dsn_ret;
- $from_options{'ENVID'} = $dsn_envid if $dsn_capable && defined $dsn_envid;
+ if (defined $btype && $btype ne '') {
+ $btype = uc $btype;
+ if ($btype ne '7BIT' && $btype ne '8BITMIME') {
+ do_log(-1,'requested BODY type %s is unknown/unsupported', $btype);
+ } elsif ($mimetransport8bit_capable) {
+ $from_options{'BODY'} = $btype;
+ } elsif ($btype ne '7BIT') {
+ do_log(0, 'requested BODY type is %s, but MTA does not announce '.
+ '8bit-MIMEtransport capability', $btype); # RFC 6152
+ }
+ }
+ $from_options{'RET'} = $dsn_ret if $dsn_capable && defined $dsn_ret;
+ if ($dsn_capable && defined $dsn_envid) {
+ # check for proper encoding (RFC 3461), just in case
+ if ($dsn_envid =~ tr/ =\x00-\x1F//) {
+ do_log(-1, "Prohibited character in ENVID: %s", $dsn_envid);
+ } else {
+ $from_options{'ENVID'} = $dsn_envid;
+ }
+ }
+ my $submitter = $msginfo->auth_submitter;
$from_options{'AUTH'} = xtext_encode($submitter) # RFC 4954 (ex RFC 2554)
if $auth_capable &&
defined($submitter) && $submitter ne '' && $submitter ne '<>';
- my $faddr = $msginfo->sender_smtp;
- $smtp_handle->mail($faddr, %from_options); # MAIL FROM
- # consider the transaction state unknown until we see a response
- $smtp_session->transaction_begins_unconfirmed; # also counts transactions
- if (!$pipelining) {
- $smtp_resp = $smtp_session->smtp_response; $fetched_mail_resp = 1;
- if (!defined $smtp_resp || $smtp_resp eq '') {
- die sprintf("%s response to MAIL, dt: %.3f s\n",
- !defined $smtp_resp ? 'No' : 'Empty',
- time - $smtp_handle->last_io_event_tx_timestamp);
- } elsif ($smtp_resp =~ /^2/) {
- do_log(3, "smtp resp to MAIL: %s", $smtp_resp);
- $smtp_session->transaction_begins; # transaction is active
- } else { # failure
- do_log(1, "smtp resp to MAIL: %s", $smtp_resp);
- # transaction state unchanged, consider it unknown
- my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id,
- '.1.0','MAIL FROM');
- for my $r (@per_recip_data) {
- next if $r->recip_done;
- $r->recip_remote_mta($relayhost);
- $r->recip_remote_mta_smtp_response($smtp_resp);
- $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2);
+ if ($smtputf8 && !$smtputf8_capable && $sender_smtp =~ tr/\x00-\x7F//c) {
+ do_log(1,'SMTPUTF8 option requested, not offered by MTA, '.
+ 'sender is non-ASCII: %s', $sender_smtp);
+ for my $r (@per_recip_data) {
+ next if $r->recip_done;
+ $r->recip_smtp_response('550 5.6.7 Non-ASCII addresses not permitted '.
+ 'for sender');
+ $r->recip_remote_mta($relayhost); $r->recip_done(2);
+ }
+ $recips_done_by_early_fail = 1;
+ } else {
+ $smtp_handle->mail($sender_smtp, %from_options); # MAIL FROM
+ # consider the transaction state unknown until we see a response
+ $smtp_session->transaction_begins_unconfirmed; # also counts transactions
+ if (!$pipelining) {
+ $smtp_resp = $smtp_session->smtp_response; $fetched_mail_resp = 1;
+ if (!defined $smtp_resp || $smtp_resp eq '') {
+ die sprintf("%s response to MAIL, dt: %.3f s\n",
+ !defined $smtp_resp ? 'No' : 'Empty',
+ time - $smtp_handle->last_io_event_tx_timestamp);
+ } elsif ($smtp_resp =~ /^2/) {
+ do_log(3, "smtp resp to MAIL: %s", $smtp_resp);
+ $smtp_session->transaction_begins; # transaction is active
+ } else { # failure
+ do_log(1, "smtp resp to MAIL: %s", $smtp_resp);
+ # transaction state unchanged, consider it unknown
+ my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id,
+ '.1.0','MAIL FROM');
+ for my $r (@per_recip_data) {
+ next if $r->recip_done;
+ $r->recip_remote_mta($relayhost);
+ $r->recip_remote_mta_smtp_response($smtp_resp);
+ $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2);
+ }
+ $recips_done_by_early_fail = 1;
}
- $recips_done_by_early_fail = 1;
+ section_time($which_section);
}
}
- section_time($which_section) if !$pipelining; # otherwise it just shows 0
$which_section = 'fwd-rcpt-to';
$smtp_session->timeout(max(60,min($smtp_rcpt_timeout,$deadline-time())));
@@ -22692,8 +23838,15 @@ sub mail_via_smtp(@) {
}
# prepare to send a RCPT TO command
my $raddr = qquote_rfc2821_local($r->recip_final_addr);
- if (!$dsn_capable) {
+ if ($smtputf8 && !$smtputf8_capable && $raddr =~ tr/\x00-\x7F//c) {
+ do_log(1,'SMTPUTF8 option requested, not offered by MTA, '.
+ 'recipient is non-ASCII: %s', $raddr);
+ $r->recip_smtp_response('553 5.6.7 Non-ASCII addresses '.
+ 'not permitted for recipient');
+ $r->recip_remote_mta($relayhost); $r->recip_done(2);
+ } elsif (!$dsn_capable) {
$smtp_handle->recipient($raddr); # a barebones RCPT TO command
+ push(@per_recip_data_rcpt_sent, $r); # remember which recips were sent
} else { # include dsn options with a RCPT TO command
my(@dsn_notify); # implies a default when the list is empty
my $dn = $r->dsn_notify;
@@ -22710,10 +23863,12 @@ sub mail_via_smtp(@) {
my(%rcpt_options);
$rcpt_options{'NOTIFY'} =
join(',', map(uc($_), at dsn_notify)) if @dsn_notify;
- $rcpt_options{'ORCPT'} = $r->dsn_orcpt if defined $r->dsn_orcpt;
+ my($addr_type, $addr) =
+ orcpt_encode($r->dsn_orcpt, $smtputf8 && $smtputf8_capable, 1);
+ $rcpt_options{'ORCPT'} = $addr_type.';'.$addr if defined $addr;
$smtp_handle->recipient($raddr, %rcpt_options); # RCPT TO
+ push(@per_recip_data_rcpt_sent, $r); # remember which recips were sent
}
- push(@per_recip_data_rcpt_sent, $r); # remember which recips were sent
if (!$pipelining) { # must fetch responses to RCPT TO right away
$smtp_resp = $smtp_session->smtp_response; $fetched_rcpt_resp = 1;
if (defined $smtp_resp && $smtp_resp ne '') {
@@ -22918,7 +24073,7 @@ sub mail_via_smtp(@) {
$msg->print_body($smtp_handle);
} else {
my($nbytes,$buff);
- while (($nbytes = $msg->read($buff,65536)) > 0) {
+ while (($nbytes = $msg->read($buff,3*16384)) > 0) {
$smtp_handle->datasend($buff);
}
defined $nbytes or die "Error reading: $!";
@@ -23093,7 +24248,8 @@ sub mail_via_smtp(@) {
# but a value of 'AV' is supplied by av_smtp_client to allow a forwarding
# method to distinguish it from ordinary submissions
my $ll = ($smtp_response =~ /^2/ || $initial_submission eq 'AV') ? 1 : -1;
- ll($ll) && do_log($ll, "%s -> %s, %s %s", $logmsg,
+ ll($ll) && do_log($ll, "%s from %s -> %s, %s %s",
+ $logmsg, $sender_smtp,
join(',', qquote_rfc2821_local(
map($_->recip_final_addr, @per_recip_data))),
join(' ', map { my $v=$from_options{$_}; defined($v)?"$_=$v":"$_" }
@@ -23143,7 +24299,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT = qw(&mail_via_pipe);
import Amavis::Conf qw(:platform c cr ca);
@@ -23347,12 +24503,12 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT = qw(&mail_via_bsmtp);
import Amavis::Conf qw(:platform $QUARANTINEDIR c cr ca);
import Amavis::Util qw(untaint min max minmax ll do_log snmp_count
- collect_equal_delivery_recips);
+ idn_to_ascii collect_equal_delivery_recips);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools;
import Amavis::Out::EditHeader;
@@ -23404,7 +24560,7 @@ sub mail_via_bsmtp(@) {
: $1 eq 'n' ? $msginfo->log_id
: $1 eq 's' ? untaint($s) # a hack, avoid using %s
: $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1) #,'-')
- : $1 eq '%' ? '%' : '%'.$1 }egs;
+ : $1 eq '%' ? '%' : '%'.$1 }gse;
# prepend directory if not specified
my $bsmtp_file_final_to_show = $bsmtp_file_final;
$bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final
@@ -23429,13 +24585,15 @@ sub mail_via_bsmtp(@) {
# The generator MAY assume that ESMTP [RFC 1869 (obsoleted by RFC 5321)]
# facilities are available, that is, it is acceptable to use the EHLO
# command and additional parameters on MAIL FROM and RCPT TO. If EHLO
-# is used MAY assume that the 8bitMIME [RFC 1652], SIZE [RFC 1870], and
+# is used MAY assume that the 8bitMIME [RFC 6152], SIZE [RFC 1870], and
# NOTARY [RFC 1891] extensions are available. In particular, NOTARY
# SHOULD be used. (nowadays called DSN)
- $mp->printf("EHLO %s\n", c('localhost_name'))
- or die "print failed (EHLO): $!";
- my $btype = $msginfo->body_type; # RFC 1652: need "8bit Data"? (RFC 2045)
+ my $myheloname = c('localhost_name'); # host name used in EHLO/HELO/LHLO
+ $myheloname = 'localhost' if $myheloname eq '';
+ $myheloname = idn_to_ascii($myheloname);
+ $mp->printf("EHLO %s\n", $myheloname) or die "print failed (EHLO): $!";
+ my $btype = $msginfo->body_type; # RFC 6152: need "8bit Data"? (RFC 2045)
$btype = '' if !defined $btype;
my $dsn_envid = $msginfo->dsn_envid; my $dsn_ret = $msginfo->dsn_ret;
$mp->printf("MAIL FROM:%s\n", join(' ',
@@ -23545,7 +24703,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&mail_to_local_mailbox);
import Amavis::Conf qw(:platform c cr ca
@@ -23664,14 +24822,14 @@ sub mail_to_local_mailbox(@) {
: $1 eq 'm' ? $mail_id
: $1 eq 'n' ? $msginfo->log_id
: $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1) #,'-')
- : $1 eq '%' ? '%' : '%'.$1 }egs;
+ : $1 eq '%' ? '%' : '%'.$1 }gse;
# $mbxname = File::Spec->catfile($mbxname, $suggested_filename);
$mbxname = "$mbxname/$suggested_filename";
if ($quarantine_subdir_levels>=1 && !$explicitly_suggested_filename) {
# using a subdirectory structure to disperse quarantine files
local($1,$2); my $subdir = substr($mail_id, 0, 1);
$subdir=~/^[A-Z0-9]\z/i or die "Unexpected first char: $subdir";
- $mbxname =~ m{^ (.*/)? ([^/]+) \z}sx; my($path,$fname) = ($1,$2);
+ $mbxname =~ m{^ (.*/)? ([^/]+) \z}xs; my($path,$fname) = ($1,$2);
# $mbxname = File::Spec->catfile($path, $subdir, $fname);
$mbxname = "$path$subdir/$fname"; # resulting full filename
my $errn = stat("$path$subdir") ? 0 : 0+$!;
@@ -23913,10 +25071,10 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform);
- import Amavis::Util qw(ll do_log);
+ import Amavis::Util qw(ll do_log idn_to_ascii);
}
use Errno qw(EINTR EAGAIN);
@@ -23944,7 +25102,7 @@ sub new {
do_log(4,"Fingerprint query: [%s]:%s %s %s",
$src_ip, $src_port, $nonce, $service_method);
my $sock; my $query; my $query_sent = 0;
- # send an UDP query to p0f-analyzer
+ # send a UDP query to p0f-analyzer
$query = '['.$src_ip.']' . ($src_port==0 ? '' : ':'.$src_port);
if (defined $service_path) {
$sock = IO::Socket::UNIX->new(Type => SOCK_DGRAM, Peer => $service_path);
@@ -23953,7 +25111,8 @@ sub new {
} else { # assume an INET or INET6 protocol family
$sock = $io_socket_module_name->new(
Type => SOCK_DGRAM, Proto => 'udp',
- PeerAddr => $service_host, PeerPort => $service_port);
+ PeerAddr => idn_to_ascii($service_host),
+ PeerPort => $service_port);
$sock or do_log(0,"Can't create a socket [%s]:%s: %s",
$service_host, $service_port, $!);
}
@@ -24219,13 +25378,13 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::rfc2821_2822_Tools;
import Amavis::Util qw(ll do_log do_log_safe min max minmax untaint
- safe_encode_utf8 format_time_interval unique_list
- snmp_count);
+ safe_encode safe_encode_utf8 idn_to_ascii
+ format_time_interval unique_list snmp_count);
import Amavis::Lookup::IP qw(lookup_ip_acl normalize_ip_addr);
import Amavis::Timing qw(section_time);
}
@@ -24282,6 +25441,7 @@ sub connect {
undef $err;
eval {
my %opt = %options; delete @opt{qw(ttl db_id password)};
+ $opt{server} = idn_to_ascii($opt{server}) if defined $opt{server};
$r = Amavis::TinyRedis->new(on_connect => sub { $self->on_connect(@_) },
%opt);
$r or die "Error: $!";
@@ -24541,16 +25701,19 @@ sub save_structured_report {
return if !$report_ref;
$self->connect if !$self->{connected};
my $r = $self->{redis};
- my $report_json = safe_encode_utf8(Amavis::JSON::encode($report_ref));
+ my $report_json = Amavis::JSON::encode($report_ref); # as string of chars
+ # use safe_encode() instead of safe_encode_utf8() here, this way we ensure
+ # the resulting string of octets is always a valid UTF-8, even in case
+ # of a non-ASCII input string with utf8 flag off
+ $report_json = safe_encode('UTF-8',$report_json); # convert to octets
do_log(5, "redis: structured_report: %s %s", $log_key, $report_json);
$r->b_call("RPUSH", $log_key, $report_json);
# keep most recent - queue size limit in case noone is pulling events
$r->b_call("LTRIM", $log_key, -$queue_size_limit, -1) if $queue_size_limit;
my $res = $r->b_results; # errors will be signalled
- ll(5) && do_log(5, "redis: save_structured_report, ".
- "%d bytes, q_lim=%s, q_size=%s",
- length $report_json, $queue_size_limit || 0,
- $res ? join(', ',@$res) : '?');
+ do_log(5, "redis: save_structured_report, %d bytes, q_lim=%s, q_size=%s",
+ length $report_json, $queue_size_limit || 0,
+ $res ? join(', ',@$res) : '?') if ll(5);
1;
}
@@ -25024,7 +26187,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform c cr ca);
import Amavis::Util qw(ll do_log do_log_safe);
@@ -25305,15 +26468,15 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::rfc2821_2822_Tools;
- import Amavis::Util qw(ll do_log do_log_safe min max minmax
+ import Amavis::Util qw(ll do_log do_log_safe min max minmax add_entropy
untaint untaint_inplace format_time_interval
- add_entropy sanitize_str safe_decode
- safe_encode safe_encode_ascii safe_encode_utf8
- snmp_count orcpt_decode ccat_split ccat_maj);
+ truncate_utf_8 orcpt_encode idn_to_ascii
+ safe_encode safe_encode_utf8 safe_decode_mime
+ snmp_count ccat_split ccat_maj);
import Amavis::Lookup qw(lookup lookup2);
import Amavis::Out::SQL::Connection ();
}
@@ -25338,7 +26501,7 @@ sub find_or_save_addr {
my $naddr = untaint($addr);
if ($naddr ne '') { # normalize address (lowercase, 7-bit, max 255 ch...)
($localpart,$domain) = split_address($naddr);
- $domain =~ s/[^\040-\176]/?/gs; $domain = lc $domain;
+ $domain = idn_to_ascii($domain);
if (!$keep_localpart_case && !c('localpart_is_case_sensitive')) {
$localpart = lc($localpart);
}
@@ -25347,7 +26510,7 @@ sub find_or_save_addr {
$naddr = $localpart.'@'.$domain;
substr($naddr,255) = '' if length($naddr) > 255;
# avoid UTF-8 SQL trouble, legitimate RFC 5321 addresses only need 7 bits
- $naddr =~ s/[^\040-\176]/?/g if !$sql_allow_8bit_address;
+ $naddr =~ s/[^\040-\176]/?/gs if !$sql_allow_8bit_address;
# SQL character strings disallow zero octets, and also disallow any other
# octet values and sequences of octet values that are invalid according to
# the database's selected character set encoding
@@ -25440,7 +26603,7 @@ sub penpals_find {
$1 eq '%m' ? (map { my $s=$_; $s=~s/[^\040-\176]/?/gs; $s }
@$message_id_list)
: shift @args),
- $1 eq '%m' ? join(',', ('?') x $n) : '?' }gxe;
+ $1 eq '%m' ? join(',', ('?') x $n) : '?' }xgse;
# keep original clause taintedness
$sel_penpals_msgid = untaint($sel_penpals_msgid) . $sel_taint;
untaint_inplace($_) for @pos_args; # untaint arguments
@@ -25513,14 +26676,7 @@ sub save_info_preliminary {
my $addr_smtp = $r->recip_addr_smtp;
if (defined $addr_smtp) {
$addr_smtp =~ s/^<(.*)>\z/$1/s;
- $addr_smtp =~ s/(\@[^@]+)\z/lc $1/se; # lowercase just a domain part
- }
- my $orig_addr = $r->dsn_orcpt; # RCPT command ORCPT option, RFC 3461
- if (defined $orig_addr) {
- $orig_addr = orcpt_decode($orig_addr);
- $orig_addr =~ s/(\@[^@]+)\z/lc $1/se; # lowercase just a domain part
- } else {
- $orig_addr = $addr_smtp;
+ $addr_smtp = mail_addr_idn_to_ascii($addr_smtp);
}
my($rid, $o_rid, $existed);
if ($addr_smtp ne '') {
@@ -25530,24 +26686,13 @@ sub save_info_preliminary {
$r->recip_maddr_id($rid);
snmp_count('SqlAddrRecipAttempts');
snmp_count($existed ? 'SqlAddrRecipHits' : 'SqlAddrRecipMisses');
- do_log(4,"save_info_preliminary %s, recip id: %s, %s%s, %s",
- $mail_id, $rid, $addr_smtp,
- $orig_addr eq $addr_smtp ? '' : " (ORCPT $orig_addr)",
- $existed ? 'exists' : 'new');
- }
- }
-## currently disabled, probably not worth saving into SQL, rarely useful
-# if ($orig_addr ne '' && lc($orig_addr) ne lc($addr_smtp)) {
-# # don't bother saving as a separate record for just a case change
-# ($o_rid,$existed) = $self->find_or_save_addr($orig_addr,$partition_tag,1);
-# if (defined $o_rid) {
-# $r->recip_maddr_id_orig($o_rid);
-# snmp_count('SqlAddrRecipAttempts');
-# snmp_count($existed ? 'SqlAddrRecipHits' : 'SqlAddrRecipMisses');
-# do_log(4,"save_info_preliminary %s, o_recip id: %s, %s, %s",
-# $mail_id, $o_rid, $orig_addr, $existed ? 'exists' : 'new');
-# }
-# }
+ my($addr_type, $addr) = orcpt_encode($r->dsn_orcpt, 1);
+ ll(4) && do_log(4,"save_info_preliminary %s, recip id: %s, %s%s, %s",
+ $mail_id, $rid, $addr_smtp,
+ defined $addr ? " (ORCPT $addr_type;$addr)" : '',
+ $existed ? 'exists' : 'new');
+ }
+ }
}
my $conn_h = $self->{conn_h}; my $sql_cl_r = cr('sql_clause');
my $ins_msg = $sql_cl_r->{'ins_msg'};
@@ -25568,7 +26713,8 @@ sub save_info_preliminary {
$partition_tag, $msginfo->mail_id, $msginfo->secret_id,
$msginfo->log_id, int($msginfo->rx_time), $time_iso,
untaint($sid), c('policy_bank_path'), untaint($msginfo->client_addr),
- 0+untaint($msginfo->msg_size), untaint(substr(c('myhostname'),0,255)));
+ 0+untaint($msginfo->msg_size),
+ untaint(substr(idn_to_utf8(c('myhostname')),0,255)));
$conn_h->commit; 1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
@@ -25667,21 +26813,15 @@ sub save_info_final {
for ($subj,$from) { # character set decoding, sanitation
chomp; s/\n(?=[ \t])//gs; s/^[ \t]+//s; s/[ \t]+\z//s; # unfold, trim
eval { # convert to UTF-8 octets, truncate to 255 bytes
- local($1);
- my $chars = safe_decode('MIME-Header',$_); # logical characters
- my $octets = safe_encode_utf8($chars); # bytes, UTF-8 encoded
- if (length($octets) > 255 &&
- $octets =~ /^ (.{0,255}) (?= [\x00-\x7F\xC0-\xFF] | \z )/xs) {
- $octets = $1; # cleanly chop a UTF-8 byte sequence, RFC 3629
- $chars = safe_decode('UTF-8',$octets); # convert back to chars
- }
+ my $chars = safe_decode_mime($_); # to logical characters
+ my $octets = safe_encode_utf8($chars); # to bytes, UTF-8 encoded
+ $octets = truncate_utf_8($octets,255);
# man DBI: Drivers should accept [unicode and non-unicode] strings
# and, if required, convert them to the character set of the
# database being used. Similarly, when fetching from the database
# character data that isn't iso-8859-1 the driver should convert
# it into UTF-8.
- # $_ = $chars; 1; # pass logical characters to SQL
- $_ = $octets; 1; # pass bytes to SQL, works better
+ $_ = $octets; 1; # pass bytes to SQL, UTF-8, works better
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
do_log(1,"save_info_final INFO: header field ".
@@ -25711,7 +26851,7 @@ sub save_info_final {
"Message-ID: %s, From: '%s', Subject: '%s'",
$mail_id, $orig, $checks_performed, $content_type,
$q_type, $q_to, $dsn_sent, $min_spam_level,
- $m_id, sanitize_str($from), sanitize_str($subj));
+ $m_id, $from, $subj);
# update message record with additional information
$conn_h->execute($upd_msg,
$content_type, $q_type, $q_to, $dsn_sent,
@@ -25738,7 +26878,7 @@ sub save_info_final {
do_log(-1, "WARN save_info_final: %s", $eval_stat);
die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
return 0;
- }
+ };
}
1;
}
@@ -25761,7 +26901,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Util qw(ll do_log untaint min max minmax);
}
@@ -25833,7 +26973,7 @@ sub open {
sub DESTROY {
my $self = $_[0];
local($@,$!,$_); my $myactualpid = $$;
- if (ref $self && $self->{conn_h}) {
+ if ($self && $self->{conn_h}) {
eval {
$self->close or die "Error closing: $!"; 1;
} or do {
@@ -26060,7 +27200,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT = qw(&mail_via_sql);
import Amavis::Conf qw(:platform c cr ca $sql_quarantine_chunksize_max);
@@ -26197,7 +27337,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Util qw(ll untaint min max minmax unique_list do_log
@@ -26276,8 +27416,10 @@ sub clamav_module_internal($@) {
$options |= &Mail::ClamAV::CL_SCAN_STDOPT if $clamav_version >= 0.13;
$options |= $opt_archive; # turn on ARCHIVE
$options &= ~$opt_mail; # turn off MAIL
- if (ref($part) && ($part->type_short eq 'MAIL' ||
- lc($part->type_declared) eq 'message/rfc822')) {
+ my $type_decl = $part->type_declared;
+ if (ref $part &&
+ ($part->type_short eq 'MAIL' ||
+ defined $type_decl && $type_decl=~m{^message/(?:rfc822|global)\z}si)) {
do_log(2, "%s: $query - enabling option CL_MAIL", $av_name);
$options |= $opt_mail; # turn on MAIL
}
@@ -26376,8 +27518,10 @@ sub sophos_savi_internal {
if (!c('bypass_decode_parts')) {
my $part = $names_to_parts->{$query}; # get corresponding parts object
my $mime_option_value = 0;
- if (ref($part) && ($part->type_short eq 'MAIL' ||
- lc($part->type_declared) eq 'message/rfc822')) {
+ my $type_decl = $part->type_declared;
+ if (ref $part &&
+ ($part->type_short eq 'MAIL' ||
+ defined $type_decl && $type_decl=~m{^message/(?:rfc822|global)\z}si)){
do_log(2, "%s: %s - enabling option Mime", $av_name, $query);
$mime_option_value = 1;
}
@@ -26449,10 +27593,10 @@ sub sophos_sssp_internal {
my $output = '';
# normal timeout for reading a response
prolong_timer('sophos_sssp_scan');
- $sssp_handle->timeout(max(2, $deadline - Time::HiRes::time));
+ $sssp_handle->timeout(max(3, $deadline - Time::HiRes::time));
for my $fname (!ref($query) ? $query : @$query) {
my $fname_enc = $fname;
- $fname_enc =~ s/([%\000-\040\177\377])/sprintf("%%%02X",ord($1))/egs;
+ $fname_enc =~ s/([%\000-\040\177\377])/sprintf("%%%02X",ord($1))/gse;
$sssp_handle->print("SSSP/1.0 SCANDIRR $fname_enc\015\012")
or die "Error writing to sssp socket";
$sssp_handle->flush or die "Error flushing sssp socket";
@@ -26472,7 +27616,7 @@ sub sophos_sssp_internal {
$sssp_handle->print("BYE\015\012") or die "Error writing to sssp socket";
$sssp_handle->flush or die "Error flushing sssp socket";
- $sssp_handle->timeout(max(2, $deadline - Time::HiRes::time));
+ $sssp_handle->timeout(max(3, $deadline - Time::HiRes::time));
while (defined($ln = $sssp_handle->get_response_line)) {
do_log(5,"sssp response to BYE: %s", $ln);
last if $ln eq "\015\012" || $ln =~ /^BYE/;
@@ -26507,7 +27651,7 @@ sub avira_savapi_internal {
$ln =~ m{^100 SAVAPI:(\d+.*)\012\z}s or die "savapi bad greeting '$ln'";
# section_time('savapi-greet');
- $remaining_time = int(max(2, $deadline - Time::HiRes::time + 0.5));
+ $remaining_time = int(max(3, $deadline - Time::HiRes::time + 0.5));
for my $cmd ("SET PRODUCT $product_id",
"SET SCAN_TIMEOUT $remaining_time",
"SET CWD $tempdir/parts",
@@ -26524,7 +27668,7 @@ sub avira_savapi_internal {
# set a normal timeout for reading a response
prolong_timer('avira_savapi_scan');
- $savapi_handle->timeout(max(2, $deadline - Time::HiRes::time));
+ $savapi_handle->timeout(max(3, $deadline - Time::HiRes::time));
my $keep_one_success; my $output = '';
for my $fname (!ref($query) ? $query : @$query) {
my $cmd = "SCAN $fname"; # files only, no directories
@@ -26571,7 +27715,7 @@ sub clamav_clamd_internal {
# set a normal timeout
prolong_timer('clamav_scan');
- $clamav_handle->timeout(max(2, $deadline - Time::HiRes::time));
+ $clamav_handle->timeout(max(3, $deadline - Time::HiRes::time));
$clamav_handle->print("zIDSESSION\0")
or die "Error writing 'zIDSESSION' to a clamd socket: $!";
@@ -26685,6 +27829,7 @@ sub av_smtp_client($$$$) {
$test_msg->body_digest($msginfo->body_digest); # copy original digest
$test_msg->dsn_ret($msginfo->dsn_ret);
$test_msg->dsn_envid($msginfo->dsn_envid);
+ $test_msg->smtputf8($msginfo->smtputf8);
$test_msg->sender($msginfo->sender); # original sender
$test_msg->sender_smtp($msginfo->sender_smtp);
$test_msg->auth_submitter($msginfo->sender_smtp);
@@ -26753,7 +27898,7 @@ sub ask_daemon_internal {
# normal timeout for reading a response
prolong_timer('ask_daemon_internal_scan');
- $sock->timeout(max(2, $deadline - Time::HiRes::time));
+ $sock->timeout(max(3, $deadline - Time::HiRes::time));
if ($multisession) {
# depends on TCP segment boundaries, unreliable
my $nread = $sock->read($output,16384);
@@ -26964,7 +28109,7 @@ sub run_av(@) {
{ $1 eq '{}' ? "$tempdir/parts"
: $1 eq '{}/*' ? ($multi=1,"$tempdir/parts/$f")
: $1 eq '*' ? ($multi=1,$f) : $1
- }gesx for @query_expanded;
+ }xgse for @query_expanded;
} else {
# collect as many filename arguments as suitable, but at least one
my $arg_size = 0;
@@ -27360,7 +28505,7 @@ use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform c cr ca);
import Amavis::Util qw(ll do_log min max minmax untaint untaint_inplace
@@ -27762,7 +28907,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars :sa c cr ca);
import Amavis::Util qw(ll do_log sanitize_str min max minmax
@@ -27869,7 +29014,7 @@ sub check_or_learn {
or die "Parent failed to close child side of the pipe2: $!";
undef $child_stdout_fh; undef $child_stderr_fh;
- my($remaining_time, $deadline) = get_deadline($scanner_name.'_scan', 0.9, 5);
+ my($remaining_time, $deadline) = get_deadline($scanner_name.'_scan', 0.8, 5);
alarm(0); # stop the timer
my $proc_fd = fileno($proc_fh);
my $resp_stdout_fd = fileno($resp_stdout_fh);
@@ -27901,7 +29046,7 @@ sub check_or_learn {
vec($win,$proc_fd,1) = 1 if defined $proc_fh &&
(!$eof_on_msg || $data_source ne '');
$ein = $rin | $win;
- my $timeout = max(2, $deadline - Time::HiRes::time);
+ my $timeout = max(3, $deadline - Time::HiRes::time);
my($nfound,$timeleft) =
select($rout=$rin, $wout=$win, $eout=$ein, $timeout);
defined $nfound && $nfound >= 0
@@ -28216,7 +29361,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars :sa c cr ca);
import Amavis::Util qw(ll do_log sanitize_str min max minmax get_deadline);
@@ -28280,7 +29425,7 @@ sub check {
my $spamd_handle = Amavis::IO::RW->new(
[ '127.0.0.1:783', '[::1]:783' ], Eol => "\015\012", Timeout => 10);
defined $spamd_handle or die "Can't connect to spamd, $@ ($!)";
- $spamd_handle->timeout(max(2, $deadline - Time::HiRes::time));
+ $spamd_handle->timeout(max(3, $deadline - Time::HiRes::time));
section_time($which_section);
$which_section = 'spamd_tx'; do_log(4,"sending to spamd");
@@ -28326,7 +29471,7 @@ sub check {
$bytes_written += length($buff);
last if $done;
}
- } elsif ($msg->isa('MIME::Entity')) { # TODO - cont. length won't match!
+ } elsif ($msg->isa('MIME::Entity')) { # TODO - content length won't match!
do_log(3,"spamc: message is MIME::Entity, size won't match");
$msg->print_body($spamd_handle);
} else {
@@ -28428,7 +29573,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
# let a 'require' understand that this module is already loaded:
$INC{'Mail/SpamAssassin/Logger/Amavislog.pm'} = 'amavisd';
@@ -28465,11 +29610,12 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars :sa $daemon_user c cr ca);
import Amavis::Util qw(ll do_log do_log_safe sanitize_str prolong_timer
- add_entropy min max minmax get_deadline);
+ add_entropy min max minmax get_deadline
+ safe_encode_utf8);
import Amavis::ProcControl qw(exit_status_str proc_status_ok
kill_proc run_command run_as_subprocess
collect_results collect_results_structured);
@@ -29184,7 +30330,11 @@ sub call_spamassassin($$$$) {
DKIMDOMAIN DKIMIDENTITY AWLSIGNERMEAN
CRM114STATUS CRM114SCORE CRM114CACHEID)) {
my $tag_value = $per_msg_status->get_tag($t);
- $supplementary_info{$t} = $tag_value if defined $tag_value;
+ if (defined $tag_value) {
+ # for some reason tags ASN and ASNCIDR have UTF8 flag on;
+ # encode any character strings to UTF-8 octets for consistency
+ $supplementary_info{$t} = safe_encode_utf8($tag_value);
+ }
}
}
{ # fudge
@@ -29195,9 +30345,10 @@ sub call_spamassassin($$$$) {
sprintf("%s ( %s )", $crm114_status,$crm114_score);
}
}
- $spam_summary = $per_msg_status->get_report; # taints $1 and $2 !
- # $spam_summary = $per_msg_status->get_tag('SUMMARY');
- $spam_report = $per_msg_status->get_tag('REPORT');
+ # get_report() taints $1 and $2 !
+ $spam_summary = safe_encode_utf8($per_msg_status->get_report);
+ # $spam_summary = safe_encode_utf8($per_msg_status->get_tag('SUMMARY'));
+ $spam_report = safe_encode_utf8($per_msg_status->get_tag('REPORT'));
# fetch the TIMING tag last:
$supplementary_info{'TIMING'} = $per_msg_status->get_tag('TIMING');
$supplementary_info{'RUSAGE-SA'} = \@sa_cpu_usage; # filled-in later
@@ -29238,7 +30389,8 @@ sub call_spamassassin($$$$) {
if ($$ != $saved_pid) {
do_log_safe(-2,"PANIC, SA checking produced a clone process ".
"of [%s], CLONE [%s] SELF-TERMINATING", $saved_pid,$$);
- POSIX::_exit(6); # avoid END and destructor processing
+ POSIX::_exit(3); # SIGQUIT, avoid END and destructor processing
+ # POSIX::_exit(6); # SIGABRT, avoid END and destructor processing
}
if ($rusage_self_before && $rusage_children_before) {
@@ -29320,7 +30472,7 @@ sub check {
# header fields, but may be extended in sub call_spamassassin() by
# reading-in the rest of the message; this may or may not happen in
# a separate process (called through run_as_subprocess or directly);
- # lines must each be terminated by a \n character, which must be the
+ # each line must be terminated by a \n character, which must be the
# only \n in a line;
#
my(@lines) = split(/^/m, $prefix, -1); $prefix = undef;
@@ -29398,15 +30550,16 @@ sub check {
if $msginfo->spam_summary ne '';
}
$msginfo->spam_report($spam_report); $msginfo->spam_summary($spam_summary);
- for (keys %$supplementary_info_ref)
- { $msginfo->supplementary_info($_, $supplementary_info_ref->{$_}) }
-
+ for (keys %$supplementary_info_ref) {
+ $msginfo->supplementary_info($_, $supplementary_info_ref->{$_});
+ }
if (defined $eval_stat) { # SA timed out?
kill_proc($pid,'a spawned SA',1,$proc_fh,$eval_stat) if defined $pid;
undef $proc_fh; undef $pid; chomp $eval_stat;
do_log(-2, "SA failed: %s", $eval_stat);
# die "$eval_stat\n" if $eval_stat !~ /timed out\b/;
}
+ 1;
}
1;
@@ -29423,7 +30576,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&init &decompose_part &determine_file_types);
import Amavis::Util qw(untaint min max minmax ll do_log snmp_count
@@ -29436,6 +30589,19 @@ BEGIN {
import Amavis::Unpackers::MIME qw(mime_decode);
import Amavis::Unpackers::NewFilename qw(consumed_bytes);
}
+
+BEGIN {
+ use vars qw($filemagic);
+ eval {
+ require File::LibMagic;
+ File::LibMagic->VERSION(1.00);
+ import File::LibMagic;
+ $filemagic = File::LibMagic->new;
+ } or do {
+ undef $filemagic;
+ };
+}
+
use subs @EXPORT_OK;
use Errno qw(ENOENT EACCES EINTR EAGAIN);
@@ -29529,10 +30695,57 @@ sub flatten_and_tidy_dir($$$;$$) {
($consumed_bytes, $item_num);
}
+sub determine_file_types($$) {
+ my($tempdir, $partslist_ref) = @_;
+ if ($filemagic) {
+ determine_file_types_libmagic($tempdir, $partslist_ref);
+ } elsif (defined $file && $file ne '') {
+ determine_file_types_fileutility($tempdir, $partslist_ref);
+ } else {
+ die "Neither File::LibMagic nor Unix utility file(1) are available";
+ }
+}
+
+# associate full and short file content types with each part
+# based on libmagic (uses File::LibMagic module)
+#
+sub determine_file_types_libmagic($$) {
+ my($tempdir, $partslist_ref) = @_;
+ my(@all_part_list) = grep($_->exists, @$partslist_ref);
+ my $initial_num_parts = scalar(@all_part_list);
+ do_log(5, 'using File::LibMagic on %d files', $initial_num_parts);
+ for my $part (@all_part_list) {
+ my($type_long, $type_short);
+ eval {
+ $type_long = $filemagic->describe_filename($part->full_name);
+ 1;
+ } or do {
+ my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
+ do_log(0, 'File::LibMagic::describe_filename failed on %s: %s',
+ $part->base_name, $eval_stat);
+ };
+ if (defined $type_long) {
+ $type_short = lookup2(0,$type_long,\@map_full_type_to_short_type_maps);
+ ll(4) && do_log(4, "File-type of %s: %s%s",
+ $part->base_name, $type_long,
+ (!defined $type_short ? ''
+ : !ref $type_short ? "; ($type_short)"
+ : '; (' . join(', ',@$type_short) . ')'
+ ) );
+ $part->type_long($type_long); $part->type_short($type_short);
+ $part->attributes_add('C')
+ if !ref($type_short) ? $type_short eq 'pgp.enc' # encrypted?
+ : grep($_ eq 'pgp.enc', @$type_short);
+ }
+ }
+ section_time(sprintf('get-file-type%d', $initial_num_parts));
+ 1;
+}
+
# call 'file(1)' utility for each part,
-# and associate (save) full and short file content types with each part
+# and associate full and short file content types with each part
#
-sub determine_file_types($$) {
+sub determine_file_types_fileutility($$) {
my($tempdir, $partslist_ref) = @_;
defined $file && $file ne ''
or die "Unix utility file(1) not available, but is needed";
@@ -29622,6 +30835,7 @@ sub determine_file_types($$) {
do_log(-2, "file(1) utility (%s) FAILED: %s", $file,$eval_stat);
# die "file(1) utility ($file) error: $eval_stat";
}
+ 1;
}
sub decompose_mail($$) {
@@ -30989,7 +32203,8 @@ sub do_cabextract($$$) {
local($1,$2); my $bytes = 0; my $ln; my $entries_cnt = 0;
for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
chomp($ln);
- next if $ln =~ /^(File size|----|Viewing cabinet:|\z)/;
+ next if $ln =~ /^(?: ?File size|----|Viewing cabinet:|\z)/s;
+ next if $ln =~ /^\s*All done, no errors/s;
if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
{ die "Maximum number of files ($MAXFILES) exceeded" }
if ($ln !~ /^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) {
@@ -31041,10 +32256,10 @@ sub do_executable($$@) {
ll(4) && do_log(4,"Check whether %s is a self-extracting archive",
$part->base_name);
- # ZIP?
- return 2 if eval { do_unzip($part,$tempdir,undef,1) };
- chomp $@;
- do_log(3, "do_executable: not a ZIP sfx, ignoring: %s", $@) if $@ ne '';
+# # ZIP?
+# return 2 if eval { do_unzip($part,$tempdir,undef,1) };
+# chomp $@;
+# do_log(3, "do_executable: not a ZIP sfx, ignoring: %s", $@) if $@ ne '';
# RAR?
return 2 if defined $unrar && eval { do_unrar($part,$tempdir,$unrar,1) };
@@ -31061,7 +32276,7 @@ sub do_executable($$@) {
chomp $@;
do_log(3, "do_executable: not an ARJ sfx, ignoring: %s", $@) if $@ ne '';
- return 0;
+ 0;
}
# my($k,$v,$fn);
@@ -31085,7 +32300,7 @@ sub run_command_copy($$$) {
eval {
my($nread, $nwrite, $tosend, $offset, $inbuf);
for (;;) {
- $nread = sysread($ifh, $inbuf, 32768);
+ $nread = sysread($ifh, $inbuf, 65536);
if (!defined($nread)) {
if ($! == EAGAIN || $! == EINTR) {
Time::HiRes::sleep(0.1); # just in case
@@ -31204,7 +32419,7 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&dkim_key_postprocess &generate_authentication_results
&dkim_make_signatures &adjust_score_by_signer_reputation
@@ -31213,7 +32428,9 @@ BEGIN {
%dkim_signing_keys_by_domain
@dkim_signing_keys_list @dkim_signing_keys_storage);
import Amavis::Util qw(min max minmax untaint ll do_log unique_list
- format_time_interval get_deadline proto_encode proto_decode);
+ format_time_interval get_deadline
+ idn_to_ascii mail_addr_idn_to_ascii idn_to_utf8
+ safe_encode_utf8 proto_encode proto_decode);
import Amavis::rfc2821_2822_Tools qw(split_address quote_rfc2821_local
qquote_rfc2821_local);
import Amavis::Timing qw(section_time);
@@ -31265,8 +32482,8 @@ sub dkim_key_postprocess() {
my $any_wild; my $j = 0;
for my $ent (@dkim_signing_keys_list) {
$ent->{v} = 'DKIM1' if !defined $ent->{v}; # provide a default
- if (defined $ent->{n}) { # encode n as qp-section (RFC 4871, RFC 2047)
- $ent->{n} =~ s{([\000-\037\177=;"])}{sprintf('=%02X',ord($1))}egs;
+ if (defined $ent->{n}) { # encode n as qp-section (RFC 6376, RFC 2047)
+ $ent->{n} =~ s{([\000-\037\177=;"])}{sprintf('=%02X',ord($1))}gse;
}
my $domain = $ent->{domain};
if (exists $ent->{g}) {
@@ -31283,7 +32500,8 @@ sub dkim_key_postprocess() {
my $regexp = $domain;
$regexp =~ s/\*{2,}/*/gs; # collapse successive wildcards
# '*' is a wildcard, quote the rest
- $regexp =~ s{ ([@\#/.^\$|*+?(){}\[\]\\]) }{$1 eq '*' ? '.*' : '\\'.$1}gex;
+ $regexp =~ s{ ([@\#/.^\$|*+?(){}\[\]\\]) }
+ { $1 eq '*' ? '.*' : '\\'.$1 }xgse;
$regexp = '^' . $regexp . '\\z'; # implicit anchors
$regexp =~ s/^\^\.\*//s; # remove leading anchor if redundant
$regexp =~ s/\.\*\\z\z//s; # remove trailing anchor if redundant
@@ -31350,8 +32568,8 @@ sub get_dkim_key(@) {
my $domain = $options{d}; my $selector = $options{s};
defined $domain && $domain ne ''
or die "get_dkim_key: domain is required, but tag 'd' is missing";
- $domain = lc($domain);
- $selector = lc($selector) if defined $selector;
+ $domain = idn_to_ascii($domain);
+ $selector = idn_to_ascii($selector) if defined $selector;
my(@indices) = $dkim_signing_keys_by_domain{$domain} ?
@{$dkim_signing_keys_by_domain{$domain}} :
$dkim_signing_keys_by_domain{'*'} ?
@@ -31366,6 +32584,8 @@ sub get_dkim_key(@) {
!defined($options{i}) ? () : split_address($options{i});
$identity_localpart = '' if !defined $identity_localpart;
$identity_domain = '' if !defined $identity_domain;
+ $identity_domain =
+ idn_to_ascii($identity_domain) if $identity_domain ne '';
# find the first key (associated with a domain) with compatible options
for my $j (@indices) {
my $ent = $dkim_signing_keys_list[$j];
@@ -31378,7 +32598,7 @@ sub get_dkim_key(@) {
next if defined $hashalg && exists $ent->{'h'} &&
!(grep($_ eq $hashalg, split(/:/, $ent->{'h'})) );
if (defined($options{i})) {
- if (lc($identity_domain) eq $domain) {
+ if ($identity_domain eq $domain) {
# ok
} elsif (exists $ent->{t} && (grep($_ eq 's', split(/:/,$ent->{t})))) {
next; # no subdomains allowed
@@ -31515,9 +32735,8 @@ sub remote_signer {
# die "Can't sign, $reason, query: " . join('; ', at query) . "\n";
do_log(0, "dkim: can't sign, %s, query: %s", $reason, join('; ', at query));
return ''; # Mail::DKIM::Algorithm::rsa_sha256 doesn't like undef
- } else {
- return decode_base64($b); # resulting signature
}
+ decode_base64($b); # resulting signature
}
# prepare requested DKIM signatures for a provided message,
@@ -31527,7 +32746,7 @@ sub dkim_make_signatures($$;$) {
my($msginfo,$initial_submission,$callback) = @_;
my(@signatures); # resulting signature objects
my(%sig_options); # signature options and constraints for choosing a key
- my(%key_options); # options associated with a signing key
+ my(%key_options); # options associated with a signing key, IDN as ACE
my(@tried_domains); # used for logging a failure
my($chosen_addr,$chosen_addr_src); my $do_sign = 0;
my $fm = $msginfo->rfc2822_from; # authors
@@ -31561,7 +32780,7 @@ sub dkim_make_signatures($$;$) {
# Resent-Sender, Resent-From, Sender, From.
# Only a signature based on 2822.From is considered an author domain
# signature, others are just third-party signatures and have no more
- # merit than any other third-party signature according to RFC 4871.
+ # merit than any other third-party signature according to RFC 6376.
#
my $rf = $msginfo->rfc2822_resent_from;
my $rs = $msginfo->rfc2822_resent_sender;
@@ -31625,8 +32844,9 @@ sub dkim_make_signatures($$;$) {
let_signing_service_choose($dkim_signing_service,
$msginfo, \@search_list, undef);
if ($sig_opt_ref) { # merge returned signature options with ours
- while (my($k,$v) = each(%$sig_opt_ref))
- { $sig_options{$k} = $v if defined $v }
+ while (my($k,$v) = each(%$sig_opt_ref)) {
+ $sig_options{$k} = $v if defined $v;
+ }
}
}
@@ -31635,7 +32855,6 @@ sub dkim_make_signatures($$;$) {
for my $pair (@search_list) {
my($addr,$addr_src) = @$pair;
my($addr_localpart,$addr_domain) = split_address($addr);
- $addr_domain = lc($addr_domain);
# fetch a list of hashes from all entries matching the address
my($dkim_options_ref,$mk_ref);
($dkim_options_ref,$mk_ref) = lookup2(1,$addr,$sobm) if $sobm && @$sobm;
@@ -31655,8 +32874,9 @@ sub dkim_make_signatures($$;$) {
}
}
# a default for a signing domain is a domain of each tried address
- if (!exists($tmp_sig_options{d}))
- { my $d = $addr_domain; $d =~ s/^\@//; $tmp_sig_options{d} = $d }
+ if (!exists($tmp_sig_options{d})) {
+ my $d = $addr_domain; $d =~ s/^\@//; $tmp_sig_options{d} = $d;
+ }
push(@tried_domains, $tmp_sig_options{d});
ll(5) && do_log(5, "dkim: signature options for %s(%s): %s",
$addr, $addr_src,
@@ -31679,13 +32899,14 @@ sub dkim_make_signatures($$;$) {
# last if defined $key_options{key};
# }
my $key = $key_options{key};
- if (defined $key && $key ne '') { # found; copy the key and its options
+ if (defined $key && $key ne '') { # found; copy the key and its options
$tmp_sig_options{key} = $key;
- $tmp_sig_options{s} = $key_options{selector};
+ $tmp_sig_options{s} = idn_to_utf8($key_options{selector});
$chosen_addr = $addr; $chosen_addr_src = $addr_src;
# merge the just collected signature options into the final set
- while (my($k,$v) = each(%tmp_sig_options))
- { $sig_options{$k} = $v if defined $v }
+ while (my($k,$v) = each(%tmp_sig_options)) {
+ $sig_options{$k} = $v if defined $v;
+ }
last;
}
}
@@ -31708,12 +32929,15 @@ sub dkim_make_signatures($$;$) {
# let Mail::DKIM use our custom code for signing (pref. 0.38 or later)
$key_options{key} = Amavis::DKIM::CustomSigner->new(
CustomSigner => \&remote_signer, MsgInfo => $msginfo,
- Selector => $s, Domain => $d, Server => $dkim_signing_service);
+ Selector => idn_to_ascii($s),
+ Domain => idn_to_ascii($d),
+ Server => $dkim_signing_service);
$key_options{selector} = $s; $key_options{domain} = $d;
$sig_options{key} = $key_options{key};
}
- if (!defined $sig_options{d} || $sig_options{d} eq '') {
+ my $sig_opt_d_ace = idn_to_ascii($sig_options{d});
+ if (!defined $sig_opt_d_ace || $sig_opt_d_ace eq '') {
do_log(2, "dkim: not signing, empty signing domain, From: %s",$from_str);
} elsif (!defined $sig_options{key} || $sig_options{key} eq '') {
do_log(2, "dkim: not signing, no applicable private key for domains %s,".
@@ -31721,8 +32945,9 @@ sub dkim_make_signatures($$;$) {
join(", ", at tried_domains), $sig_options{s}, $from_str);
} else {
# copy key's options to signature options for convenience
- for (keys %key_options)
- { $sig_options{'KEY.'.$_} = $key_options{$_} if /^[ghknst]\z/ }
+ for (keys %key_options) {
+ $sig_options{'KEY.'.$_} = $key_options{$_} if /^[ghknst]\z/;
+ }
$sig_options{'KEY.key_ind'} = $key_options{key_ind};
# check matching of identity to a signing domain or provide a default;
@@ -31739,8 +32964,8 @@ sub dkim_make_signatures($$;$) {
# provide default for i in a form of a sender's domain
local($1);
if ($chosen_addr =~ /\@([^\@]*)\z/) {
- my $identity_domain = lc($1);
- if ($identity_domain =~ /.\.\Q$sig_options{d}\E\z/si) {
+ my $identity_domain = $1;
+ if (idn_to_ascii($identity_domain) =~ /.\.\Q$sig_opt_d_ace\E\z/s) {
$sig_options{i} = '@'.$identity_domain;
do_log(5, "dkim: identity defaults to %s", $sig_options{i});
}
@@ -31751,14 +32976,14 @@ sub dkim_make_signatures($$;$) {
} else { # check if the requested i is compatible with d
local($1);
my $identity_domain = $sig_options{i} =~ /\@([^\@]*)\z/ ? $1 : '';
- if (!$key_allows_subdomains &&
- lc($identity_domain) ne lc($sig_options{d})) {
+ my $identity_domain_ace = idn_to_ascii($identity_domain);
+ if (!$key_allows_subdomains && $identity_domain_ace ne $sig_opt_d_ace){
do_log(2, "dkim: not signing, identity domain %s not the same as ".
"a signing domain %s, flags t=%s, From: %s",
$sig_options{i}, $sig_options{d}, $sig_options{'KEY.t'},
$from_str);
} elsif ($key_allows_subdomains &&
- $identity_domain !~ /(?:^|\.)\Q$sig_options{d}\E\z/i) {
+ $identity_domain_ace !~ /(?:^|\.)\Q$sig_opt_d_ace\E\z/i) {
do_log(2, "dkim: not signing, identity %s not a subdomain of %s, ".
"From: %s", $sig_options{i}, $sig_options{d}, $from_str);
} else {
@@ -31767,14 +32992,15 @@ sub dkim_make_signatures($$;$) {
}
}
}
+ my $sig_opt_d_ace = idn_to_ascii($sig_options{d});
if ($do_sign) { # avoid adding same signature on multiple passes through MTA
my $sigs_ref = $msginfo->dkim_signatures_valid;
if ($sigs_ref) {
for my $sig (@$sigs_ref) {
- if ( lc($sig_options{d}) eq lc($sig->domain) &&
+ if ( idn_to_ascii($sig->domain) eq $sig_opt_d_ace &&
(!defined $sig_options{i} || $sig_options{i} eq $sig->identity)) {
do_log(2, "dkim: not signing, already signed by domain %s, ".
- "From: %s", $sig_options{d}, $from_str);
+ "From: %s", $sig_opt_d_ace, $from_str);
$do_sign = 0;
}
}
@@ -31786,19 +33012,22 @@ sub dkim_make_signatures($$;$) {
my $xt = $msginfo->rx_time + $sig_options{ttl};
$sig_options{x} = int($xt) + ($xt > int($xt) ? 1 : 0); # ceiling
}
- # remove redundant options with RFC 4871 -default values
+ # remove redundant options with RFC 6376 -default values
for my $k (keys %sig_options) { delete $sig_options{$k} if !defined $k }
- delete $sig_options{i} if lc($sig_options{i}) eq '@'.lc($sig_options{d});
+ delete $sig_options{i} if $sig_options{i} =~ /^\@/ &&
+ idn_to_ascii($sig_options{i}) eq '@'.$sig_opt_d_ace;
delete $sig_options{c} if $sig_options{c} eq 'simple/simple' ||
$sig_options{c} eq 'simple';
delete $sig_options{q} if $sig_options{q} eq 'dns/txt';
if (ref $callback eq 'CODE') { &$callback($msginfo,\%sig_options) }
if (ll(2)) {
- my $opts = join(', ', map($_ eq 'key' ? () : ($_.'=>'.$sig_options{$_}),
- sort keys %sig_options));
+ my $opts = join(', ',map($_ eq 'key' ? ()
+ : ($_ . '=>' . safe_encode_utf8($sig_options{$_})),
+ sort keys %sig_options));
do_log(2,"dkim: signing (%s), From: %s (%s:%s), %s",
- grep(/\@\Q$sig_options{d}\E\z/si, @rfc2822_from) ? 'author'
- : '3rd-party',
+ grep(/\@\Q$sig_opt_d_ace\E\z/si,
+ map(mail_addr_idn_to_ascii($_), @rfc2822_from))
+ ? 'author' : '3rd-party',
$from_str, $chosen_addr_src, qquote_rfc2821_local($chosen_addr),
$opts);
}
@@ -31862,16 +33091,24 @@ sub dkim_make_signatures($$;$) {
my $j = int($expiration);
$expiration = $expiration > $j ? $j+1 : $j; # ceiling
}
+ # RFC 6531 section 3.2: Any domain name to be looked up in the DNS
+ # MUST conform to and be processed as specified for Internationalizing
+ # Domain Names in Applications (IDNA) [RFC5890]. When doing lookups,
+ # the SMTPUTF8-aware SMTP client or server MUST either use a Unicode-
+ # aware DNS library, or transform the internationalized domain name
+ # to A-label form (i.e., a fully- qualified domain name that contains
+ # one or more A-labels but no U-labels) as specified in RFC 5890.
$dkim->add_signature( Mail::DKIM::Signature->new(
- Selector => $sig_options{s},
- Domain => $sig_options{d},
+ Selector => idn_to_ascii($sig_options{s}),
+ Domain => idn_to_ascii($sig_options{d}),
Timestamp => int($msginfo->rx_time), # floor
Headers => join(':', reverse @field_names_to_be_signed),
Key => $key,
!defined $sig_options{c} ? () : (Method => $sig_options{c}),
!defined $sig_options{a} ? () : (Algorithm => $sig_options{a}),
!defined $sig_options{q} ? () : (Query => $sig_options{q}),
- !defined $sig_options{i} ? () : (Identity => $sig_options{i}),
+ !defined $sig_options{i} ? () : (Identity =>
+ mail_addr_idn_to_ascii($sig_options{i})),
!defined $expiration ? () : (Expiration => $expiration), # ceiling
));
undef;
@@ -31959,6 +33196,7 @@ sub generate_authentication_results($;$$) {
$sigs_ref = $msginfo->dkim_signatures_all if @_ < 3; # for all by default
my $authservid = c('myauthservid');
$authservid = c('myhostname') if !defined $authservid || $authservid eq '';
+ $authservid = idn_to_ascii($authservid);
# note that RFC 5451 declares A-R header field as structured, which is why
# we are inserting a \n into top-level locations suitable for folding,
# and let sub hdr() choose suitable folding points
@@ -32011,8 +33249,7 @@ sub generate_authentication_results($;$$) {
: $sig_result eq 'fail' ? 'fail'
: $sig_result eq 'invalid' ? 'neutral' : 'permerror';
}
- my $d = $sig->domain;
- $d = lc $d if defined $d;
+ my $sdid_ace = idn_to_ascii($sig->domain);
my $str = '';
my $add_header_b; # RFC 6008, should we add a header.b for this signature?
my $key_size = eval {
@@ -32024,7 +33261,7 @@ sub generate_authentication_results($;$$) {
my $rfc2822_sender = $msginfo->rfc2822_sender;
my $fm = $msginfo->rfc2822_from;
my(@rfc2822_from) = !defined($fm) ? () : ref $fm ? @$fm : $fm;
- my $id = defined $d ? '@'.$d : '';
+ my $id_ace = defined $sdid_ace ? '@'.$sdid_ace : '';
$str .= ";\n domainkeys=" . $result_val;
$str .= sprintf(' (%d-bit key)', $key_size) if $key_size;
if (defined $details && $details ne '' && lc $details ne lc $result_val){
@@ -32032,13 +33269,13 @@ sub generate_authentication_results($;$$) {
$details =~ s{([\000-\037\177"\\])}{\\$1}gs; # RFC 5322 qtext
$str .= "\n reason=\"$details\"";
}
- if (@rfc2822_from &&
- $rfc2822_from[0] =~ /(\@[^\@]*)\z/s && lc($1) eq $id) {
+ if (@rfc2822_from && $rfc2822_from[0] =~ /(\@[^\@]*)\z/s &&
+ idn_to_ascii($1) eq $id_ace) {
$str .= "\n header.from=" .
join(',', map(quote_rfc2821_local($_), @rfc2822_from));
}
- if (defined($rfc2822_sender) &&
- $rfc2822_sender =~ /(\@[^\@]*)\z/s && lc($1) eq $id) {
+ if (defined($rfc2822_sender) && $rfc2822_sender =~ /(\@[^\@]*)\z/s &&
+ idn_to_ascii($1) eq $id_ace) {
$str .= "\n header.sender=" . quote_rfc2821_local($rfc2822_sender);
}
} else { # a DKIM signature
@@ -32052,7 +33289,7 @@ sub generate_authentication_results($;$$) {
}
}
- $str .= "\n header.d=" . $d if defined $d;
+ $str .= "\n header.d=" . $sdid_ace if defined $sdid_ace;
my $b = $sig->data;
if (defined $b && $add_header_b) {
# RFC 6008: The value associated with this item in the header field
@@ -32103,7 +33340,7 @@ sub adjust_score_by_signer_reputation($) {
my $srm = ca('signer_reputation_maps');
# walk through all valid signatures, find best (smallest) reputation value
for my $sig (@$sigs_ref) {
- my $sdid = lc $sig->domain;
+ my $sdid = $sig->domain;
my($val,$key) = lookup2(0, '@'.$sdid, $srm);
if (defined $val &&
(!defined $best_reputation_score || $val < $best_reputation_score)) {
@@ -32201,19 +33438,19 @@ sub collect_some_dkim_info($) {
$key_size||0 );
}
- my $sdid = lc $sig->domain;
# See if a signature matches address in any of the sender/author fields.
# In the absence of an explicit Sender header field, the first author
# acts as the 'agent responsible for the transmission of the message'.
my(@addr_list) = ($msginfo->sender,
defined $rfc2822_sender ? $rfc2822_sender : $rfc2822_from[0],
@rfc2822_from);
+ my $sdid_ace = idn_to_ascii($sig->domain);
for my $addr (@addr_list) {
next if !defined $addr;
local($1); my $domain;
- $domain = lc($1) if $addr =~ /\@([^\@]*)\z/s;
+ $domain = $1 if $addr =~ /\@([^\@]*)\z/s;
# turn addresses in @addr_list into booleans, representing match outcome
- $addr = defined($domain) && $domain eq $sdid ? 1 : 0;
+ $addr = defined $domain && idn_to_ascii($domain) eq $sdid_ace ? 1 : 0;
}
# # Label which header fields are covered by each signature;
@@ -32253,15 +33490,16 @@ sub collect_some_dkim_info($) {
$msginfo->dkim_thirdparty_sig($sig_domain) if !$msginfo->dkim_author_sig;
if (@$atpbm) { # any author to policy bank name mappings?
for my $j (0..$#rfc2822_from) { # for each author (usually only one)
- my $key = $rfc2822_from[$j];
+ my $key_ace = mail_addr_idn_to_ascii($rfc2822_from[$j]);
# query key: as-is author address for author domain signatures, and
# author address with '/@signer-domain' appended for 3rd party sign.
# e.g.: 'user at example.com', 'user at sub.example.com/@example.org'
- for my $opt ( ($addr_list[$j+2] ? '' : ()), '/@'.lc($sig->domain) ) {
- next if $bn_auth_already_queried{$key.$opt};
- my($result,$matchingkey) = lookup2(0,$key,$atpbm,
+ my $sdid_ace = idn_to_ascii($sig->domain);
+ for my $opt ( ($addr_list[$j+2] ? '' : ()), '/@'.$sdid_ace ) {
+ next if $bn_auth_already_queried{$key_ace.$opt};
+ my($result,$matchingkey) = lookup2(0,$key_ace,$atpbm,
Label=>'AuthToPB', $opt eq '' ? () : (AppendStr=>$opt));
- $bn_auth_already_queried{$key.$opt} = 1;
+ $bn_auth_already_queried{$key_ace.$opt} = 1;
if ($result) {
if ($result eq '1') {
# a handy usability trick to supply a hardwired policy bank
@@ -32350,13 +33588,14 @@ no warnings 'uninitialized';
BEGIN {
require Exporter;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
- $VERSION = '2.321';
+ $VERSION = '2.403';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&show_or_test_dkim_public_keys &generate_dkim_private_key
&convert_dkim_keys_file);
import Amavis::Conf qw(:platform c cr ca
@dkim_signing_keys_list @dkim_signing_keys_storage);
- import Amavis::Util qw(untaint ll do_log);
+ import Amavis::Util qw(untaint ll do_log
+ safe_encode_utf8 idn_to_ascii idn_to_utf8);
import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp);
}
use subs @EXPORT_OK;
@@ -32378,7 +33617,8 @@ use Crypt::OpenSSL::RSA ();
#
sub show_or_test_dkim_public_keys($$) {
my($cmd,$args) = @_;
- my(@seek_domains) = @$args; # when list is empty all domains are implied
+ # when list is empty all domains are implied
+ my(@seek_domains) = map(idn_to_ascii($_), @$args);
my(@sort_list) = map { my $d = lc($dkim_signing_keys_list[$_]->{domain});
my $d_re = $dkim_signing_keys_list[$_]->{domain_re};
[$_, $d, $d_re, join('.',reverse split(/\./,$d,-1))] }
@@ -32389,16 +33629,21 @@ sub show_or_test_dkim_public_keys($$) {
my $any = 0;
for my $e (@sort_list) {
my($j,$domain,$domain_re) = @$e; local($1);
+ $domain = safe_encode_utf8($domain);
+ my $domain_ace = idn_to_ascii($domain);
next if @seek_domains &&
!grep { defined $domain_re ? lc($_) =~ /$domain_re/
: /^\.(.*)\z/s ?
- $domain eq lc($1) || $domain =~ /(?:\.|\z)\Q$1\E\z/si
- : $domain eq lc($_) } @seek_domains;
+ $domain_ace eq lc($1) ||
+ $domain_ace =~ /(?:\.|\z)\Q$1\E\z/si
+ : $domain_ace eq lc($_) } @seek_domains;
$any++;
my $key_opts = $dkim_signing_keys_list[$j];
if ($cmd eq 'testkeys' || $cmd eq 'testkey') {
test_dkim_key(%$key_opts);
} else {
+ my $selector = safe_encode_utf8($key_opts->{selector});
+ my $selector_ace = idn_to_ascii($selector);
my $key_storage_ind = $key_opts->{key_storage_ind};
my($key,$dev,$inode,$fname) =
@{ $dkim_signing_keys_storage[$key_storage_ind] };
@@ -32406,12 +33651,15 @@ sub show_or_test_dkim_public_keys($$) {
@pub = grep(!/^---.*?---\z/ && !/^[ \t]*\z/, @pub);
my(@tags) = map($_.'='.$key_opts->{$_},
grep(defined $key_opts->{$_}, qw(v g h k s t n)));
- printf("; key#%d, domain %s, %s\n",
- $key_opts->{key_ind} + 1, $domain, $fname) if defined $fname;
+ my $key_size = 8 * $key->size;
+ printf("; key#%d %d bits, i=%s, d=%s%s\n",
+ $key_opts->{key_ind} + 1, $key_size,
+ $selector, $domain,
+ defined $fname ? ', '.$fname : '');
printf("; CANNOT DECLARE A WILDCARDED LABEL IN DNS, ".
"AVOID OR EDIT MANUALLY!\n") if defined $key_opts->{domain_re};
printf("%s._domainkey.%s.\t%s TXT (%s)\n\n",
- $key_opts->{selector}, $domain, '3600',
+ $selector_ace, $domain_ace, '3600',
join('', map("\n" . ' "' . $_ . '"',
join('; ', at tags,'p='), @pub)) );
}
@@ -32435,17 +33683,20 @@ sub test_dkim_key(@) {
# $pkcs1 =~ s/^---.*?---(?:\r?\n|\z)//gm; $pkcs1 =~ tr/\r\n//d;
# $key = Mail::DKIM::PrivateKey->load(Data => $pkcs1);
}
+ my $domain = idn_to_utf8($key_options{domain});
+ my $domain_ace = idn_to_ascii($domain);
+ my $selector_ace = idn_to_ascii($key_options{selector});
my $policyfn = sub {
my $dkim = $_[0];
$dkim->add_signature( Mail::DKIM::Signature->new(
- Selector => $key_options{selector}, Domain => $key_options{domain},
+ Selector => $selector_ace, Domain => $domain_ace,
Method => 'simple/simple', Algorithm => 'rsa-sha256',
Timestamp => int($now), Expiration => int($now)+24*3600, Key => $key,
)); undef;
};
my $msg = sprintf(
"From: test\@%s\nMessage-ID: <123\@%s>\nDate: %s\nSubject: test\n\ntest\n",
- $key_options{domain}, $key_options{domain}, rfc2822_timestamp($now));
+ $domain, $domain, rfc2822_timestamp($now));
$msg =~ s{\n}{\015\012}gs;
my(@gen_signatures, @read_signatures);
eval {
@@ -32470,10 +33721,10 @@ sub test_dkim_key(@) {
print STDERR "dkim verification failed: $eval_stat\n";
};
# printf("%s\n", $fname) if defined $fname;
- printf("TESTING#%d: %-33s => %s\n", $key_options{key_ind} + 1,
+ printf("TESTING#%d %s: %s => %s\n",
+ $key_options{key_ind} + 1, $domain,
$_->selector . '._domainkey.' . $_->domain,
$_->result_detail) for @read_signatures;
-
}
sub generate_dkim_private_key(@) {
@@ -32562,11 +33813,11 @@ sub convert_dkim_keys_file($) {
($sender_localpart, $sender_domain) =
Amavis::rfc2821_2822_Tools::split_address(
Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($sender_pattern));
- $sender_domain =~ s/^\@//; $sender_domain = lc($sender_domain);
- $sender_pattern = $sender_localpart . '@' . $sender_domain;
+ $sender_domain =~ s/^\@//;
+ $sender_pattern = $sender_localpart.'@'.idn_to_ascii($sender_domain);
}
if ($signing_domain eq '*') { $signing_domain = $sender_domain }
- $signing_domain = lc($signing_domain);
+ $signing_domain = idn_to_ascii($signing_domain);
if ($signing_domain ne '' &&
!$domain_selectors{$signing_domain}{$selector}) {
# dkim_key($signing_domain,$selector,$key_fn); # declare a signing key
@@ -32592,7 +33843,7 @@ sub convert_dkim_keys_file($) {
} else {
$sender_pattern =~ s/\*{2,}/*/gs; # collapse successive wildcards
$sender_pattern =~ # '*' is a wildcard, quote the rest
- s{ ([@\#/.^\$|*+?(){}\[\]\\]) }{ $1 eq '*' ? '.*' : '\\'.$1 }gex;
+ s{ ([@\#/.^\$|*+?(){}\[\]\\]) }{ $1 eq '*' ? '.*' : '\\'.$1 }xgse;
$sender_pattern = '^' . $sender_pattern . '\\z'; # implicit anchors
# remove trailing first, leading next, preferring /^.*\z/ -> /^/, not /\z/
$sender_pattern =~ s/\.\*\\z\z//s; # remove trailing anchor if redundant
@@ -32637,19 +33888,19 @@ __DATA__
OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
UNCHECKED[?[:ccat|minor]||-ENCRYPTED|]|BANNED (%F)|INFECTED (%V)]#
{[:actions_performed]}#
-,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] %s -> [%D|,]#
+,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] [:mail_addr_decode_octets|%s] -> [%D|[:mail_addr_decode_octets|%D]|,]#
[? %q ||, quarantine: %q]#
[? %Q ||, Queue-ID: %Q]#
-[? %m ||, Message-ID: %m]#
-[? %r ||, Resent-Message-ID: %r]#
+[? %m ||, Message-ID: [:mail_addr_decode_octets|%m]]#
+[? %r ||, Resent-Message-ID: [:mail_addr_decode_octets|%r]]#
[? %i ||, mail_id: %i]#
, Hits: [:SCORE]#
, size: %z#
[? [:partition_tag] ||, pt: [:partition_tag]]#
[~[:remote_mta_smtp_response]|["^$"]||[", queued_as: "]]\
[remote_mta_smtp_response|[~%x|["queued as ([0-9A-Za-z]+)$"]|["%1"]|["%0"]]|/]#
-#, Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
-#, From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
+#, Subject: [:dquote|[:mime2utf8|[:header_field_octets|Subject]|100|1]]#
+#, From: [:uquote|[:mail_addr_decode_octets|[:rfc2822_from]]]#
#[? %#T ||, Tests: \[[%T|,]\]]#
[? [:dkim|sig_sd] ||, dkim_sd=[:dkim|sig_sd]]#
[? [:dkim|newsig_sd] ||, dkim_new=[:dkim|newsig_sd]]#
@@ -32660,17 +33911,17 @@ UNCHECKED[?[:ccat|minor]||-ENCRYPTED|]|BANNED (%F)|INFECTED (%V)]#
OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
UNCHECKED[?[:ccat|minor]||-ENCRYPTED|]|BANNED (%F)|INFECTED (%V)]#
{[:actions_performed]}#
-,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] %s -> [%O|,]#
+,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] [:mail_addr_decode_octets|%s] -> [%O|[:mail_addr_decode_octets|%O]|,]#
[? %q ||, quarantine: %q]#
[? %Q ||, Queue-ID: %Q]#
-[? %m ||, Message-ID: %m]#
-[? %r ||, Resent-Message-ID: %r]#
+[? %m ||, Message-ID: [:mail_addr_decode_octets|%m]]#
+[? %r ||, Resent-Message-ID: [:mail_addr_decode_octets|%r]]#
[? %i ||, mail_id: %i]#
, Hits: [:SCORE]#
, size: %z#
[? [:partition_tag] ||, pt: [:partition_tag]]#
-#, Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
-#, From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
+#, Subject: [:dquote|[:mime2utf8|[:header_field_octets|Subject]|100|1]]#
+#, From: [:uquote|[:mail_addr_decode_octets|[:rfc2822_from]]]#
#[? %#T ||, Tests: \[[%T|,]\]]#
[? [:dkim|sig_sd] ||, dkim_sd=[:dkim|sig_sd]]#
[? [:dkim|newsig_sd] ||, dkim_new=[:dkim|newsig_sd]]#
@@ -32688,13 +33939,14 @@ __DATA__
OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
UNCHECKED[?[:ccat|minor]||-ENCRYPTED|]|BANNED (%F)|INFECTED (%V)]#
{[:actions_performed]}#
-,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] %s -> [%D|,]#
-, ([ip_trace_public|%x| < ])#
+,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] [:client_protocol]/[:protocol] [:mail_addr_decode_octets|%s] -> [%D|[:mail_addr_decode_octets|%D]|,]#
+#, ([ip_trace_public|%x| < ])#
+, ([ip_proto_trace_public|%x| < ])#
[? [:tls_in] ||, tls: [:tls_in]]#
[? %q ||, quarantine: %q]#
[? %Q ||, Queue-ID: %Q]#
-[? %m ||, Message-ID: %m]#
-[? %r ||, Resent-Message-ID: %r]#
+[? %m ||, Message-ID: [:mail_addr_decode_octets|%m]]#
+[? %r ||, Resent-Message-ID: [:mail_addr_decode_octets|%r]]#
, mail_id: %i#
#, secret_id: [:secret_id]#
, b: [:substr|[:b64urlenc|[:body_digest]]|0|9]#
@@ -32703,8 +33955,8 @@ UNCHECKED[?[:ccat|minor]||-ENCRYPTED|]|BANNED (%F)|INFECTED (%V)]#
[? [:partition_tag] ||, pt: [:partition_tag]]#
[~[:remote_mta_smtp_response]|["^$"]||[", queued_as: "]]\
[remote_mta_smtp_response|[~%x|["queued as ([0-9A-Za-z]+)$"]|["%1"]|["%0"]]|/]#
-, Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
-, From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
+, Subject: [:dquote|[:mime2utf8|[:header_field_octets|Subject]|100|1]]#
+, From: [:uquote|[:mail_addr_decode_octets|[:rfc2822_from]]]#
[? [:dkim|author] || (dkim:AUTHOR)]#
[? [:useragent|name] ||, [:useragent|name]: [:uquote|[:useragent|body]]]#
, helo=[:client_helo]#
@@ -32736,21 +33988,22 @@ UNCHECKED[?[:ccat|minor]||-ENCRYPTED|]|BANNED (%F)|INFECTED (%V)]#
OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
UNCHECKED[?[:ccat|minor]||-ENCRYPTED|]|BANNED (%F)|INFECTED (%V)]#
{[:actions_performed]}#
-,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] %s -> [%O|,]#
-, ([ip_trace_public|%x| < ])#
+,[?%p|| %p][?%a||[?%l|| LOCAL] [:client_addr_port]][?%e|| \[%e\]] [:client_protocol]/[:protocol] [:mail_addr_decode_octets|%s] -> [%O|[:mail_addr_decode_octets|%O]|,]#
+#, ([ip_trace_public|%x| < ])#
+, ([ip_proto_trace_public|%x| < ])#
[? [:tls_in] ||, tls: [:tls_in]]#
[? %q ||, quarantine: %q]#
[? %Q ||, Queue-ID: %Q]#
-[? %m ||, Message-ID: %m]#
-[? %r ||, Resent-Message-ID: %r]#
+[? %m ||, Message-ID: [:mail_addr_decode_octets|%m]]#
+[? %r ||, Resent-Message-ID: [:mail_addr_decode_octets|%r]]#
, mail_id: %i#
#, secret_id: [:secret_id]#
, b: [:substr|[:b64urlenc|[:body_digest]]|0|9]#
, Hits: [:SCORE]#
, size: %z#
[? [:partition_tag] ||, pt: [:partition_tag]]#
-, Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
-, From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
+, Subject: [:dquote|[:mime2utf8|[:header_field_octets|Subject]|100|1]]#
+, From: [:uquote|[:mail_addr_decode_octets|[:rfc2822_from]]]#
[? [:dkim|author] || (dkim:AUTHOR)]#
[? [:useragent|name] ||, [:useragent|name]: [:uquote|[:useragent|body]]]#
, helo=[:client_helo]#
@@ -32791,7 +34044,7 @@ __DATA__
#([:ccat|name|main]) #
[? [:ccat|major] |OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER|SPAMMY|SPAM|\
UNCHECKED|BANNED (%F)|INFECTED (%V)]#
-, %s -> [%D|,], Hits: %c#
+, [:mail_addr_decode_octets|%s] -> [%D|[:mail_addr_decode_octets|%D]|,], Hits: %c#
, tag=[:tag_level], tag2=[:tag2_level], kill=[:kill_level]#
[~[:remote_mta_smtp_response]|["^$"]||\
["queued as ([0-9A-Za-z]+)"]|[", queued_as: %1"]|[", fwd: %0"]]#
@@ -32802,7 +34055,7 @@ UNCHECKED|BANNED (%F)|INFECTED (%V)]#
[? [:ccat|major|blocking] |#
OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER|SPAMMY|SPAM|\
UNCHECKED|BANNED (%F)|INFECTED (%V)]#
-, %s -> [%O|,], Hits: %c#
+, [:mail_addr_decode_octets|%s] -> [%D|[:mail_addr_decode_octets|%D]|,], Hits: %c#
, tag=[:tag_level], tag2=[:tag2_level], kill=[:kill_level]#
, %0/%1/%2/%k#
]
@@ -32831,11 +34084,14 @@ Subject: [?%#D|Undeliverable mail|Delivery status notification]\
]
Message-ID: <DSN%i@%h>
-[? %#D |#|Your message WAS SUCCESSFULLY RELAYED to:[\n %D]
+[? %#D |#|Your message WAS SUCCESSFULLY RELAYED to:\
+[%D|\n [:mail_addr_decode|%D]|]
+
[~[:dsn_notify]|["\\bSUCCESS\\b"]|\
and you explicitly requested a delivery status notification on success.\n]\
]
-[? %#N |#|The message WAS NOT relayed to:[\n %N]
+[? %#N |#|The message WAS NOT relayed to:\
+[%N|\n [:mail_addr_decode|%N]|]
]
[:wrap|78|||This [?%#D|nondelivery|delivery] report was \
generated by the program amavisd-new at host %h. \
@@ -32846,7 +34102,7 @@ Our internal reference code for your message is %n/%i]
[? [:explain_badh] ||[? [:ccat|minor]
|INVALID HEADER
|INVALID HEADER: BAD MIME HEADER SECTION OR BAD MIME STRUCTURE
-|INVALID HEADER: INVALID 8-BIT CHARACTERS IN HEADER SECTION
+|INVALID HEADER: INVALID NON-ASCII CHARACTERS IN HEADER SECTION
|INVALID HEADER: INVALID CONTROL CHARACTERS IN HEADER SECTION
|INVALID HEADER: FOLDED HEADER FIELD LINE MADE UP ENTIRELY OF WHITESPACE
|INVALID HEADER: HEADER LINE LONGER THAN RFC 5322 LIMIT OF 998 CHARACTERS
@@ -32858,15 +34114,16 @@ Our internal reference code for your message is %n/%i]
[[:wrap|78| | |%X]\n]
]\
#
-[:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
-[:wrap|78|| |From: [:header_field|From|100][?[:dkim|author]|| (dkim:AUTHOR)]]
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s][?[:dkim|envsender]|| (OK)]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
[? [:header_field|Sender]|#|\
-[:wrap|78|| |Sender: [:header_field|Sender|100]\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
[?[:dkim|sender]|| (dkim:SENDER)]]]
-[? %m |#|[:wrap|78|| |Message-ID: %m]]
-[? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
+[? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+[? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
[? %#X|#|[? [:useragent] |#|[:wrap|78|| |[:useragent]]]]
-[? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
# ccat_min 0: other, 1: bad MIME, 2: 8-bit char, 3: NUL/CR,
# 4: empty, 5: long, 6: syntax, 7: missing, 8: multiple
@@ -32880,15 +34137,19 @@ WHAT IS AN INVALID CHARACTER IN A MAIL HEADER SECTION?
It does not allow the use of characters with codes above 127 to be
used directly (non-encoded) in a mail header section.
- If such characters (e.g. with diacritics) from ISO Latin or other
- alphabets need to be included in a header section, these characters
- need to be properly encoded according to RFC 2047. Such encoding
- is often done transparently by mail reader (MUA), but if automatic
- encoding is not available (e.g. by some older MUA) it is a user's
- responsibility to avoid using such characters in a header section,
- or to encode them manually. Typically the offending header fields
- in this category are 'Subject', 'Organization', and comment fields
- or display names in e-mail addresses of 'From', 'To' or 'Cc'.
+ If such characters (e.g. with diacritics, or non-Latin) from UTF-8
+ or other character set need to be included in a message header
+ section, such message needs to be submitted to an SMTPUTF8-capable
+ mailer (RFC 6532), or these characters need to be properly encoded
+ according to RFC 2047.
+
+ Necessary encoding is often done transparently by a mail reader,
+ but if automatic encoding is not available (e.g. by some older MUA)
+ it is a user's responsibility to avoid using such characters in
+ a header section, or to encode them manually. Typically offending
+ header fields in this category are 'Subject', 'Organization', and
+ comment fields or display names in e-mail addresses of 'From',
+ 'To', or 'Cc'.
Sometimes such invalid header fields are inserted automatically
by some MUA, MTA, content filter, or other mail handling service.
@@ -32961,7 +34222,7 @@ Subject: [? [:ccat|major]
|BANNED contents from you (%F)\
|VIRUS in message apparently from you (%V)\
]
-[? %m |#|In-Reply-To: %m]
+[? %m |#|In-Reply-To: [:mail_addr_decode|%m]]
Message-ID: <VS%i@%h>
[? [:ccat|major] |Clean|Clean|MTA-BLOCKED|OVERSIZED|INVALID HEADER|\
@@ -32972,23 +34233,25 @@ Our content checker found
[? %#F |#|[:wrap|78| | |banned [? %#F |names|name|names]: %F]]
[? %#X |#|[[:wrap|78| | |%X]\n]]
-in email presumably from you %s
-to the following [? %#R |recipients|recipient|recipients]:[
--> %R]
+in email presumably from you [:mail_addr_decode|%s]
+to the following [? %#R |recipients|recipient|recipients]:\
+[%R|\n-> [:mail_addr_decode|%R]|]
Our internal reference code for your message is %n/%i
[? %a |#|[:wrap|78|| |First upstream SMTP client IP address: [:client_addr_port] %g]]
-[:wrap|78|| |Received from: [ip_trace_all|%x| < ]]
-[:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
-[:wrap|78|| |From: [:header_field|From|100][?[:dkim|author]|| (dkim:AUTHOR)]]
+[:wrap|78|| |Received trace: [ip_proto_trace_all|%x| < ]]
+
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s][?[:dkim|envsender]|| (OK)]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
[? [:header_field|Sender]|#|\
-[:wrap|78|| |Sender: [:header_field|Sender|100]\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
[?[:dkim|sender]|| (dkim:SENDER)]]]
-[? %m |#|[:wrap|78|| |Message-ID: %m]]
-[? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
-[? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
+[? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+[? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
[? %#D |Delivery of the email was stopped!
@@ -33004,7 +34267,7 @@ or MIME type or contents type violating our access policy.
To transfer contents that may be considered risky or unwanted
by site policies, or simply too large for mailing, please consider
-publishing your content on the web, and only sending an URL of the
+publishing your content on the web, and only sending a URL of the
document to the recipient.
Depending on the recipient and sender site policies, with a little
@@ -33044,7 +34307,7 @@ Date: %d
Subject: [? [:ccat|major] |Clean mail|Clean mail|MTA-blocked mail|\
OVERSIZED mail|INVALID HEADER in mail|Spammy|Spam|UNCHECKED contents in mail|\
BANNED contents (%F) in mail|VIRUS (%V) in mail]\
- FROM [?%l||LOCAL ][?%a||[:client_addr_port] ]%s
+ FROM [?%l||LOCAL ][?%a||[:client_addr_port] ][:mail_addr_decode|%s]
To: [? %#T |undisclosed-recipients:;|[%T|, ]]
[? %#C |#|Cc: [%C|, ]]
Message-ID: <VA%i@%h>
@@ -33065,24 +34328,26 @@ Content type: [:ccat|name|main]#
Internal reference code for the message is %n/%i
[? %a |#|[:wrap|78|| |First upstream SMTP client IP address: [:client_addr_port] %g]]
-[:wrap|78|| |Received from: [ip_trace_all|%x| < ]]
-[:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
-[:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
+[:wrap|78|| |Received trace: [ip_proto_trace_all|%x| < ]]
+
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s][?[:dkim|envsender]|| (OK)]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
[? [:header_field|Sender]|#|\
-[:wrap|78|| |Sender: [:header_field|Sender]\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
[?[:dkim|sender]|| (dkim:SENDER)]]]
-[? %m |#|[:wrap|78|| |Message-ID: %m]]
-[? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
-[? %j |#|[:wrap|78|| |Subject: %j]]
+[? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+[? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
[? %q |Not quarantined.|The message has been quarantined as: %q]
[? %#S |Notification to sender will not be mailed.
]#
-[? %#D |#|The message WILL BE relayed to:[\n%D]
+[? %#D |#|The message WILL BE relayed to:[%D|\n[:mail_addr_decode|%D]|]
]
-[? %#N |#|The message WAS NOT relayed to:[\n%N]
+[? %#N |#|The message WAS NOT relayed to:[%N|\n[:mail_addr_decode|%N]|]
]
[? %#V |#|[? %#v |#|Virus scanner output:[\n %v]
]]
@@ -33097,7 +34362,7 @@ From: %f
Date: %d
Subject: [? [:ccat|major] |Clean mail|Clean mail|MTA-blocked mail|\
OVERSIZED mail|INVALID HEADER in mail|Spammy|Spam|UNCHECKED contents in mail|\
-BANNED contents (%F) in mail|VIRUS (%V) in mail] TO YOU from %s
+BANNED contents (%F) in mail|VIRUS (%V) in mail] TO YOU from [:mail_addr_decode|%s]
[? [:header_field|To] |To: undisclosed-recipients:;|To: [:header_field|To]]
[? [:header_field|Cc] |#|Cc: [:header_field|Cc]]
Message-ID: <VR%i@%h>
@@ -33110,25 +34375,27 @@ Our content checker found
[? %#X |#|[[:wrap|78| | |%X]\n]]
in an email to you [? %#V |from:|from probably faked sender:]
- %o
-[? %#V |#|claiming to be: %s]
+ [:mail_addr_decode|%o]
+[? %#V |#|claiming to be: [:mail_addr_decode|%s]]
Content type: [:ccat|name|main]#
[? [:ccat|is_blocked_by_nonmain] ||, blocked for [:ccat|name]]
Our internal reference code for your message is %n/%i
[? %a |#|[:wrap|78|| |First upstream SMTP client IP address: [:client_addr_port] %g]]
-[:wrap|78|| |Received from: [ip_trace_all|%x| < ]]
-[:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
-[:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
+[:wrap|78|| |Received trace: [ip_proto_trace_all|%x| < ]]
+
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s][?[:dkim|envsender]|| (OK)]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
[? [:header_field|Sender]|#|\
-[:wrap|78|| |Sender: [:header_field|Sender]\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
[?[:dkim|sender]|| (dkim:SENDER)]]]
-[? %m |#|[:wrap|78|| |Message-ID: %m]]
-[? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
+[? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+[? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
[? [:useragent] |#|[:wrap|78|| |[:useragent]]]
-[? %j |#|[:wrap|78|| |Subject: %j]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
[? %q |Not quarantined.|The message has been quarantined as: %q]
Please contact your system administrator for details.
@@ -33141,11 +34408,11 @@ __DATA__
# Long header fields will be automatically wrapped by the program.
#
Subject: Considered UNSOLICITED BULK EMAIL, apparently from you
-[? %m |#|In-Reply-To: %m]
+[? %m |#|In-Reply-To: [:mail_addr_decode|%m]]
Message-ID: <SS%i@%h>
-A message from %s[
-to: %R]
+A message from [:mail_addr_decode|%s]\
+[%R|\nto: [:mail_addr_decode|%R]|]
was considered unsolicited bulk e-mail (UBE).
@@ -33161,17 +34428,19 @@ losing genuine mail and sending undesired backscatter is sought,
and there can be some collateral damage on either side.
[? %a |#|[:wrap|78|| |First upstream SMTP client IP address: [:client_addr_port] %g]]
-[:wrap|78|| |Received from: [ip_trace_all|%x| < ]]
-[:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
-[:wrap|78|| |From: [:header_field|From|100][?[:dkim|author]|| (dkim:AUTHOR)]]
+[:wrap|78|| |Received trace: [ip_proto_trace_all|%x| < ]]
+
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s][?[:dkim|envsender]|| (OK)]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
[? [:header_field|Sender]|#|\
-[:wrap|78|| |Sender: [:header_field|Sender|100]\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
[?[:dkim|sender]|| (dkim:SENDER)]]]
-[? %m |#|[:wrap|78|| |Message-ID: %m]]
-[? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
+[? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+[? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
# [? [:useragent] |#|[:wrap|78|| |[:useragent]]]
-[? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
[? %#X |#|\n[[:wrap|78|| |%X]\n]]
[? %#D |Delivery of the email was stopped!
@@ -33189,7 +34458,7 @@ __DATA__
#
From: %f
Date: %d
-Subject: Spam FROM [?%l||LOCAL ][?%a||[:client_addr_port] ]%s
+Subject: Spam FROM [?%l||LOCAL ][?%a||[:client_addr_port] ][:mail_addr_decode|%s]
To: [? %#T |undisclosed-recipients:;|[%T|, ]]
[? %#C |#|Cc: [%C|, ]]
Message-ID: <SA%i@%h>
@@ -33199,22 +34468,24 @@ Content type: [:ccat|name|main]#
Internal reference code for the message is %n/%i
[? %a |#|[:wrap|78|| |First upstream SMTP client IP address: [:client_addr_port] %g]]
-[:wrap|78|| |Received from: [ip_trace_all|%x| < ]]
-[:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
-[:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
+[:wrap|78|| |Received trace: [ip_proto_trace_all|%x| < ]]
+
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s][?[:dkim|envsender]|| (OK)]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
[? [:header_field|Sender]|#|\
-[:wrap|78|| |Sender: [:header_field|Sender]\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
[?[:dkim|sender]|| (dkim:SENDER)]]]
-[? %m |#|[:wrap|78|| |Message-ID: %m]]
-[? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
+[? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+[? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
[? [:useragent] |#|[:wrap|78|| |[:useragent]]]
-[? %j |#|[:wrap|78|| |Subject: %j]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
[? %q |Not quarantined.|The message has been quarantined as: %q]
-[? %#D |#|The message WILL BE relayed to:[\n%D]
+[? %#D |#|The message WILL BE relayed to:[%D|\n[:mail_addr_decode|%D]|]
]
-[? %#N |#|The message WAS NOT relayed to:[\n%N]
+[? %#N |#|The message WAS NOT relayed to:[%N|\n[:mail_addr_decode|%N]|]
]
Spam scanner report:
[%A
@@ -33235,15 +34506,16 @@ Message-ID: <QRA%i@%h>
Please find attached a message which was held in a quarantine,
and has now been released.
-[:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
-[:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s][?[:dkim|envsender]|| (OK)]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
[? [:header_field|Sender]|#|\
-[:wrap|78|| |Sender: [:header_field|Sender]\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
[?[:dkim|sender]|| (dkim:SENDER)]]]
-# [? %m |#|[:wrap|78|| |Message-ID: %m]]
-# [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
+# [? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+# [? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
# [? [:useragent] |#|[:wrap|78|| |[:useragent]]]
-[? %j |#|[:wrap|78|| |Subject: %j]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
Our internal reference code for the message is %n/%i
#
@@ -33284,12 +34556,15 @@ Message-ID: <ARF%i@%h>
This is an e-mail [:feedback_type] report for a message \
[? %a |\nreceived on %d,|received from\nIP address [:client_addr_port] on %d,]
-[:wrap|78|| |Return-Path: %s]
-[:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
-[? [:header_field|Sender]|#|[:wrap|78|| |Sender: [:header_field|Sender]]]
-[? %m |#|[:wrap|78|| |Message-ID: %m]]
-[? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
-[? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
+[:wrap|78|| |Return-Path: [:mail_addr_decode|%s]]
+[:wrap|78|| |From: [:mime_decode|[:header_field_octets|From]|100]\
+[?[:dkim|author]|| (dkim:AUTHOR)]]
+[? [:header_field|Sender]|#|\
+[:wrap|78|| |Sender: [:mime_decode|[:header_field_octets|Sender]|100]\
+[?[:dkim|sender]|| (dkim:SENDER)]]]
+[? %m |#|[:wrap|78|| |Message-ID: [:mail_addr_decode|%m]]]
+[? %r |#|[:wrap|78|| |Resent-Message-ID: [:mail_addr_decode|%r]]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
[?[:dkim|author]|#|
A first-party DKIM or DomainKeys signature is valid, d=[:dkim|author].]
@@ -33316,14 +34591,14 @@ To: [? %#T |undisclosed-recipients:;|[%T|, ]]
Reply-To: postmaster@%h
Message-ID: <ARE%i@%h>
Auto-Submitted: auto-replied
-[:wrap|76||\t|Subject: Auto: autoresponse to: %s]
-[? %m |#|In-Reply-To: %m]
+[:wrap|76||\t|Subject: Auto: autoresponse to: [:mail_addr_decode|%s]]
+[? %m |#|In-Reply-To: [:mail_addr_decode|%m]]
Precedence: junk
This is an auto-response to a message \
[? %a |\nreceived on %d,|received from\nIP address [:client_addr_port] on %d,]
-envelope sender: %s
+envelope sender: [:mail_addr_decode|%s]
(author) From: [:rfc2822_from]
-[? %j |#|[:wrap|78|| |Subject: %j]]
+[? %j |#|[:wrap|78|| |Subject: [:mime_decode|[:header_field_octets|Subject]|100]]]
[?[:dkim|author]|#|
A first-party DKIM or DomainKeys signature is valid, d=[:dkim|author].]
diff --git a/amavisd-new-courier.patch b/amavisd-new-courier.patch
index 5cda457..67be687 100644
--- a/amavisd-new-courier.patch
+++ b/amavisd-new-courier.patch
@@ -1,5 +1,5 @@
---- amavisd.ori 2014-06-27 19:43:59.240215903 +0200
-+++ amavisd 2014-06-27 19:44:18.402213378 +0200
+--- amavisd.ori 2014-10-22 19:11:04.558937249 +0200
++++ amavisd 2014-10-22 19:11:12.611936745 +0200
@@ -108,5 +108,5 @@
# Amavis::In::AMPDP
# Amavis::In::SMTP
@@ -14,7 +14,7 @@
+ IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Select
IO::Stringy Digest::MD5 Unix::Syslog File::Basename
Compress::Zlib MIME::Base64 MIME::QuotedPrint MIME::Words
-@@ -11852,4 +11852,18 @@
+@@ -12672,4 +12672,18 @@
### Net::Server hook
+### This hook takes place immediately after the "->run()" method is called.
@@ -33,14 +33,14 @@
+### Net::Server hook
### Occurs in the parent (master) process after (possibly) opening a log file,
### creating pid file, reopening STDIN/STDOUT to /dev/null and daemonizing;
-@@ -11857,5 +11871,5 @@
+@@ -12677,5 +12691,5 @@
#
sub post_configure_hook {
-# umask(0007); # affect protection of Unix sockets created by Net::Server
+ umask(0007); # affect protection of Unix sockets created by Net::Server
}
-@@ -11876,9 +11890,18 @@
+@@ -12696,9 +12710,18 @@
### Net::Server hook
### Occurs in the parent (master) process after binding to sockets,
-### but before chrooting and dropping privileges
@@ -61,7 +61,7 @@
+ }
}
-@@ -11936,4 +11959,15 @@
+@@ -12756,4 +12779,15 @@
}
$spamcontrol_obj->init_pre_fork if $spamcontrol_obj;
+ if ($courierfilter_shutdown) {
@@ -77,7 +77,7 @@
+ }
my(@modules_extra) = grep(!exists $modules_basic{$_}, keys %INC);
if (@modules_extra) {
-@@ -12405,5 +12439,7 @@
+@@ -13229,5 +13263,7 @@
$ampdp_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
} elsif ($suggested_protocol eq 'COURIER') {
- die "unavailable support for protocol: $suggested_protocol";
@@ -86,7 +86,7 @@
+ $courier_in_obj->process_courier_request($sock, $conn, \&check_mail);
} elsif ($suggested_protocol eq 'QMQPqq') {
die "unavailable support for protocol: $suggested_protocol";
-@@ -12513,4 +12549,24 @@
+@@ -13337,4 +13373,24 @@
}
+### Net::Server hook
@@ -111,7 +111,7 @@
+
### Child is about to be terminated
### user customizable Net::Server hook
-@@ -17687,4 +17743,9 @@
+@@ -18592,4 +18648,9 @@
undef $Amavis::Conf::log_verbose_templ;
+# courierfilter shutdown needs can_read_hook, added in Net::Server 0.90
@@ -121,14 +121,14 @@
+
if (defined $desired_user && $daemon_user ne '') {
local($1);
-@@ -18340,4 +18401,6 @@
+@@ -19246,4 +19307,6 @@
host => $bind_to[0], # default bind, redundant, merged to @listen_sockets
listen => $listen_queue_size, # undef for a default
+ # need to set multi_port for can_read_hook
+ multi_port => $courierfilter_shutdown ? 1 : undef,
max_servers => $max_servers, # number of pre-forked children
!defined($min_servers) ? ()
-@@ -21826,5 +21889,424 @@
+@@ -22854,5 +22917,424 @@
no warnings 'uninitialized';
-BEGIN { die "Code not available for module Amavis::In::Courier" }
diff --git a/amavisd-new-qmqpqq.patch b/amavisd-new-qmqpqq.patch
index 4118326..fbb69f6 100644
--- a/amavisd-new-qmqpqq.patch
+++ b/amavisd-new-qmqpqq.patch
@@ -1,36 +1,36 @@
---- amavisd.ori 2014-06-27 19:43:59.240215903 +0200
-+++ amavisd 2014-06-27 19:45:10.174210666 +0200
+--- amavisd.ori 2014-10-22 19:11:04.558937249 +0200
++++ amavisd 2014-10-22 19:12:33.480930887 +0200
@@ -109,4 +109,5 @@
# Amavis::In::SMTP
#( Amavis::In::Courier )
+# Amavis::In::QMQPqq
# Amavis::Out::SMTP::Protocol
# Amavis::Out::SMTP::Session
-@@ -4755,4 +4756,5 @@
- $myproduct_name,
- $conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port);
+@@ -5206,4 +5207,5 @@
+ # RFC 3848, RFC 6531
+ # http://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml
+ # must not use proto name QMQPqq in 'with'
- $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; #RFC 3848
- $s .= "\n id $id" if defined $id && $id ne '';
-@@ -10989,4 +10991,5 @@
+ $s .= "\n with $smtp_proto"
+ if $smtp_proto =~ /^ (?: SMTP | (?: ES|L|UTF8S|UTF8L) MTP S? A? ) \z/xsi;
+@@ -11762,4 +11764,5 @@
$extra_code_sql_lookup $extra_code_ldap
$extra_code_in_ampdp $extra_code_in_smtp $extra_code_in_courier
+ $extra_code_in_qmqpqq
$extra_code_out_smtp $extra_code_out_pipe
$extra_code_out_bsmtp $extra_code_out_local $extra_code_p0f
-@@ -11016,4 +11019,5 @@
+@@ -11789,4 +11792,5 @@
# Amavis::In::AMPDP, Amavis::In::SMTP and In::Courier objects
use vars qw($ampdp_in_obj $smtp_in_obj $courier_in_obj);
+use vars qw($qmqpqq_in_obj); # Amavis::In::QMQPqq object
use vars qw($sql_dataset_conn_lookups); # Amavis::Out::SQL::Connection object
-@@ -11761,4 +11765,5 @@
+@@ -12581,4 +12585,5 @@
do_log(0,"SMTP-in proto code %s loaded", $extra_code_in_smtp ?'':" NOT");
do_log(0,"Courier proto code %s loaded", $extra_code_in_courier ?'':" NOT");
+ do_log(0,"QMQPqq-in proto code %s loaded", $extra_code_in_qmqpqq ?'':" NOT");
do_log(0,"SMTP-out proto code %s loaded", $extra_code_out_smtp ?'':" NOT");
do_log(0,"Pipe-out proto code %s loaded", $extra_code_out_pipe ?'':" NOT");
-@@ -12407,5 +12412,9 @@
+@@ -13231,5 +13236,9 @@
die "unavailable support for protocol: $suggested_protocol";
} elsif ($suggested_protocol eq 'QMQPqq') {
- die "unavailable support for protocol: $suggested_protocol";
@@ -41,20 +41,19 @@
+ $qmqpqq_in_obj->process_qmqpqq_request($sock,$conn,\&check_mail);
} elsif ($suggested_protocol eq 'TCP-LOOKUP') { #postfix maps, experimental
process_tcp_lookup_request($sock, $conn);
-@@ -12530,4 +12539,6 @@
+@@ -13354,4 +13363,5 @@
do_log_safe(5,"child_finish_hook: invoking DESTROY methods");
undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
+ undef $qmqpqq_in_obj;
-+ undef $qmqpqq_in_obj;
undef $sql_storage; undef $sql_wblist; undef $sql_lookups;
undef $sql_dataset_conn_lookups; undef $sql_dataset_conn_storage;
-@@ -17481,4 +17492,5 @@
+@@ -18386,4 +18396,5 @@
$extra_code_sql_lookup, $extra_code_ldap,
$extra_code_in_ampdp, $extra_code_in_smtp, $extra_code_in_courier,
+ $extra_code_in_qmqpqq,
$extra_code_out_smtp, $extra_code_out_pipe,
$extra_code_out_bsmtp, $extra_code_out_local,
-@@ -17838,5 +17850,11 @@
+@@ -18743,5 +18754,11 @@
undef $extra_code_in_courier;
}
- if ($needed_protocols_in{'QMQPqq'}) { die "In::QMQPqq code not available" }
@@ -67,7 +66,7 @@
+ }
}
-@@ -21832,4 +21850,276 @@
+@@ -22860,4 +22877,276 @@
__DATA__
#
+package Amavis::In::QMQPqq;
@@ -344,8 +343,8 @@
+#
package Amavis::Out::SMTP::Protocol;
use strict;
---- amavisd.conf.ori 2014-06-27 19:44:08.520275737 +0200
-+++ amavisd.conf 2014-06-27 19:45:10.175209744 +0200
+--- amavisd.conf.ori 2014-10-22 19:10:16.102940379 +0200
++++ amavisd.conf 2014-10-22 19:12:33.481931103 +0200
@@ -56,6 +56,6 @@
# option(s) -p overrides $inet_socket_port and $unix_socketname
diff --git a/amavisd.conf b/amavisd.conf
index a09597f..d023e10 100644
--- a/amavisd.conf
+++ b/amavisd.conf
@@ -329,6 +329,7 @@ $banned_filename_re = new_RE(
['lrz', \&do_uncompress,
['lrzip -q -k -d -o -', 'lrzcat -q -k'] ],
['lzo', \&do_uncompress, 'lzop -d'],
+ ['lz4', \&do_uncompress, ['lz4c -d'] ],
['rpm', \&do_uncompress, ['rpm2cpio.pl', 'rpm2cpio'] ],
[['cpio','tar'], \&do_pax_cpio, ['pax', 'gcpio', 'cpio'] ],
# ['/usr/local/heirloom/usr/5bin/pax', 'pax', 'gcpio', 'cpio']
@@ -347,7 +348,7 @@ $banned_filename_re = new_RE(
[['zip','kmz'], \&do_7zip, ['7za', '7z'] ],
[['zip','kmz'], \&do_unzip],
['7z', \&do_7zip, ['7zr', '7za', '7z'] ],
- [[qw(7z zip gz bz2 Z tar)],
+ [[qw(gz bz2 Z tar)],
\&do_7zip, ['7za', '7z'] ],
[[qw(xz lzma jar cpio arj rar swf lha iso cab deb rpm)],
\&do_7zip, '7z' ],
diff --git a/amavisd.conf-default b/amavisd.conf-default
index a6d5896..aabcf68 100644
--- a/amavisd.conf-default
+++ b/amavisd.conf-default
@@ -3,10 +3,6 @@ use strict;
## A CONFIGURATION FILE FOR AMAVISD-NEW, LISTING ALL CONFIGURATION VARIABLES
## WITH THEIR DEFAULT VALUES (FOR REFERENCE ONLY, NON-AUTHORITATIVE)
-## This software is licensed under the GNU General Public License (GPL).
-## See comments at the start of file amavisd for the whole license text.
-## Copyright (C) 2002-2012 Mark Martinec, All Rights Reserved.
-
## The 'after-default' comment indicates that these variables obtain their
## default value if the config file left them undefined. It means these values
## are not yet available during processing of the configuration file, but that
@@ -151,7 +147,7 @@ use strict;
## MAIL FORWARDING
# $forward_method = 'smtp:[127.0.0.1]:10025'; # may be arrayref
-# # or 'smtp:[::1]:10025' when INET6 available and INET unavail.
+# # or 'smtp:[::1]:10025' when INET6 is available
# @forward_method_maps = ( sub { Opaque(c('forward_method')) } );
# $resend_method = undef; # falls back to $forward_method
# $always_bcc = undef;
@@ -166,7 +162,7 @@ use strict;
# $release_method = undef; # falls back to $notify_method
# $requeue_method = 'smtp:[127.0.0.1]:25';
-# # or 'smtp:[::1]:25' when INET6 available and INET unavail.
+# # or 'smtp:[::1]:25' when INET6 is available
# $release_format = 'resend'; # (dsn), (arf), attach, plain, resend
# $report_format = 'arf'; # (dsn), arf, attach, plain, resend
# $attachment_password = ''; # '': no pwd, undef: PIN, code ref, or static str
@@ -209,7 +205,7 @@ use strict;
## NOTIFICATIONS (DSN, admin, recip)
# $notify_method = 'smtp:[127.0.0.1]:10025';
-# # or 'smtp:[::1]:10025' when INET6 available and INET unavail.
+# # or 'smtp:[::1]:10025' when INET6 is available
# $propagate_dsn_if_possible = 1;
# $terminate_dsn_on_notify_success = 0;
@@ -366,6 +362,7 @@ use strict;
# ['lrz', \&do_uncompress,
# ['lrzip -q -k -d -o -', 'lrzcat -q -k'] ],
# ['lzo', \&do_uncompress, \$unlzop],
+# ['lz4', \&do_uncompress, ['lz4c -d'] ],
# ['rpm', \&do_uncompress, \$rpm2cpio],
# [['cpio','tar'], \&do_pax_cpio, \$pax],
### ['tar', \&do_tar], # no longer supported
@@ -384,7 +381,7 @@ use strict;
# [['zip','kmz'], \&do_7zip, ['7za', '7z'] ],
# [['zip','kmz'], \&do_unzip],
# ['7z', \&do_7zip, ['7zr', '7za', '7z'] ],
-# [[qw(7z zip gz bz2 Z tar)],
+# [[qw(gz bz2 Z tar)],
# \&do_7zip, ['7za', '7z'] ],
# [[qw(xz lzma jar cpio arj rar swf lha iso cab deb rpm)],
# \&do_7zip, '7z' ],
@@ -409,6 +406,7 @@ use strict;
# [ qr'^(Heuristics\.)?Phishing\.' => 0.1 ],
# [ qr'^(Email|HTML)\.Phishing\.(?!.*Sanesecurity)' => 0.1 ],
# [ qr'^Sanesecurity\.(Malware|Rogue|Trojan)\.' => undef ],# keep as infected
+# [ qr'^Sanesecurity\.Foxhole\.' => undef ],# keep as infected
# [ qr'^Sanesecurity\.' => 0.1 ],
# [ qr'^Sanesecurity_PhishBar_' => 0 ],
# [ qr'^Sanesecurity.TestSig_' => 0 ],
@@ -427,6 +425,7 @@ use strict;
# [ qr'^HTML/Bankish' => 0.1 ], # F-Prot
# [ qr'^PORCUPINE_JUNK' => 0.1 ],
# [ qr'^PORCUPINE_PHISHING' => 0.1 ],
+# [ qr'^Porcupine\.Junk' => 0.1 ],
# [ qr'-SecuriteInfo\.com(\.|\z)' => undef ], # keep as infected
# [ qr'^MBL_NA\.UNOFFICIAL' => 0.1 ], # false positives
# [ qr'^MBL_' => undef ], # keep as infected
@@ -630,13 +629,16 @@ use strict;
## MAPPING A CONTENTS CATEGORY TO A SETTING CHOSEN
# %final_destiny_maps_by_ccat = (
+# # value is normally a list of by-recipient lookup tables, but for compa-
+# # tibility with old %final_destiny_by_ccat a value may also be a scalar
# CC_VIRUS, sub { c('final_virus_destiny') },
# CC_BANNED, sub { c('final_banned_destiny') },
# CC_UNCHECKED, sub { c('final_unchecked_destiny') },
# CC_SPAM, sub { c('final_spam_destiny') },
# CC_BADH, sub { c('final_bad_header_destiny') },
-# CC_MTA.',1', D_TEMPFAIL,
-# CC_MTA.',2', D_REJECT,
+# CC_MTA.',1', D_TEMPFAIL, # MTA response was 4xx
+# CC_MTA.',2', D_REJECT, # MTA response was 5xx
+# CC_MTA, D_TEMPFAIL,
# CC_OVERSIZED, D_BOUNCE,
# CC_CATCHALL, D_PASS,
# );
@@ -648,7 +650,9 @@ use strict;
# # a multiline message will produce a valid multiline SMTP response
# CC_VIRUS, 'id=%n - INFECTED: %V',
# CC_BANNED, 'id=%n - BANNED: %F',
-# CC_UNCHECKED, 'id=%n - UNCHECKED',
+# CC_UNCHECKED.',1', 'id=%n - UNCHECKED: encrypted',
+# CC_UNCHECKED.',2', 'id=%n - UNCHECKED: over limits',
+# CC_UNCHECKED, 'id=%n - UNCHECKED',
# CC_SPAM, 'id=%n - spam',
# CC_SPAMMY.',1', 'id=%n - spammy (tag3)',
# CC_SPAMMY, 'id=%n - spammy',
@@ -677,6 +681,7 @@ use strict;
# CC_BADH, sub { ca('bad_header_lovers_maps') },
# );
# %defang_maps_by_ccat = (
+# # compatible with legacy %defang_by_ccat: value may be a scalar
# CC_VIRUS, sub { c('defang_virus') },
# CC_BANNED, sub { c('defang_banned') },
# CC_UNCHECKED, sub { c('defang_undecipherable') },
@@ -753,7 +758,7 @@ use strict;
# CC_CATCHALL, sub { cr('notify_virus_admin_templ') },
# );
# %notify_recips_templ_by_ccat = (
-# CC_SPAM, sub { cr('notify_spam_recips_templ') }, #usualy empty
+# CC_SPAM, sub { cr('notify_spam_recips_templ') }, #usually empty
# CC_CATCHALL, sub { cr('notify_virus_recips_templ') },
# );
# %notify_sender_templ_by_ccat = ( # bounce templates
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/amavisd-new/pkg-amavisd-new.git
More information about the Amavisd-new-commits
mailing list