[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 &section_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
     &quote_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