PageRenderTime 68ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 2ms

/amavisd-new-2.8.0/amavisd

#
Perl | 11249 lines | 9407 code | 279 blank | 1563 comment | 824 complexity | 4fc193aa494cbf85ec5fb01922acc8f3 MD5 | raw file
Possible License(s): GPL-2.0
  1. #!/usr/bin/perl -T
  2. #!/usr/bin/perl -d:NYTProf
  3. #------------------------------------------------------------------------------
  4. # This is amavisd-new.
  5. # It is an interface between a message transfer agent (MTA) and virus
  6. # scanners and/or spam scanners, functioning as a mail content filter.
  7. #
  8. # It is a performance-enhanced and feature-enriched version of amavisd
  9. # (which in turn is a daemonized version of AMaViS), initially based
  10. # on amavisd-snapshot-20020300).
  11. #
  12. # All work since amavisd-snapshot-20020300:
  13. # Copyright (C) 2002-2012 Mark Martinec,
  14. # All Rights Reserved.
  15. # with contributions from the amavis-user mailing list and individuals,
  16. # as acknowledged in the release notes.
  17. #
  18. # This program is free software; you can redistribute it and/or modify
  19. # it under the terms of the GNU General Public License as published by
  20. # the Free Software Foundation; either version 2 of the License, or
  21. # (at your option) any later version.
  22. #
  23. # This program is distributed in the hope that it will be useful,
  24. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. # GNU General Public License for details.
  27. #
  28. # You should have received a copy of the GNU General Public License
  29. # along with this program; if not, write to the Free Software
  30. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  31. # Author: Mark Martinec <Mark.Martinec@ijs.si>
  32. # Patches and problem reports are welcome.
  33. #
  34. # The latest version of this program is available at:
  35. # http://www.ijs.si/software/amavisd/
  36. #------------------------------------------------------------------------------
  37. # Here is a boilerplate from the amavisd(-snapshot) version,
  38. # which is the version that served as a base code for the initial
  39. # version of amavisd-new. License terms were the same:
  40. #
  41. # Author: Chris Mason <cmason@unixzone.com>
  42. # Current maintainer: Lars Hecking <lhecking@users.sourceforge.net>
  43. # Based on work by:
  44. # Mogens Kjaer, Carlsberg Laboratory, <mk@crc.dk>
  45. # Juergen Quade, Softing GmbH, <quade@softing.com>
  46. # Christian Bricart <shiva@aachalon.de>
  47. # Rainer Link <link@foo.fh-furtwangen.de>
  48. # This script is part of the AMaViS package. For more information see:
  49. # http://amavis.org/
  50. # Copyright (C) 2000 - 2002 the people mentioned above
  51. # This software is licensed under the GNU General Public License (GPL)
  52. # See: http://www.gnu.org/copyleft/gpl.html
  53. #------------------------------------------------------------------------------
  54. #------------------------------------------------------------------------------
  55. #Index of packages in this file
  56. # Amavis::Boot
  57. # Amavis::Conf
  58. # Amavis::Log
  59. # Amavis::DbgLog
  60. # Amavis::Timing
  61. # Amavis::Util
  62. # Amavis::ProcControl
  63. # Amavis::rfc2821_2822_Tools
  64. # Amavis::Lookup::RE
  65. # Amavis::Lookup::IP
  66. # Amavis::Lookup::Opaque
  67. # Amavis::Lookup::OpaqueRef
  68. # Amavis::Lookup::Label
  69. # Amavis::Lookup::SQLfield (just the new() method)
  70. # Amavis::Lookup::LDAPattr (just the new() method)
  71. # Amavis::Lookup
  72. # Amavis::Expand
  73. # Amavis::TempDir
  74. # Amavis::IO::FileHandle
  75. # Amavis::IO::Zlib
  76. # Amavis::IO::RW
  77. # Amavis::In::Connection
  78. # Amavis::In::Message::PerRecip
  79. # Amavis::In::Message
  80. # Amavis::Out::EditHeader
  81. # Amavis::Out
  82. # Amavis::UnmangleSender
  83. # Amavis::Unpackers::NewFilename
  84. # Amavis::Unpackers::Part
  85. # Amavis::Unpackers::OurFiler
  86. # Amavis::Unpackers::Validity
  87. # Amavis::Unpackers::MIME
  88. # Amavis::Notify
  89. # Amavis::Custom
  90. # Amavis
  91. #optionally compiled-in packages: ---------------------------------------------
  92. # Amavis::ZMQ
  93. # Amavis::DB::SNMP
  94. # Amavis::DB
  95. # Amavis::Lookup::SQLfield (the rest)
  96. # Amavis::Lookup::SQL
  97. # Amavis::LDAP::Connection
  98. # Amavis::Lookup::LDAP
  99. # Amavis::Lookup::LDAPattr (the rest)
  100. # Amavis::In::AMPDP
  101. # Amavis::In::SMTP
  102. #( Amavis::In::Courier )
  103. # Amavis::Out::SMTP::Protocol
  104. # Amavis::Out::SMTP::Session
  105. # Amavis::Out::SMTP
  106. # Amavis::Out::Pipe
  107. # Amavis::Out::BSMTP
  108. # Amavis::Out::Local
  109. # Amavis::OS_Fingerprint
  110. # Amavis::Out::SQL::Connection
  111. # Amavis::Out::SQL::Log
  112. # Amavis::IO::SQL
  113. # Amavis::Out::SQL::Quarantine
  114. # Amavis::AV
  115. # Amavis::SpamControl
  116. # Amavis::SpamControl::ExtProg
  117. # Amavis::SpamControl::SpamdClient
  118. # Mail::SpamAssassin::Logger::Amavislog
  119. # Amavis::SpamControl::SpamAssassin
  120. # Amavis::Unpackers
  121. # Amavis::DKIM::CustomSigner
  122. # Amavis::DKIM
  123. # Amavis::Tools
  124. #------------------------------------------------------------------------------
  125. use strict;
  126. use re 'taint';
  127. use warnings;
  128. use warnings FATAL => qw(utf8 void);
  129. no warnings 'uninitialized';
  130. #
  131. package Amavis::Boot;
  132. use strict;
  133. use re 'taint';
  134. use Errno qw(ENOENT EACCES);
  135. # replacement for a 'require' with a more informative error handling
  136. #sub my_require($) {
  137. # my($filename) = @_;
  138. # my $result;
  139. # if (exists $INC{$filename} && !$INC{$filename}) {
  140. # die "Compilation failed in require\n";
  141. # } elsif (exists $INC{$filename}) {
  142. # $result = 1; # already loaded
  143. # } else {
  144. # my $found = 0;
  145. # for my $prefix (@INC) {
  146. # my $full_fname = "$prefix/$filename";
  147. # my(@stat_list) = stat($full_fname); # symlinks-friendly
  148. # my $errn = @stat_list ? 0 : 0+$!;
  149. # if ($errn != ENOENT) {
  150. # $found = 1;
  151. # $INC{$filename} = $full_fname;
  152. # my $owner_uid = $stat_list[4];
  153. # my $msg;
  154. # if ($errn) { $msg = "is inaccessible: $!" }
  155. # elsif (-d _) { $msg = "is a directory" }
  156. # elsif (!-f _) { $msg = "is not a regular file" }
  157. # elsif ($> && -o _) { $msg = "should not be owned by EUID $>" }
  158. # elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" }
  159. # elsif ($owner_uid) { $msg = "should be owned by root (uid 0) "}
  160. # !defined($msg) or die "Requiring $full_fname, file $msg,\n";
  161. # $! = 0;
  162. # $result = do $full_fname;
  163. # if (!defined($result) && $@ ne '') {
  164. # undef $INC{$filename}; chomp($@);
  165. # die "Error in file $full_fname: $@\n";
  166. # } elsif (!defined($result) && $! != 0) {
  167. # undef $INC{$filename};
  168. # die "Error reading file $full_fname: $!\n";
  169. # } elsif (!$result) {
  170. # undef $INC{$filename};
  171. # die "Module $full_fname did not return a true value\n";
  172. # }
  173. # last;
  174. # }
  175. # }
  176. # die sprintf("my_require: Can't locate %s in \@INC (\@INC contains: %s)\n",
  177. # $filename, join(' ',@INC)) if !$found;
  178. # }
  179. # $result;
  180. #}
  181. # Fetch all required modules (or nicely report missing ones), and compile them
  182. # once-and-for-all at the parent process, so that forked children can inherit
  183. # and share already compiled code in memory. Children will still need to 'use'
  184. # modules if they want to inherit from their name space.
  185. #
  186. sub fetch_modules($$@) {
  187. my($reason, $required, @modules) = @_;
  188. my $have_sawampersand = Devel::SawAmpersand->UNIVERSAL::can('sawampersand');
  189. my $amp = $have_sawampersand && Devel::SawAmpersand::sawampersand() ? 1 : 0;
  190. warn 'fetch_modules: PL_sawampersand flag was already turned on' if $amp;
  191. my(@missing);
  192. for my $m (@modules) {
  193. local $_ = $m;
  194. $_ .= /^auto::/ ? '.al' : '.pm' if !m{^/} && !m{\.(?:pm|pl|al|ix)\z};
  195. s{::}{/}g;
  196. # eval { my_require $_ } #more informative on err, but some problems reported
  197. eval { require $_ }
  198. or do {
  199. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  200. push(@missing,$m);
  201. $eval_stat =~ s/^/ /mgs; # indent
  202. printf STDERR ("fetch_modules: error loading %s module %s:\n%s\n",
  203. $required ? 'required' : 'optional', $_, $eval_stat)
  204. if $eval_stat !~ /\bCan't locate \Q$_\E in \@INC\b/;
  205. };
  206. if ($have_sawampersand && !$amp && Devel::SawAmpersand::sawampersand())
  207. { $amp = 1; warn "Loading of module $m turned on PL_sawampersand flag" }
  208. }
  209. die "ERROR: MISSING $reason:\n" . join('', map(" $_\n", @missing))
  210. if $required && @missing;
  211. \@missing;
  212. }
  213. BEGIN {
  214. if ($] <= 5.008) { # deal with a glob() taint bug (perl 5.6.1, 5.8.0)
  215. fetch_modules('REQUIRED BASIC MODULES', 1, qw(File::Glob));
  216. File::Glob->import(':globally'); # use the same module as Perl 5.8 uses
  217. }
  218. fetch_modules('REQUIRED BASIC MODULES', 1, qw(
  219. Exporter POSIX Fcntl Socket Errno Carp Time::HiRes
  220. IO::Handle IO::File IO::Socket IO::Socket::UNIX
  221. IO::Stringy Digest::MD5 Unix::Syslog File::Basename
  222. Compress::Zlib MIME::Base64 MIME::QuotedPrint MIME::Words
  223. MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder
  224. MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint
  225. MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64
  226. Net::Server Net::Server::PreFork
  227. ));
  228. # with earlier versions of Perl one may need to add additional modules
  229. # to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ...
  230. fetch_modules('OPTIONAL BASIC MODULES', 0, qw(
  231. PerlIO PerlIO::scalar Unix::Getrusage
  232. Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid
  233. auto::POSIX::SigAction::new auto::POSIX::SigAction::safe
  234. MIME::Decoder::BinHex
  235. ));
  236. 1;
  237. }
  238. 1;
  239. #
  240. package Amavis::Conf;
  241. use strict;
  242. use re 'taint';
  243. # constants; intentionally leave value -1 unassigned for compatibility
  244. use constant D_TEMPFAIL => -4;
  245. use constant D_REJECT => -3;
  246. use constant D_BOUNCE => -2;
  247. use constant D_DISCARD => 0;
  248. use constant D_PASS => 1;
  249. # major contents_category constants, in increasing order of importance
  250. use constant CC_CATCHALL => 0;
  251. use constant CC_CLEAN => 1; # tag_level = "CC_CLEAN,1"
  252. use constant CC_MTA => 2; # trouble passing mail back to MTA
  253. use constant CC_OVERSIZED => 3;
  254. use constant CC_BADH => 4;
  255. use constant CC_SPAMMY => 5; # tag2_level (and: tag3_level = CC_SPAMMY,1)
  256. use constant CC_SPAM => 6; # kill_level
  257. use constant CC_UNCHECKED => 7;
  258. use constant CC_BANNED => 8;
  259. use constant CC_VIRUS => 9;
  260. #
  261. # in other words: major_ccat minor_ccat %subject_tag_maps_by_ccat
  262. ## if score >= kill level => CC_SPAM 0
  263. ## elsif score >= tag3 level => CC_SPAMMY 1 @spam_subject_tag3_maps
  264. ## elsif score >= tag2 level => CC_SPAMMY 0 @spam_subject_tag2_maps
  265. ## elsif score >= tag level => CC_CLEAN 1 @spam_subject_tag_maps
  266. ## else => CC_CLEAN 0
  267. BEGIN {
  268. require Exporter;
  269. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  270. $VERSION = '2.316';
  271. @ISA = qw(Exporter);
  272. %EXPORT_TAGS = (
  273. 'dynamic_confvars' => # per- policy bank settings
  274. [qw(
  275. $child_timeout $smtpd_timeout
  276. $policy_bank_name $protocol @inet_acl
  277. $myhostname $myauthservid $snmp_contact $snmp_location
  278. $myprogram_name $syslog_ident $syslog_facility
  279. $log_level $log_templ $log_recip_templ $enable_log_capture_dump
  280. $forward_method $notify_method $resend_method $report_format
  281. $release_method $requeue_method $release_format
  282. $attachment_password $attachment_email_name $attachment_outer_name
  283. $os_fingerprint_method $os_fingerprint_dst_ip_and_port
  284. $originating @smtpd_discard_ehlo_keywords $soft_bounce
  285. $propagate_dsn_if_possible $terminate_dsn_on_notify_success
  286. $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded
  287. $auth_required_out $auth_required_inp $auth_required_release
  288. @auth_mech_avail $tls_security_level_in $tls_security_level_out
  289. $local_client_bind_address $smtpd_message_size_limit
  290. $localhost_name $smtpd_greeting_banner $smtpd_quit_banner
  291. $mailfrom_to_quarantine $warn_offsite $bypass_decode_parts @decoders
  292. @av_scanners @av_scanners_backup @spam_scanners
  293. $first_infected_stops_scan $virus_scanners_failure_is_fatal
  294. $sa_spam_level_char $sa_mail_body_size_limit
  295. $penpals_bonus_score $penpals_halflife $bounce_killer_score
  296. $reputation_factor
  297. $undecipherable_subject_tag $localpart_is_case_sensitive
  298. $recipient_delimiter $replace_existing_extension
  299. $hdr_encoding $bdy_encoding $hdr_encoding_qb
  300. $allow_disclaimers
  301. $prepend_header_fields_hdridx
  302. $allow_fixing_improper_header
  303. $allow_fixing_improper_header_folding $allow_fixing_long_header_lines
  304. %allowed_added_header_fields %prefer_our_added_header_fields
  305. %allowed_header_tests
  306. $X_HEADER_TAG $X_HEADER_LINE
  307. $remove_existing_x_scanned_headers $remove_existing_spam_headers
  308. %sql_clause $partition_tag
  309. %local_delivery_aliases $banned_namepath_re
  310. $per_recip_whitelist_sender_lookup_tables
  311. $per_recip_blacklist_sender_lookup_tables
  312. @anomy_sanitizer_args @altermime_args_defang
  313. @altermime_args_disclaimer @disclaimer_options_bysender_maps
  314. %signed_header_fields @dkim_signature_options_bysender_maps
  315. $enable_dkim_verification $enable_dkim_signing $dkim_signing_service
  316. $enable_ldap
  317. @local_domains_maps @mynetworks_maps @client_ipaddr_policy
  318. @forward_method_maps @newvirus_admin_maps @banned_filename_maps
  319. @spam_quarantine_bysender_to_maps
  320. @spam_tag_level_maps @spam_tag2_level_maps @spam_tag3_level_maps
  321. @spam_kill_level_maps
  322. @spam_subject_tag_maps @spam_subject_tag2_maps @spam_subject_tag3_maps
  323. @spam_dsn_cutoff_level_maps @spam_dsn_cutoff_level_bysender_maps
  324. @spam_crediblefrom_dsn_cutoff_level_maps
  325. @spam_crediblefrom_dsn_cutoff_level_bysender_maps
  326. @spam_quarantine_cutoff_level_maps @spam_notifyadmin_cutoff_level_maps
  327. @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
  328. @author_to_policy_bank_maps @signer_reputation_maps
  329. @message_size_limit_maps @debug_sender_maps @debug_recipient_maps
  330. @bypass_virus_checks_maps @bypass_spam_checks_maps
  331. @bypass_banned_checks_maps @bypass_header_checks_maps
  332. @viruses_that_fake_sender_maps
  333. @virus_name_to_spam_score_maps @virus_name_to_policy_bank_maps
  334. @remove_existing_spam_headers_maps
  335. @sa_userconf_maps @sa_username_maps
  336. %final_destiny_by_ccat %forward_method_maps_by_ccat
  337. %lovers_maps_by_ccat %defang_maps_by_ccat %subject_tag_maps_by_ccat
  338. %quarantine_method_by_ccat %quarantine_to_maps_by_ccat
  339. %notify_admin_templ_by_ccat %notify_recips_templ_by_ccat
  340. %notify_sender_templ_by_ccat %notify_autoresp_templ_by_ccat
  341. %notify_release_templ_by_ccat %notify_report_templ_by_ccat
  342. %warnsender_by_ccat
  343. %hdrfrom_notify_admin_by_ccat %mailfrom_notify_admin_by_ccat
  344. %hdrfrom_notify_recip_by_ccat %mailfrom_notify_recip_by_ccat
  345. %hdrfrom_notify_sender_by_ccat
  346. %hdrfrom_notify_release_by_ccat %hdrfrom_notify_report_by_ccat
  347. %admin_maps_by_ccat %warnrecip_maps_by_ccat
  348. %always_bcc_by_ccat %dsn_bcc_by_ccat
  349. %addr_extension_maps_by_ccat %addr_rewrite_maps_by_ccat
  350. %smtp_reason_by_ccat
  351. )],
  352. 'confvars' => # global settings (not per-policy, not per-recipient)
  353. [qw(
  354. $myproduct_name $myversion_id $myversion_id_numeric $myversion_date
  355. $myversion $instance_name @additional_perl_modules
  356. $MYHOME $TEMPBASE $QUARANTINEDIR $quarantine_subdir_levels
  357. $daemonize $courierfilter_shutdown $pid_file $lock_file $db_home
  358. $enable_db $enable_zmq @zmq_sockets $mail_id_size_bits
  359. $daemon_user $daemon_group $daemon_chroot_dir $path
  360. $DEBUG $do_syslog $logfile $allow_preserving_evidence $enable_log_capture
  361. $log_short_templ $log_verbose_templ $logline_maxlen
  362. $nanny_details_level $max_servers $max_requests
  363. $min_servers $min_spare_servers $max_spare_servers
  364. %current_policy_bank %policy_bank %interface_policy
  365. @listen_sockets $inet_socket_port $inet_socket_bind $listen_queue_size
  366. $unix_socketname $unix_socket_mode
  367. $smtp_connection_cache_on_demand $smtp_connection_cache_enable
  368. $smtpd_recipient_limit
  369. $smtpd_tls_cert_file $smtpd_tls_key_file
  370. $enforce_smtpd_message_size_limit_64kb_min
  371. $MAXLEVELS $MAXFILES
  372. $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
  373. $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR
  374. $database_sessions_persistent $lookup_maps_imply_sql_and_ldap
  375. @lookup_sql_dsn @storage_sql_dsn
  376. $sql_schema_version $timestamp_fmt_mysql
  377. $sql_quarantine_chunksize_max $sql_allow_8bit_address
  378. $sql_lookups_no_at_means_domain $ldap_lookups_no_at_means_domain
  379. $sql_store_info_for_all_msgs
  380. $trim_trailing_space_in_lookup_result_fields
  381. $default_ldap $mail_digest_algorithm
  382. @keep_decoded_original_maps @map_full_type_to_short_type_maps
  383. %banned_rules $penpals_threshold_low $penpals_threshold_high
  384. %dkim_signing_keys_by_domain
  385. @dkim_signing_keys_list @dkim_signing_keys_storage
  386. $file $altermime $enable_anomy_sanitizer
  387. )],
  388. 'sa' => # global SpamAssassin settings
  389. [qw(
  390. $spamcontrol_obj $sa_num_instances
  391. $helpers_home $sa_configpath $sa_siteconfigpath $sa_userprefs_file
  392. $sa_local_tests_only $sa_timeout $sa_debug
  393. $dspam $sa_spawned
  394. )],
  395. 'platform' => [qw(
  396. $can_truncate $unicode_aware $my_pid
  397. $AF_INET6 $have_inet4 $have_inet6 $have_socket_ip
  398. &D_TEMPFAIL &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS
  399. &CC_CATCHALL &CC_CLEAN &CC_MTA &CC_OVERSIZED &CC_BADH
  400. &CC_SPAMMY &CC_SPAM &CC_UNCHECKED &CC_BANNED &CC_VIRUS
  401. %ccat_display_names %ccat_display_names_major
  402. )],
  403. # other variables settable by user in amavisd.conf,
  404. # but not directly accessible to the program
  405. 'hidden_confvars' => [qw(
  406. $mydomain
  407. )],
  408. 'legacy_dynamic_confvars' =>
  409. # the rest of the program does not use these settings directly and they
  410. # should not be visible in, or imported to other modules, but may be
  411. # referenced indirectly through *_by_ccat variables for compatibility
  412. [qw(
  413. $final_virus_destiny $final_banned_destiny $final_unchecked_destiny
  414. $final_spam_destiny $final_bad_header_destiny
  415. @virus_lovers_maps @spam_lovers_maps @unchecked_lovers_maps
  416. @banned_files_lovers_maps @bad_header_lovers_maps
  417. $always_bcc $dsn_bcc
  418. $mailfrom_notify_sender $mailfrom_notify_recip
  419. $mailfrom_notify_admin $mailfrom_notify_spamadmin
  420. $hdrfrom_notify_sender $hdrfrom_notify_recip
  421. $hdrfrom_notify_admin $hdrfrom_notify_spamadmin
  422. $hdrfrom_notify_release $hdrfrom_notify_report
  423. $notify_virus_admin_templ $notify_spam_admin_templ
  424. $notify_virus_recips_templ $notify_spam_recips_templ
  425. $notify_virus_sender_templ $notify_spam_sender_templ
  426. $notify_sender_templ $notify_release_templ
  427. $notify_report_templ $notify_autoresp_templ
  428. $warnbannedsender $warnbadhsender
  429. $defang_virus $defang_banned $defang_spam
  430. $defang_bad_header $defang_undecipherable $defang_all
  431. $virus_quarantine_method $banned_files_quarantine_method
  432. $unchecked_quarantine_method $spam_quarantine_method
  433. $bad_header_quarantine_method $clean_quarantine_method
  434. $archive_quarantine_method
  435. @virus_quarantine_to_maps @banned_quarantine_to_maps
  436. @unchecked_quarantine_to_maps @spam_quarantine_to_maps
  437. @bad_header_quarantine_to_maps @clean_quarantine_to_maps
  438. @archive_quarantine_to_maps
  439. @virus_admin_maps @banned_admin_maps
  440. @spam_admin_maps @bad_header_admin_maps @spam_modifies_subj_maps
  441. @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
  442. @addr_extension_virus_maps @addr_extension_spam_maps
  443. @addr_extension_banned_maps @addr_extension_bad_header_maps
  444. )],
  445. 'legacy_confvars' =>
  446. # legacy variables, predeclared for compatibility of amavisd.conf
  447. # The rest of the program does not use them directly and they should
  448. # not be visible in other modules, but may be referenced through
  449. # @*_maps variables for backwards compatibility
  450. [qw(
  451. %local_domains @local_domains_acl $local_domains_re @mynetworks
  452. %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re
  453. %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re
  454. %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re
  455. %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re
  456. %virus_lovers @virus_lovers_acl $virus_lovers_re
  457. %spam_lovers @spam_lovers_acl $spam_lovers_re
  458. %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re
  459. %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re
  460. %virus_admin %spam_admin
  461. $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin
  462. $warnvirusrecip $warnbannedrecip $warnbadhrecip
  463. $virus_quarantine_to $banned_quarantine_to $unchecked_quarantine_to
  464. $spam_quarantine_to $spam_quarantine_bysender_to
  465. $bad_header_quarantine_to $clean_quarantine_to $archive_quarantine_to
  466. $keep_decoded_original_re $map_full_type_to_short_type_re
  467. $banned_filename_re $viruses_that_fake_sender_re
  468. $sa_tag_level_deflt $sa_tag2_level_deflt $sa_tag3_level_deflt
  469. $sa_kill_level_deflt
  470. $sa_quarantine_cutoff_level @spam_notifyadmin_cutoff_level_maps
  471. $sa_dsn_cutoff_level $sa_crediblefrom_dsn_cutoff_level
  472. $sa_spam_modifies_subj $sa_spam_subject_tag1 $sa_spam_subject_tag
  473. %whitelist_sender @whitelist_sender_acl $whitelist_sender_re
  474. %blacklist_sender @blacklist_sender_acl $blacklist_sender_re
  475. $addr_extension_virus $addr_extension_spam
  476. $addr_extension_banned $addr_extension_bad_header
  477. $sql_select_policy $sql_select_white_black_list
  478. $gets_addr_in_quoted_form @debug_sender_acl
  479. $arc $bzip2 $lzop $lha $unarj $gzip $uncompress $unfreeze
  480. $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole $tnef
  481. $gunzip $bunzip2 $unlzop $unstuff
  482. $SYSLOG_LEVEL $syslog_priority $append_header_fields_to_bottom
  483. $insert_received_line $notify_xmailer_header $relayhost_is_client
  484. $sa_spam_report_header $sa_auto_whitelist
  485. $warnvirussender $warnspamsender
  486. $enable_global_cache
  487. $virus_check_negative_ttl $virus_check_positive_ttl
  488. $spam_check_negative_ttl $spam_check_positive_ttl
  489. )],
  490. );
  491. Exporter::export_tags qw(dynamic_confvars confvars sa platform
  492. hidden_confvars legacy_dynamic_confvars legacy_confvars);
  493. 1;
  494. } # BEGIN
  495. use POSIX ();
  496. use Carp ();
  497. use Errno qw(ENOENT EACCES EBADF);
  498. use vars @EXPORT;
  499. sub c($); sub cr($); sub ca($); sub dkim_key($$$;@); # prototypes
  500. use subs qw(c cr ca dkim_key); # access subroutines to config vars and keys
  501. BEGIN { push(@EXPORT,qw(c cr ca dkim_key)) }
  502. # access to dynamic config variables, returns a scalar config variable value;
  503. # one level of indirection is allowed
  504. #
  505. sub c($) {
  506. my $var = $current_policy_bank{$_[0]};
  507. if (!defined $var) {
  508. my $name = $_[0];
  509. if (!exists $current_policy_bank{$name}) {
  510. Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
  511. $name, $current_policy_bank{'policy_bank_name'}));
  512. }
  513. }
  514. my $r = ref $var;
  515. !$r ? $var : $r eq 'SCALAR' || $r eq 'REF' ? $$var : $var;
  516. }
  517. # return a ref to a config variable value, or undef if var is undefined
  518. #
  519. sub cr($) {
  520. my $var = $current_policy_bank{$_[0]};
  521. if (!defined $var) {
  522. my $name = $_[0];
  523. if (!exists $current_policy_bank{$name}) {
  524. Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
  525. $name, $current_policy_bank{'policy_bank_name'}));
  526. }
  527. }
  528. !defined $var ? undef : !ref $var ? \$var : $var;
  529. }
  530. # return a ref to a config variable value (which is supposed to be an array),
  531. # converting undef to an empty array, and a scalar to a one-element array
  532. # if necessary
  533. #
  534. sub ca($) {
  535. my $var = $current_policy_bank{$_[0]};
  536. if (!defined $var) {
  537. my $name = $_[0];
  538. if (!exists $current_policy_bank{$name}) {
  539. Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
  540. $name, $current_policy_bank{'policy_bank_name'}));
  541. }
  542. }
  543. !defined $var ? [] : !ref $var ? [$var] : $var;
  544. }
  545. sub deprecate_var($$$) {
  546. my($data_type, $var_name, $init_value) = @_;
  547. my $code = <<'EOD';
  548. tie(%n, '%p', %v) or die 'Tieing a variable %n failed';
  549. package %p;
  550. use strict; use Carp ();
  551. sub TIESCALAR { my($class,$val) = @_; bless \$val, $class }
  552. sub FETCH { my $self = shift; $$self }
  553. sub STORE { my($self,$newv) = @_; my $oldv = $$self;
  554. if ((defined $oldv || defined $newv) && (%t)) {
  555. Carp::carp('Variable %n was retired, changing its value has no effect.'
  556. . " See release notes.\n");
  557. }
  558. $$self = $newv;
  559. }
  560. 1;
  561. EOD
  562. if ($data_type eq 'bool') {
  563. $code =~ s{%t}'($oldv ? 1 : 0) != ($newv ? 1 : 0)'g;
  564. } elsif ($data_type eq 'num') {
  565. $code =~ s{%t}'!defined $oldv || !defined $newv || $oldv != $newv'g;
  566. } elsif ($data_type eq 'str') {
  567. $code =~ s{%t}'!defined $oldv || !defined $newv || $oldv ne $newv'g;
  568. } else {
  569. die "Error deprecating a variable $var_name: bad type $data_type";
  570. }
  571. $code =~ s/%n/$var_name/g;
  572. $code =~ s/%v/\$init_value/g;
  573. my $barename = $var_name;
  574. $barename =~ s/^[\$\@%&]//; $code =~ s/%p/Amavis::Deprecate::$barename/g;
  575. eval $code
  576. or do { chomp $@; die "Error deprecating a variable $var_name: $@" };
  577. }
  578. # Store a private DKIM signing key for a given domain and selector.
  579. # The argument $key can be a Mail::DKIM::PrivateKey object or a file
  580. # name containing a key in a PEM format (e.g. as generated by openssl).
  581. # For compatibility with dkim_milter the signing domain can include a '*'
  582. # as a wildcard - this is not recommended as this way amavisd could produce
  583. # signatures which have no corresponding public key published in DNS.
  584. # The proper way is to have one dkim_key entry for each published DNS RR.
  585. # Optional arguments can provide additional information about the resource
  586. # record (RR) of a public key, i.e. its options according to RFC 4871.
  587. # The subroutine is typically called from a configuration file, once for
  588. # each signing key available.
  589. #
  590. sub dkim_key($$$;@) {
  591. my($domain,$selector,$key) = @_; shift; shift; shift;
  592. @_%2 == 0 or die "dkim_key: a list of key/value pairs expected as options\n";
  593. my(%key_options) = @_; # remaining args are options from a public key RR
  594. defined $domain && $domain ne ''
  595. or die "dkim_key: domain must not be empty: ($domain,$selector,$key)";
  596. defined $selector && $selector ne ''
  597. or die "dkim_key: selector must not be empty: ($domain,$selector,$key)";
  598. my $key_storage_ind;
  599. if (ref $key) { # key already preprocessed and provided as an object
  600. push(@dkim_signing_keys_storage, [$key]);
  601. $key_storage_ind = $#dkim_signing_keys_storage;
  602. } else { # assume a name of a file containing a private key in PEM format
  603. my $fname = $key;
  604. my $pem_fh = IO::File->new; # open a file with a private key
  605. $pem_fh->open($fname,'<') or die "Can't open PEM file $fname: $!";
  606. my(@stat_list) = stat($pem_fh); # soft-link friendly
  607. @stat_list or warn "Error accessing $fname: $!";
  608. my($dev,$inode) = @stat_list;
  609. if ($dev && $inode) {
  610. for my $j (0..$#dkim_signing_keys_storage) { # same file reused?
  611. my($k,$dv,$in,$fn) = @{$dkim_signing_keys_storage[$j]};
  612. if ($dv == $dev && $in == $inode) { $key_storage_ind = $j; last }
  613. }
  614. }
  615. if (!defined($key_storage_ind)) {
  616. # read file and store its contents as a new entry
  617. $key = ''; Amavis::Util::read_file($pem_fh,\$key);
  618. my $key_fit = $key; # shrink allocated storage size to actual size
  619. undef $key; # release storage
  620. push(@dkim_signing_keys_storage, [$key_fit, $dev, $inode, $fname]);
  621. $key_storage_ind = $#dkim_signing_keys_storage;
  622. }
  623. $pem_fh->close or die "Error closing file $fname: $!";
  624. $key_options{k} = 'rsa' if defined $key_options{k}; # force RSA
  625. }
  626. $domain = lc($domain) if !ref($domain); # possibly a regexp
  627. $selector = lc($selector);
  628. $key_options{domain} = $domain; $key_options{selector} = $selector;
  629. $key_options{key_storage_ind} = $key_storage_ind;
  630. if (@dkim_signing_keys_list > 100) {
  631. # sorry, skip the test to avoid slow O(n^2) searches
  632. } else {
  633. !grep($_->{domain} eq $domain && $_->{selector} eq $selector,
  634. @dkim_signing_keys_list)
  635. or die "dkim_key: selector $selector for domain $domain already in use\n";
  636. }
  637. $key_options{key_ind} = $#dkim_signing_keys_list + 1;
  638. push(@dkim_signing_keys_list, \%key_options); # using a list preserves order
  639. }
  640. # essential initializations, right at the program start time, may run as root!
  641. #
  642. use vars qw($read_config_files_depth @actual_config_files);
  643. BEGIN { # init_primary: version, $unicode_aware, base policy bank
  644. $myprogram_name = $0; # typically 'amavisd'
  645. local $1; $myprogram_name =~ s{([^/]*)\z}{$1}s;
  646. $myproduct_name = 'amavisd-new';
  647. $myversion_id = '2.8.0'; $myversion_date = '20120630';
  648. $myversion = "$myproduct_name-$myversion_id ($myversion_date)";
  649. $myversion_id_numeric = # x.yyyzzz, allows numerical compare, like Perl $]
  650. sprintf('%8.6f', $1 + ($2 + $3/1000)/1000)
  651. if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/;
  652. $sql_schema_version = $myversion_id_numeric;
  653. $unicode_aware =
  654. $] >= 5.008 && length("\x{263a}")==1 && eval { require Encode };
  655. $read_config_files_depth = 0;
  656. eval { require Devel::SawAmpersand } or 1; # load if avail, ignore failure
  657. # initialize policy bank hash to contain dynamic config settings
  658. for my $tag (@EXPORT_TAGS{'dynamic_confvars', 'legacy_dynamic_confvars'}) {
  659. for my $v (@$tag) {
  660. local($1,$2);
  661. if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" }
  662. else {
  663. no strict 'refs'; my($type,$name) = ($1,$2);
  664. $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"}
  665. : $type eq '@' ? \@{"Amavis::Conf::$name"}
  666. : $type eq '%' ? \%{"Amavis::Conf::$name"}
  667. : undef;
  668. }
  669. }
  670. }
  671. $current_policy_bank{'policy_bank_name'} = ''; # builtin policy
  672. $current_policy_bank{'policy_bank_path'} = '';
  673. $policy_bank{''} = { %current_policy_bank }; # copy
  674. 1;
  675. } # end BEGIN - init_primary
  676. # boot-time initializations of simple global settings, may run as root!
  677. #
  678. BEGIN {
  679. # serves only as a quick default for other configuration settings
  680. $MYHOME = '/var/amavis';
  681. $mydomain = '!change-mydomain-variable!.example.com';#intentionally bad deflt
  682. # Create debugging output - true: log to stderr; false: log to syslog/file
  683. $DEBUG = 0;
  684. # In case of trouble, allow preserving temporary files for forensics
  685. $allow_preserving_evidence = 1;
  686. # Cause Net::Server parameters 'background' and 'setsid' to be set,
  687. # resulting in the program to detach itself from the terminal
  688. $daemonize = 1;
  689. # Net::Server pre-forking settings - defaults, overruled by amavisd.conf
  690. $max_servers = 2; # number of pre-forked children
  691. $max_requests = 20; # retire a child after that many accepts, 0=unlimited
  692. # timeout for our processing:
  693. $child_timeout = 8*60; # abort child if it does not complete a task in n sec
  694. # timeout for waiting on client input:
  695. $smtpd_timeout = 8*60; # disconnect session if client is idle for too long;
  696. # $smtpd_timeout should be higher than Postfix's max_idle (default 100s)
  697. # Assume STDIN is a courierfilter pipe and shutdown when it becomes readable
  698. $courierfilter_shutdown = 0;
  699. # Can file be truncated?
  700. # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature,
  701. # not required by Posix).
  702. # Things will go faster with SMTP-in, otherwise (e.g. with milter)
  703. # it makes no difference as file truncation will not be used.
  704. $can_truncate = 1;
  705. # Customizable notification messages, logging
  706. $syslog_ident = 'amavis';
  707. $syslog_facility = 'mail';
  708. $log_level = 0;
  709. # should be less than (1023 - prefix), i.e. 980,
  710. # to avoid syslog truncating lines; see sub write_log
  711. $logline_maxlen = 980;
  712. $nanny_details_level = 1; # register_proc verbosity: 0, 1, 2
  713. # $inner_sock_specs in amavis-services should match one of the sockets
  714. # in the @zmq_sockets list
  715. # @zmq_sockets = ( "ipc://$MYHOME/amavisd-zmq.sock" ); # after-default
  716. # $enable_zmq = undef; # load optional module Amavis::ZMQ
  717. # # (interface to 0MQ or Crossroads I/O)
  718. # $enable_db = undef; # load optional modules Amavis::DB & Amavis::DB::SNMP
  719. # $enable_dkim_signing = undef;
  720. # $enable_dkim_verification = undef;
  721. $reputation_factor = 0.2; # a value between 0 and 1, controlling the amount
  722. # of 'bending' of a calculated spam score towards a fixed score assigned
  723. # to a signing domain (its 'reputation') through @signer_reputation_maps;
  724. # the formula is: adjusted_spam_score = f*reputation + (1-f)*spam_score;
  725. # which has the same semantics as auto_whitelist_factor in SpamAssassin AWL
  726. $database_sessions_persistent = 1; # keep SQL & LDAP sessions open when idle
  727. $lookup_maps_imply_sql_and_ldap = 1; # set to 0 to disable
  728. # Algorithm name for generating a mail header digest and a mail body digest:
  729. # either 'MD5' (will use Digest::MD5, fastest and smallest digest), or
  730. # anything else accepted by Digest::SHA->new(), e.g. 'SHA-1' or 'SHA-256'.
  731. # The generated digest may end up as part of a quarantine file name
  732. # or via macro %b in log or notification templates.
  733. #
  734. $mail_digest_algorithm = 'MD5'; # or 'SHA-1' or 'SHA-256', ...
  735. # Where to find SQL server(s) and database to support SQL lookups?
  736. # A list of triples: (dsn,user,passw). Specify more than one
  737. # for multiple (backup) SQL servers.
  738. #
  739. #@storage_sql_dsn =
  740. #@lookup_sql_dsn =
  741. # ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'],
  742. # ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] );
  743. # Does a database mail address field with no '@' character represent a
  744. # local username or a domain name? By default it implies a username in
  745. # SQL and LDAP lookups (but represents a domain in hash and acl lookups),
  746. # so domain names in SQL and LDAP should be specified as '@domain'.
  747. # Setting these to true will cause 'xxx' to be interpreted as a domain
  748. # name, just like in hash or acl lookups.
  749. #
  750. $sql_lookups_no_at_means_domain = 0;
  751. $ldap_lookups_no_at_means_domain = 0;
  752. # Maximum size (in bytes) for data written to a field 'quarantine.mail_text'
  753. # when quarantining to SQL. Must not exceed size allowed for a data type
  754. # on a given SQL server. It also determines a buffer size in amavisd.
  755. # Too large a value may exceed process virtual memory limits or just waste
  756. # memory, too small a value splits large mail into too many chunks, which
  757. # may be less efficient to process.
  758. #
  759. $sql_quarantine_chunksize_max = 16384;
  760. $sql_allow_8bit_address = 0;
  761. # the length of mail_id in bits, must be an integral multiple of 24
  762. # (i.e. divisible by 6 and 8); the mail_id is represented externally
  763. # as a base64url-encoded string of size $mail_id_size_bits / 6
  764. #
  765. $mail_id_size_bits = 72; # 24, 48, 72, 96
  766. $sql_store_info_for_all_msgs = 1;
  767. $penpals_bonus_score = undef; # maximal (positive) score value by which spam
  768. # score is lowered when sender is known to have previously received mail
  769. # from our local user from this mail system. Zero or undef disables
  770. # pen pals lookups in SQL tables msgs and msgrcpt, and is a default.
  771. $penpals_halflife = 7*24*60*60; # exponential decay time constant in seconds;
  772. # pen pal bonus is halved for each halflife period since the last mail
  773. # sent by a local user to a current message's sender
  774. $penpals_threshold_low = 1.0; # SA score below which pen pals lookups are
  775. # not performed to save time; undef lets the threshold be ignored;
  776. $penpals_threshold_high = undef;
  777. # when (SA_score - $penpals_bonus_score > $penpals_threshold_high)
  778. # pen pals lookup will not be performed to save time, as it could not
  779. # influence blocking of spam even at maximal penpals bonus (age=0);
  780. # usual choice for value would be kill level or other reasonably high
  781. # value; undef lets the threshold be ignored and is a default (useful
  782. # for testing and statistics gathering);
  783. $bounce_killer_score = 0;
  784. #
  785. # Receiving mail related
  786. # $unix_socketname = '/var/amavis/amavisd.sock'; # e.g. milter or release
  787. # $inet_socket_port = 10024; # accept SMTP on this TCP port
  788. # $inet_socket_port = [10024,10026,10027]; # ...possibly on more than one
  789. $AF_INET6 = eval { require Socket; Socket::AF_INET6() } ||
  790. eval { require Socket6; Socket6::AF_INET6() };
  791. # prefer using IO::Socket::IP if it exists, otherwise
  792. # fall back to IO::Socket::INET6 or IO::Socket::INET as appropriate
  793. #
  794. $have_socket_ip = eval {
  795. require IO::Socket::IP;
  796. };
  797. $have_inet4 = # can we make a PF_INET socket?
  798. $have_socket_ip ? eval {
  799. my $sock = IO::Socket::IP->new(LocalAddr => '0.0.0.0', Proto => 'udp');
  800. $sock->close or die "error closing inet6 socket: $!" if $sock;
  801. $sock ? 1 : undef;
  802. } : eval {
  803. require IO::Socket::INET;
  804. my $sock = IO::Socket::INET->new(LocalAddr => '0.0.0.0', Proto => 'udp');
  805. $sock->close or die "error closing inet socket: $!" if $sock;
  806. $sock ? 1 : undef;
  807. };
  808. $have_inet6 = # can we make a PF_INET6 socket?
  809. $have_socket_ip ? eval {
  810. my $sock = IO::Socket::IP->new(LocalAddr => '::', Proto => 'udp');
  811. $sock->close or die "error closing inet6 socket: $!" if $sock;
  812. $sock ? 1 : undef;
  813. } : eval {
  814. require IO::Socket::INET6;
  815. my $sock = IO::Socket::INET6->new(LocalAddr => '::', Proto => 'udp');
  816. $sock->close or die "error closing inet6 socket: $!" if $sock;
  817. $sock ? 1 : undef;
  818. };
  819. # bind socket to a loopback interface
  820. if (Net::Server->VERSION < 2) {
  821. $inet_socket_bind = '127.0.0.1';
  822. } else { # requires Net::Server 2 or a patched 0.99 with IPv6 support)
  823. $inet_socket_bind = $have_inet4 && $have_inet6 ? ['127.0.0.1', '[::1]']
  824. : $have_inet6 ? '[::1]' : '127.0.0.1';
  825. }
  826. @inet_acl = qw( 127.0.0.1 [::1] ); # allow SMTP access only from localhost
  827. @mynetworks = qw( 127.0.0.0/8 [::1] [FE80::]/10 [FEC0::]/10
  828. 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 );
  829. $originating = 0; # a boolean, initially reflects @mynetworks,
  830. # but may be modified later through a policy bank
  831. $forward_method = $have_inet6 && !$have_inet4 ? 'smtp:[::1]:10025'
  832. : 'smtp:[127.0.0.1]:10025';
  833. $notify_method = $forward_method;
  834. $resend_method = undef; # overrides $forward_method on defanging if nonempty
  835. $release_method = undef; # overrides $notify_method on releasing
  836. # from quarantine if nonempty
  837. $requeue_method = # requeuing release from a quarantine
  838. $have_inet6 && !$have_inet4 ? 'smtp:[::1]:25' : 'smtp:[127.0.0.1]:25';
  839. $release_format = 'resend'; # (dsn), (arf), attach, plain, resend
  840. $report_format = 'arf'; # (dsn), arf, attach, plain, resend
  841. # when $release_format is 'attach', the following control the attachment:
  842. $attachment_password = ''; # '': no pwd; undef: PIN; code ref; or static str
  843. $attachment_email_name = 'msg-%m.eml';
  844. $attachment_outer_name = 'msg-%m.zip';
  845. $virus_quarantine_method = 'local:virus-%m';
  846. $banned_files_quarantine_method = 'local:banned-%m';
  847. $spam_quarantine_method = 'local:spam-%m.gz';
  848. $bad_header_quarantine_method = 'local:badh-%m';
  849. $unchecked_quarantine_method = undef; # 'local:unchecked-%m';
  850. $clean_quarantine_method = undef; # 'local:clean-%m';
  851. $archive_quarantine_method = undef; # 'local:archive-%m.gz';
  852. $prepend_header_fields_hdridx = 0; # normally 0, use 1 for co-existence
  853. # with signing DK and DKIM milters
  854. $remove_existing_x_scanned_headers = 0;
  855. $remove_existing_spam_headers = 1;
  856. # fix improper header fields in passed or released mail - this setting
  857. # is a pre-condition for $allow_fixing_improper_header_folding and similar
  858. # (future) fixups; (desirable, but may break DKIM validation of messages
  859. # with illegal header section)
  860. $allow_fixing_improper_header = 1;
  861. # fix improper folded header fields made up entirely of whitespace, by
  862. # removing all-whitespace lines ($allow_fixing_improper_header must be true)
  863. $allow_fixing_improper_header_folding = 1;
  864. # truncate header section lines longer than 998 characters as limited
  865. # by the RFC 5322 ($allow_fixing_improper_header must be true)
  866. $allow_fixing_long_header_lines = 1;
  867. # encoding (charset in MIME terminology)
  868. # to be used in RFC 2047-encoded ...
  869. # $hdr_encoding = 'iso-8859-1'; # ... header field bodies
  870. # $bdy_encoding = 'iso-8859-1'; # ... notification body text
  871. $hdr_encoding = 'UTF-8'; # ... header field bodies
  872. $bdy_encoding = 'UTF-8'; # ... notification body text
  873. # encoding (encoding in MIME terminology)
  874. $hdr_encoding_qb = 'Q'; # quoted-printable (default)
  875. #$hdr_encoding_qb = 'B'; # base64 (usual for far east charsets)
  876. $smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit
  877. # $myhostname is used by SMTP server module in the initial SMTP welcome line,
  878. # in inserted Received: lines, Message-ID in notifications, log entries, ...
  879. $myhostname = (POSIX::uname)[1]; # should be a FQDN !
  880. $snmp_contact = ''; # a value of sysContact OID
  881. $snmp_location = ''; # a value of sysLocation OID
  882. $smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready';
  883. $smtpd_quit_banner = '${helo-name} ${product} closing transmission channel';
  884. $enforce_smtpd_message_size_limit_64kb_min = 1;
  885. # $localhost_name is the name of THIS host running amavisd
  886. # (typically 'localhost'). It is used in HELO SMTP command
  887. # when reinjecting mail back to MTA via SMTP for final delivery,
  888. # and in inserted Received header field
  889. $localhost_name = 'localhost';
  890. $propagate_dsn_if_possible = 1; # pass on DSN if MTA announces this
  891. # capability; useful to be turned off globally but enabled in
  892. # MYNETS policy bank to hide internal mail routing from outsiders
  893. $terminate_dsn_on_notify_success = 0; # when true=>handle DSN NOTIFY=SUCCESS
  894. # locally, do not let NOTIFY=SUCCESS propagate to MTA (but allow
  895. # other DSN options like NOTIFY=NEVER/FAILURE/DELAY, ORCPT, RET,
  896. # and ENVID to propagate if possible)
  897. #@auth_mech_avail = ('PLAIN','LOGIN'); # empty list disables incoming AUTH
  898. #$auth_required_inp = 1; # incoming SMTP authentication required by amavisd?
  899. #$auth_required_out = 1; # SMTP authentication required by MTA
  900. $auth_required_release = 1; # secret_id is required for a quarantine release
  901. $tls_security_level_in = undef; # undef, 'may', 'encrypt', ...
  902. $tls_security_level_out = undef; # undef, 'may', 'encrypt', ...
  903. $smtpd_tls_cert_file = undef; # e.g. "$MYHOME/cert/amavisd-cert.pem"
  904. $smtpd_tls_key_file = undef; # e.g. "$MYHOME/cert/amavisd-key.pem"
  905. # SMTP AUTH username and password for notification submissions
  906. # (and reauthentication of forwarded mail if requested)
  907. #$amavis_auth_user = undef; # perhaps: 'amavisd'
  908. #$amavis_auth_pass = undef;
  909. #$auth_reauthenticate_forwarded = undef; # supply our own credentials also
  910. # for forwarded (passed) mail
  911. $smtp_connection_cache_on_demand = 1;
  912. $smtp_connection_cache_enable = 1;
  913. # whom quarantined messages appear to be sent from (envelope sender)
  914. # $mailfrom_to_quarantine = undef; # orig. sender if undef, or set explicitly
  915. # where to send quarantined malware - specify undef to disable, or an
  916. # e-mail address containing '@', or just a local part, which will be
  917. # mapped by %local_delivery_aliases into local mailbox name or directory.
  918. # The lookup key is a recipient address
  919. $virus_quarantine_to = 'virus-quarantine';
  920. $banned_quarantine_to = 'banned-quarantine';
  921. $unchecked_quarantine_to = 'unchecked-quarantine';
  922. $spam_quarantine_to = 'spam-quarantine';
  923. $bad_header_quarantine_to = 'bad-header-quarantine';
  924. $clean_quarantine_to = 'clean-quarantine';
  925. $archive_quarantine_to = 'archive-quarantine';
  926. # similar to $spam_quarantine_to, but the lookup key is the sender address:
  927. $spam_quarantine_bysender_to = undef; # dflt: no by-sender spam quarantine
  928. # quarantine directory or mailbox file or empty
  929. # (only used if $*_quarantine_to specifies direct local delivery)
  930. $QUARANTINEDIR = undef; # no quarantine unless overridden by config
  931. $undecipherable_subject_tag = '***UNCHECKED*** ';
  932. # NOTE: all entries can accept mail_body_size_limit and score_factor options
  933. @spam_scanners = (
  934. ['SpamAssassin', 'Amavis::SpamControl::SpamAssassin' ],
  935. # ['SpamdClient', 'Amavis::SpamControl::SpamdClient',
  936. # mail_body_size_limit => 65000, score_factor => 1.0,
  937. # ],
  938. # ['DSPAM', 'Amavis::SpamControl::ExtProg', $dspam,
  939. # [ qw(--stdout --classify --deliver=innocent,spam
  940. # --mode=toe --feature noise
  941. # --user), $daemon_user ],
  942. # mail_body_size_limit => 65000, score_factor => 1.0,
  943. # ],
  944. # ['CRM114', 'Amavis::SpamControl::ExtProg', 'crm',
  945. # [ qw(-u /var/amavis/home/.crm114 mailreaver.crm
  946. # --dontstore --report_only --stats_only
  947. # --good_threshold=10 --spam_threshold=-10) ],
  948. # mail_body_size_limit => 65000, score_factor => -0.20,
  949. # lock_file => '/var/amavis/crm114.lock',
  950. # lock_type => 'shared', learner_lock_type => 'exclusive',
  951. # ],
  952. # ['Bogofilter', 'Amavis::SpamControl::ExtProg', 'bogofilter',
  953. # [ qw(-e -v)], # -u
  954. # mail_body_size_limit => 65000, score_factor => 1.0,
  955. # ],
  956. );
  957. $sa_spawned = 0; # true: run SA in a subprocess; false: call SA directly
  958. # string to prepend to Subject header field when message qualifies as spam
  959. # $sa_spam_subject_tag1 = undef; # example: '***Possible Spam*** '
  960. # $sa_spam_subject_tag = undef; # example: '***Spam*** '
  961. $sa_spam_level_char = '*'; # character to be used in X-Spam-Level bar;
  962. # empty or undef disables adding this header field
  963. $sa_num_instances = 1; # number of SA instances,
  964. # usually 1, memory-expensive, keep small
  965. $sa_local_tests_only = 0;
  966. $sa_debug = undef;
  967. $sa_timeout = 30; # no longer used since 2.6.5
  968. $file = 'file'; # path to the file(1) utility for classifying contents
  969. $altermime = 'altermime'; # path to the altermime utility (optional)
  970. @altermime_args_defang = qw(--verbose --removeall);
  971. @altermime_args_disclaimer = qw(--disclaimer=/etc/altermime-disclaimer.txt);
  972. # @altermime_args_disclaimer =
  973. # qw(--disclaimer=/etc/_OPTION_.txt --disclaimer-html=/etc/_OPTION_.html);
  974. # @disclaimer_options_bysender_maps = ( 'altermime-disclaimer' );
  975. $MIN_EXPANSION_FACTOR = 5; # times original mail size
  976. $MAX_EXPANSION_FACTOR = 500; # times original mail size
  977. # See amavisd.conf and README.lookups for details.
  978. # What to do with the message (this is independent of quarantining):
  979. # Reject: tell MTA to generate a non-delivery notification, MTA gets 5xx
  980. # Bounce: generate a non-delivery notification by ourselves, MTA gets 250
  981. # Discard: drop the message and pretend it was delivered, MTA gets 250
  982. # Pass: accept/forward a message, MTA gets 250
  983. # TempFail: temporary failure, client should retry, MTA gets 4xx
  984. #
  985. # COMPATIBILITY NOTE: the separation of *_destiny values into
  986. # D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warn*sender only
  987. # still useful with D_PASS. The combination of D_DISCARD + $warn*sender=1
  988. # is mapped into D_BOUNCE for compatibility.
  989. # The following symbolic constants can be used in *destiny settings:
  990. #
  991. # D_PASS mail will pass to recipients, regardless of contents;
  992. #
  993. # D_DISCARD mail will not be delivered to its recipients, sender will NOT be
  994. # notified. Effectively we lose mail (but it will be quarantined
  995. # unless disabled).
  996. #
  997. # D_BOUNCE mail will not be delivered to its recipients, a non-delivery
  998. # notification (bounce) will be sent to the sender by amavisd-new
  999. # (unless suppressed). Bounce (DSN) will not be sent if a virus
  1000. # name matches $viruses_that_fake_sender_maps, or to messages
  1001. # from mailing lists (Precedence: bulk|list|junk), or for spam
  1002. # exceeding spam_dsn_cutoff_level
  1003. #
  1004. # D_REJECT mail will not be delivered to its recipients, amavisd will
  1005. # return a 5xx status response. Depending on an MTA/amavisd setup
  1006. # this will result either in a reject status passed back to a
  1007. # connecting SMTP client (in a pre-queue setup: proxy or milter),
  1008. # or an MTA will generate a bounce in a post-queue setup.
  1009. # If not all recipients agree on rejecting a message (like when
  1010. # different recipients have different thresholds on bad mail
  1011. # contents and LMTP is not used) amavisd sends a bounce by itself
  1012. # (same as D_BOUNCE).
  1013. #
  1014. # D_TEMPFAIL indicates a temporary failure, mail will not be delivered to
  1015. # its recipients, sender should retry the operation later.
  1016. #
  1017. # Notes:
  1018. # D_REJECT and D_BOUNCE are similar,the difference is in who is responsible
  1019. # for informing the sender about non-delivery, and how informative
  1020. # the notification can be (amavisd-new knows more than MTA);
  1021. # With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status
  1022. # notification, colloquially called 'bounce') - depending on MTA
  1023. # and its interface to a content checker; best suited for sendmail
  1024. # milter or other pre-queue filtering setups
  1025. # With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the
  1026. # reason for mail non-delivery but unable to reject the original
  1027. # SMTP session, and is in position to suppress DSN if considered
  1028. # unsuitable). Best suited for Postfix and other dual-MTA setups.
  1029. # Exceeded spam cutoff limit or faked virus sender implicitly
  1030. # turns D_BOUNCE into a D_DISCARD;
  1031. # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS, D_TEMPFAIL
  1032. $final_virus_destiny = D_DISCARD;
  1033. $final_banned_destiny = D_DISCARD;
  1034. $final_unchecked_destiny = D_PASS;
  1035. $final_spam_destiny = D_PASS;
  1036. $final_bad_header_destiny = D_PASS;
  1037. # If decided to pass viruses (or spam) to certain recipients using
  1038. # %lovers_maps_by_ccat, or by %final_destiny_by_ccat resulting in D_PASS,
  1039. # one may set the corresponding %addr_extension_maps_by_ccat to some string,
  1040. # and the recipient address will have this string appended as an address
  1041. # extension to a local-part (mailbox part) of the address. This extension
  1042. # can be used by a final local delivery agent for example to place such mail
  1043. # in different folder. Leaving this variable undefined or an empty string
  1044. # prevents appending address extension. Recipients which do not match
  1045. # @local_domains_maps are not affected (i.e. non-local recipients (=outbound
  1046. # mail) do not get address extension appended).
  1047. #
  1048. # LDAs usually default to stripping away address extension if no special
  1049. # handling for it is specified, so having this option enabled normally
  1050. # does no harm, provided the $recipients_delimiter character matches
  1051. # the setting at the final MTA's local delivery agent (LDA).
  1052. #
  1053. # $addr_extension_virus = 'virus'; # for example
  1054. # $addr_extension_spam = 'spam';
  1055. # $addr_extension_banned = 'banned';
  1056. # $addr_extension_bad_header = 'badh';
  1057. # Delimiter between local part of the recipient address and address extension
  1058. # (which can optionally be added, see variable %addr_extension_maps_by_ccat.
  1059. # E.g. recipient address <user@domain.example> gets
  1060. # changed to <user+virus@domain.example>.
  1061. #
  1062. # Delimiter should match an equivalent (final) MTA delimiter setting.
  1063. # (e.g. for Postfix add 'recipient_delimiter = +' to main.cf).
  1064. # Setting it to an empty string or to undef disables this feature
  1065. # regardless of %addr_extension_maps_by_ccat setting.
  1066. # $recipient_delimiter = '+';
  1067. $replace_existing_extension = 1; # true: replace ext; false: append ext
  1068. # Affects matching of localpart of e-mail addresses (left of '@')
  1069. # in lookups: true = case sensitive, false = case insensitive
  1070. $localpart_is_case_sensitive = 0;
  1071. # Trim trailing whitespace from SQL fields, LDAP attribute values
  1072. # and hash righthand-sides as read by read_hash(); disabled by default;
  1073. # turn it on for compatibility with pre-2.4.0 versions.
  1074. $trim_trailing_space_in_lookup_result_fields = 0;
  1075. # since 2.7.0: deprecated some old variables:
  1076. #
  1077. deprecate_var('bool', '$insert_received_line', 1);
  1078. deprecate_var('bool', '$relayhost_is_client', undef);
  1079. deprecate_var('bool', '$warnvirussender', undef);
  1080. deprecate_var('bool', '$warnspamsender', undef);
  1081. deprecate_var('bool', '$sa_spam_report_header', undef);
  1082. deprecate_var('bool', '$sa_spam_modifies_subj', 1);
  1083. deprecate_var('bool', '$sa_auto_whitelist', undef);
  1084. deprecate_var('num', '$sa_timeout', 30);
  1085. deprecate_var('str', '$syslog_priority', 'debug');
  1086. deprecate_var('str', '$SYSLOG_LEVEL', 'mail.debug');
  1087. deprecate_var('str', '$notify_xmailer_header', undef);
  1088. # deprecate_var('array','@spam_modifies_subj_maps');
  1089. 1;
  1090. } # end BEGIN - init_secondary
  1091. # init structured variables like %sql_clause, $map_full_type_to_short_type_re,
  1092. # %ccat_display_names, @decoders, build default maps; may run as root!
  1093. #
  1094. BEGIN {
  1095. $allowed_added_header_fields{lc($_)} = 1 for qw(
  1096. Received DKIM-Signature Authentication-Results VBR-Info
  1097. X-Quarantine-ID X-Amavis-Alert X-Amavis-Hold X-Amavis-Modified
  1098. X-Amavis-PenPals X-Amavis-OS-Fingerprint X-Amavis-PolicyBank
  1099. X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score
  1100. X-Spam-Report X-Spam-Checker-Version X-Spam-Tests
  1101. X-CRM114-Status X-CRM114-CacheID X-CRM114-Notice X-CRM114-Action
  1102. X-DSPAM-Result X-DSPAM-Class X-DSPAM-Signature X-DSPAM-Processed
  1103. X-DSPAM-Confidence X-DSPAM-Probability X-DSPAM-User X-DSPAM-Factors
  1104. X-Bogosity
  1105. );
  1106. $allowed_added_header_fields{lc('X-Spam-Report')} = 0;
  1107. $allowed_added_header_fields{lc('X-Spam-Checker-Version')} = 0;
  1108. # $allowed_added_header_fields{lc(c(lc $X_HEADER_TAG))}=1; #later:read_config
  1109. # even though SpamAssassin does provide the following header fields, we
  1110. # prefer to provide our own version (per-recipient scores, version hiding);
  1111. # our own non-"X-Spam" header fields are always preferred and need not
  1112. # be listed here
  1113. $prefer_our_added_header_fields{lc($_)} = 1 for qw(
  1114. X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score X-Spam-Report
  1115. X-Spam-Checker-Version
  1116. X-CRM114-Status X-CRM114-CacheID X-DSPAM-Result X-DSPAM-Signature
  1117. );
  1118. # controls which header section tests are performed in check_header_validity,
  1119. # keys correspond to minor contents categories for CC_BADH
  1120. $allowed_header_tests{lc($_)} = 1 for qw(
  1121. other mime 8bit control empty long syntax missing multiple);
  1122. # RFC 4871 standard set of header fields to be signed:
  1123. my(@sign_headers) = qw(From Sender Reply-To Subject Date Message-ID To Cc
  1124. In-Reply-To References MIME-Version Content-Type Content-Transfer-Encoding
  1125. Content-ID Content-Description Resent-Date Resent-From Resent-Sender
  1126. Resent-To Resent-Cc Resent-Message-ID List-Id List-Post List-Owner
  1127. List-Subscribe List-Unsubscribe List-Help List-Archive);
  1128. # additional header fields considered appropriate, see also RFC 4021
  1129. # and IANA registry "Permanent Message Header Field Names";
  1130. # see RFC 3834 for Auto-Submitted; RFC 5518 for VBR-Info (Vouch By Reference)
  1131. push(@sign_headers, qw(Received Precedence
  1132. Original-Message-ID Message-Context PICS-Label Sensitivity Solicitation
  1133. Content-Location Content-Features Content-Disposition Content-Language
  1134. Content-Alternative Content-Base Content-MD5 Content-Duration Content-Class
  1135. Accept-Language Auto-Submitted Archived-At VBR-Info));
  1136. # note that we are signing Received despite the advise in RFC 4871;
  1137. # some additional nonstandard header fields:
  1138. push(@sign_headers, qw(Organization Organisation User-Agent X-Mailer));
  1139. $signed_header_fields{lc($_)} = 1 for @sign_headers;
  1140. # Excluded:
  1141. # DKIM-Signature DomainKey-Signature Authentication-Results
  1142. # Keywords Comments Errors-To X-Virus-Scanned X-Archived-At X-No-Archive
  1143. # Some MTAs are dropping Disposition-Notification-To, exclude:
  1144. # Disposition-Notification-To Disposition-Notification-Options
  1145. # Some mail scanners are dropping Return-Receipt-To, exclude it.
  1146. # Signing a 'Sender' may not be a good idea because when such mail is sent
  1147. # through a mailing list, this header field is usually replaced by a new one,
  1148. # invalidating a signature. Long To and Cc address lists are often mangled,
  1149. # especially when containing non-encoded display names. Off: Sender, To, Cc
  1150. $signed_header_fields{lc($_)} = 0 for qw(Sender To Cc);
  1151. #
  1152. # a value greater than 1 causes signing of one additional null instance of
  1153. # a header field, thus prohibiting prepending additional occurrences of such
  1154. # header field without breaking a signature
  1155. $signed_header_fields{lc($_)} = 2 for qw(From Date Subject Content-Type);
  1156. # provide names for content categories - to be used only for logging,
  1157. # SNMP counter names and display purposes
  1158. %ccat_display_names = (
  1159. CC_CATCHALL, 'CatchAll', # last resort, should not normally appear
  1160. CC_CLEAN, 'Clean',
  1161. CC_CLEAN.',1', 'CleanTag', # tag_level
  1162. CC_MTA, 'MtaFailed', # unable to forward (general)
  1163. CC_MTA.',1', 'MtaTempFailed', # MTA response was 4xx
  1164. CC_MTA.',2', 'MtaRejected', # MTA response was 5xx
  1165. CC_OVERSIZED, 'Oversized',
  1166. CC_BADH, 'BadHdr',
  1167. CC_BADH.',1', 'BadHdrMime',
  1168. CC_BADH.',2', 'BadHdr8bit',
  1169. CC_BADH.',3', 'BadHdrChar',
  1170. CC_BADH.',4', 'BadHdrSpace',
  1171. CC_BADH.',5', 'BadHdrLong',
  1172. CC_BADH.',6', 'BadHdrSyntax',
  1173. CC_BADH.',7', 'BadHdrMissing',
  1174. CC_BADH.',8', 'BadHdrDupl',
  1175. CC_SPAMMY, 'Spammy', # tag2_level
  1176. CC_SPAMMY.',1','Spammy3', # tag3_level
  1177. CC_SPAM, 'Spam', # kill_level
  1178. CC_UNCHECKED, 'Unchecked',
  1179. CC_BANNED, 'Banned',
  1180. CC_VIRUS, 'Virus',
  1181. );
  1182. # provide names for content categories - to be used only for logging,
  1183. # SNMP counter names and display purposes, similar to %ccat_display_names
  1184. # but only major contents category names are listed
  1185. %ccat_display_names_major = (
  1186. CC_CATCHALL, 'CatchAll', # last resort, should not normally appear
  1187. CC_CLEAN, 'Clean',
  1188. CC_MTA, 'MtaFailed', # unable to forward
  1189. CC_OVERSIZED, 'Oversized',
  1190. CC_BADH, 'BadHdr',
  1191. CC_SPAMMY, 'Spammy', # tag2_level
  1192. CC_SPAM, 'Spam', # kill_level
  1193. CC_UNCHECKED, 'Unchecked',
  1194. CC_BANNED, 'Banned',
  1195. CC_VIRUS, 'Virus',
  1196. );
  1197. # $partition_tag is a user-specified SQL field value in tables maddr, msgs,
  1198. # msgrcpt and quarantine, inserted into new records, but can be useful even
  1199. # without SQL, accessible through a macro %P and in quarantine templates.
  1200. # It is usually an integer, but depending on a schema may be of other data
  1201. # type e.g. a string. May be used to speed up purging of old records by using
  1202. # partitioned tables (MySQL 5.1+, PostgreSQL 8.1+). A possible usage can
  1203. # be a week-of-a-year, or some other slowly changing value, allowing to
  1204. # quickly drop old table partitions without wasting time on deleting
  1205. # individual records. Mail addresses in table maddr are self-contained
  1206. # within a partition tag, which means that the same mail address may
  1207. # appear in more than one maddr partition (using different 'id's), and
  1208. # that tables msgs and msgrcpt are guaranteed to reference a maddr.id
  1209. # within their own partition tag. The $partition_tag may be a scalar
  1210. # (an integer or a string), or a reference to a subroutine, which will be
  1211. # called with an object of type Amavis::In::Message as argument, and its
  1212. # result will be used as a partition tag value. Possible usage:
  1213. #
  1214. # $partition_tag =
  1215. # sub { my($msginfo)=@_; iso8601_week($msginfo->rx_time) };
  1216. #or:
  1217. # $partition_tag =
  1218. # sub { my($msginfo)=@_; iso8601_yearweek($msginfo->rx_time) };
  1219. #
  1220. #or based on a day of a week for short-term cycling (Mo=1, Tu=2,... Su=7):
  1221. # $partition_tag =
  1222. # sub { my($msginfo)=@_; iso8601_weekday($msginfo->rx_time) };
  1223. #
  1224. # $spam_quarantine_method = 'local:W%P/spam/%m.gz'; # quar dir by week num
  1225. # The SQL select clause to fetch per-recipient policy settings.
  1226. # The %k will be replaced by a comma-separated list of query addresses
  1227. # for a recipient (e.g. a full address, domain only, catchall), %a will be
  1228. # replaced by an exact recipient address (same as the first entry in %k,
  1229. # suitable for pattern matching), %l by a full unmodified localpart, %u by
  1230. # a lowercased username (a localpart without extension), %e by lowercased
  1231. # addr extension (which includes a delimiter), and %d for lowercased domain.
  1232. # Use ORDER if there is a chance that multiple records will match - the
  1233. # first match wins (i.e. the first returned record). If field names are
  1234. # not unique (e.g. 'id'), the later field overwrites the earlier in a hash
  1235. # returned by lookup, which is why we use 'users.*, policy.*, users.id',
  1236. # i.e. the id is repeated at the end.
  1237. # This is a legacy variable for upwards compatibility, now only referenced
  1238. # by the program through a %sql_clause entry 'sel_policy' - newer config
  1239. # files may assign directly to $sql_clause{'sel_policy'} if preferred.
  1240. #
  1241. $sql_select_policy =
  1242. 'SELECT users.*, policy.*, users.id'.
  1243. ' FROM users LEFT JOIN policy ON users.policy_id=policy.id'.
  1244. ' WHERE users.email IN (%k) ORDER BY users.priority DESC';
  1245. # Btw, MySQL and PostgreSQL are happy with 'SELECT *, users.id',
  1246. # but Oracle wants 'SELECT users.*, policy.*, users.id', which is
  1247. # also acceptable to MySQL and PostgreSQL.
  1248. # The SQL select clause to check sender in per-recipient whitelist/blacklist.
  1249. # The first SELECT argument '?' will be users.id from recipient SQL lookup,
  1250. # the %k will be replaced by a comma-separated list of query addresses
  1251. # for a sender (e.g. a full address, domain only, catchall), %a will be
  1252. # replaced by an exact sender address (same as the first entry in %k,
  1253. # suitable for pattern matching), %l by a full unmodified localpart, %u by
  1254. # a lowercased username (a localpart without extension), %e by lowercased
  1255. # addr extension (which includes a delimiter), and %d for lowercased domain.
  1256. # Only the first occurrence of '?' will be replaced by users.id,
  1257. # subsequent occurrences of '?' will see empty string as an argument.
  1258. # There can be zero or more occurrences of each %k, %a, %l, %u, %e, %d,
  1259. # lookup keys will be replicated accordingly.
  1260. # This is a separate legacy variable for upwards compatibility, now only
  1261. # referenced by the program through %sql_clause entry 'sel_wblist' - newer
  1262. # config files may assign directly to $sql_clause{'sel_wblist'} if preferred.
  1263. #
  1264. $sql_select_white_black_list =
  1265. 'SELECT wb FROM wblist JOIN mailaddr ON wblist.sid=mailaddr.id'.
  1266. ' WHERE wblist.rid=? AND mailaddr.email IN (%k)'.
  1267. ' ORDER BY mailaddr.priority DESC';
  1268. %sql_clause = (
  1269. 'sel_policy' => \$sql_select_policy,
  1270. 'sel_wblist' => \$sql_select_white_black_list,
  1271. 'sel_adr' =>
  1272. 'SELECT id FROM maddr WHERE partition_tag=? AND email=?',
  1273. 'ins_adr' =>
  1274. 'INSERT INTO maddr (partition_tag, email, domain) VALUES (?,?,?)',
  1275. 'ins_msg' =>
  1276. 'INSERT INTO msgs (partition_tag, mail_id, secret_id, am_id,'.
  1277. ' time_num, time_iso, sid, policy, client_addr, size, host)'.
  1278. ' VALUES (?,?,?,?,?,?,?,?,?,?,?)',
  1279. 'upd_msg' =>
  1280. 'UPDATE msgs SET content=?, quar_type=?, quar_loc=?, dsn_sent=?,'.
  1281. ' spam_level=?, message_id=?, from_addr=?, subject=?, client_addr=?,'.
  1282. ' originating=?'.
  1283. ' WHERE partition_tag=? AND mail_id=?',
  1284. 'ins_rcp' =>
  1285. 'INSERT INTO msgrcpt (partition_tag, mail_id, rseqnum, rid, is_local,'.
  1286. ' content, ds, rs, bl, wl, bspam_level, smtp_resp)'.
  1287. ' VALUES (?,?,?,?,?,?,?,?,?,?,?,?)',
  1288. 'ins_quar' =>
  1289. 'INSERT INTO quarantine (partition_tag, mail_id, chunk_ind, mail_text)'.
  1290. ' VALUES (?,?,?,?)',
  1291. 'sel_msg' => # obtains partition_tag if missing in a release request
  1292. 'SELECT partition_tag FROM msgs WHERE mail_id=?',
  1293. 'sel_quar' =>
  1294. 'SELECT mail_text FROM quarantine'.
  1295. ' WHERE partition_tag=? AND mail_id=?'.
  1296. ' ORDER BY chunk_ind',
  1297. 'sel_penpals' => # no message-id references list
  1298. "SELECT msgs.time_num, msgs.mail_id, subject".
  1299. " FROM msgs JOIN msgrcpt USING (partition_tag,mail_id)".
  1300. " WHERE sid=? AND rid=? AND msgs.content!='V' AND ds='P'".
  1301. " ORDER BY msgs.time_num DESC", # LIMIT 1
  1302. 'sel_penpals_msgid' => # with a nonempty list of message-id references
  1303. "SELECT msgs.time_num, msgs.mail_id, subject, message_id, rid".
  1304. " FROM msgs JOIN msgrcpt USING (partition_tag,mail_id)".
  1305. " WHERE sid=? AND msgs.content!='V' AND ds='P' AND message_id IN (%m)".
  1306. " AND rid!=sid".
  1307. " ORDER BY rid=? DESC, msgs.time_num DESC", # LIMIT 1
  1308. );
  1309. # NOTE on $sql_clause{'upd_msg'}: MySQL clobbers timestamp on update
  1310. # (unless DEFAULT 0 is used) setting it to current local time and
  1311. # losing the cherishly preserved and prepared time of mail reception.
  1312. # From the MySQL 4.1 documentation:
  1313. # * With neither DEFAULT nor ON UPDATE clauses, it is the same as
  1314. # DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.
  1315. # * suppress the automatic initialization and update behaviors for the first
  1316. # TIMESTAMP column by explicitly assigning it a constant DEFAULT value
  1317. # (for example, DEFAULT 0)
  1318. # * The first TIMESTAMP column in table row automatically is updated to
  1319. # the current timestamp when the value of any other column in the row is
  1320. # changed, unless the TIMESTAMP column explicitly is assigned a value
  1321. # other than NULL.
  1322. # maps full string as returned by a file(1) utility into a short string;
  1323. # first match wins, more specific entries should precede general ones!
  1324. # the result may be a string or a ref to a list of strings;
  1325. # see also sub decompose_part()
  1326. # prepare an arrayref, later to be converted to an Amavis::Lookup::RE object
  1327. $map_full_type_to_short_type_re = [
  1328. [qr/^empty\z/ => 'empty'],
  1329. [qr/^directory\z/ => 'dir'],
  1330. [qr/^can't (stat|read)\b/ => 'dat'], # file(1) diagnostics
  1331. [qr/^cannot open\b/ => 'dat'], # file(1) diagnostics
  1332. [qr/^ERROR:/ => 'dat'], # file(1) diagnostics
  1333. [qr/can't read magic file|couldn't find any magic files/ => 'dat'],
  1334. [qr/^data\z/ => 'dat'],
  1335. [qr/^ISO-8859.*\btext\b/ => 'txt'],
  1336. [qr/^Non-ISO.*ASCII\b.*\btext\b/ => 'txt'],
  1337. [qr/^Unicode\b.*\btext\b/i => 'txt'],
  1338. [qr/^UTF.* Unicode text\b/i => 'txt'],
  1339. [qr/^'diff' output text\b/ => 'txt'],
  1340. [qr/^GNU message catalog\b/ => 'mo'],
  1341. [qr/^PGP encrypted data\b/ => 'pgp'],
  1342. [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ],
  1343. [qr/^PGP armored\b/ => ['pgp','pgp.asc'] ],
  1344. ### 'file' is a bit too trigger happy to claim something is 'mail text'
  1345. # [qr/^RFC 822 mail text\b/ => 'mail'],
  1346. [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'],
  1347. [qr/^JPEG image data\b/ => ['image','jpg'] ],
  1348. [qr/^GIF image data\b/ => ['image','gif'] ],
  1349. [qr/^PNG image data\b/ => ['image','png'] ],
  1350. [qr/^TIFF image data\b/ => ['image','tif'] ],
  1351. [qr/^PCX\b.*\bimage data\b/ => ['image','pcx'] ],
  1352. [qr/^PC bitmap data\b/ => ['image','bmp'] ],
  1353. [qr/^SVG Scalable Vector Graphics image\b/ => ['image','svg'] ],
  1354. [qr/^MP2\b/ => ['audio','mpa','mp2'] ],
  1355. [qr/^MP3\b/ => ['audio','mpa','mp3'] ],
  1356. [qr/\bMPEG ADTS, layer III\b/ => ['audio','mpa','mp3'] ],
  1357. [qr/^ISO Media, MPEG v4 system, 3GPP\b/=> ['audio','mpa','3gpp'] ],
  1358. [qr/^ISO Media, MPEG v4 system\b/ => ['audio','mpa','m4a','m4b'] ],
  1359. [qr/^FLAC audio bitstream data\b/ => ['audio','flac'] ],
  1360. [qr/^Ogg data, FLAC audio\b/ => ['audio','oga'] ],
  1361. [qr/^Ogg data\b/ => ['audio','ogg'] ],
  1362. [qr/^MPEG video stream data\b/ => ['movie','mpv'] ],
  1363. [qr/^MPEG system stream data\b/ => ['movie','mpg'] ],
  1364. [qr/^MPEG\b/ => ['movie','mpg'] ],
  1365. [qr/^Matroska data\b/ => ['movie','mkv'] ],
  1366. [qr/^Microsoft ASF\b/ => ['movie','wmv'] ],
  1367. [qr/^RIFF\b.*\bAVI\b/ => ['movie','avi'] ],
  1368. [qr/^RIFF\b.*\banimated cursor\b/ => ['movie','ani'] ],
  1369. [qr/^RIFF\b.*\bWAVE audio\b/ => ['audio','wav'] ],
  1370. [qr/^Macromedia Flash data\b/ => 'swf'],
  1371. [qr/^HTML document text\b/ => 'html'],
  1372. [qr/^XML document text\b/ => 'xml'],
  1373. [qr/^exported SGML document text\b/ => 'sgml'],
  1374. [qr/^PostScript document text\b/ => 'ps'],
  1375. [qr/^PDF document\b/ => 'pdf'],
  1376. [qr/^Rich Text Format data\b/ => 'rtf'],
  1377. [qr/^Microsoft Office Document\b/i => 'doc'], # OLE2: doc, ppt, xls,...
  1378. [qr/^Microsoft Installer\b/i => 'doc'], # file(1) may misclassify
  1379. [qr/^ms-windows meta(file|font)\b/i => 'wmf'],
  1380. [qr/^LaTeX\b.*\bdocument text\b/ => 'lat'],
  1381. [qr/^TeX DVI file\b/ => 'dvi'],
  1382. [qr/\bdocument text\b/ => 'txt'],
  1383. [qr/^compiled Java class data\b/ => 'java'],
  1384. [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'],
  1385. [qr/^Compressed Google KML Document\b/ => 'kmz'],
  1386. [qr/^frozen\b/ => 'F'],
  1387. [qr/^gzip compressed\b/ => 'gz'],
  1388. [qr/^bzip compressed\b/ => 'bz'],
  1389. [qr/^bzip2 compressed\b/ => 'bz2'],
  1390. [qr/^xz compressed\b/ => 'xz'],
  1391. [qr/^lzma compressed\b/ => 'lzma'],
  1392. [qr/^lrz compressed\b/ => 'lrz'], #***(untested)
  1393. [qr/^lzop compressed\b/ => 'lzo'],
  1394. [qr/^compress'd/ => 'Z'],
  1395. [qr/^Zip archive\b/i => 'zip'],
  1396. [qr/^7-zip archive\b/i => '7z'],
  1397. [qr/^RAR archive\b/i => 'rar'],
  1398. [qr/^LHa.*\barchive\b/i => 'lha'], # (also known as .lzh)
  1399. [qr/^ARC archive\b/i => 'arc'],
  1400. [qr/^ARJ archive\b/i => 'arj'],
  1401. [qr/^Zoo archive\b/i => 'zoo'],
  1402. [qr/^(\S+\s+)?tar archive\b/i => 'tar'],
  1403. [qr/^(\S+\s+)?cpio archive\b/i => 'cpio'],
  1404. [qr/^StuffIt Archive\b/i => 'sit'],
  1405. [qr/^Debian binary package\b/i => 'deb'], # std. Unix archive (ar)
  1406. [qr/^current ar archive\b/i => 'a'], # std. Unix archive (ar)
  1407. [qr/^RPM\b/ => 'rpm'],
  1408. [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'],
  1409. [qr/^Microsoft Cabinet (file|archive)\b/i => 'cab'],
  1410. [qr/^InstallShield Cabinet file\b/ => 'installshield'],
  1411. [qr/^ISO 9660 CD-ROM filesystem\b/i => 'iso'],
  1412. [qr/^(uuencoded|xxencoded)\b/i => 'uue'],
  1413. [qr/^binhex\b/i => 'hqx'],
  1414. [qr/^(ASCII|text)\b/i => 'asc'],
  1415. [qr/^Emacs.*byte-compiled Lisp data/i => 'asc'], # BinHex with empty line
  1416. [qr/\bscript text executable\b/ => 'txt'],
  1417. [qr/^MS Windows\b.*\bDLL\b/ => ['exe','dll'] ],
  1418. [qr/\bexecutable for MS Windows\b.*\bDLL\b/ => ['exe','dll'] ],
  1419. [qr/^MS-DOS executable \(built-in\)/ => 'asc'], # starts with LZ
  1420. [qr/^(MS-)?DOS executable\b.*\bDLL\b/ => ['exe','dll'] ],
  1421. [qr/^MS Windows\b.*\bexecutable\b/ => ['exe','exe-ms'] ],
  1422. [qr/\bexecutable\b.*\bfor MS Windows\b/ => ['exe','exe-ms'] ],
  1423. [qr/^COM executable for DOS\b/ => 'asc'], # misclassified?
  1424. [qr/^DOS executable \(COM\)/ => 'asc'], # misclassified?
  1425. [qr/^(MS-)?DOS executable\b(?!.*\(COM\))/ => ['exe','exe-ms'] ],
  1426. [qr/^PA-RISC.*\bexecutable\b/ => ['exe','exe-unix'] ],
  1427. [qr/^ELF .*\bexecutable\b/ => ['exe','exe-unix'] ],
  1428. [qr/^COFF format .*\bexecutable\b/ => ['exe','exe-unix'] ],
  1429. [qr/^executable \(RISC System\b/ => ['exe','exe-unix'] ],
  1430. [qr/^VMS\b.*\bexecutable\b/ => ['exe','exe-vms'] ],
  1431. [qr/\bexecutable\b/i => 'exe'],
  1432. [qr/\bshared object, /i => 'so'],
  1433. [qr/\brelocatable, /i => 'o'],
  1434. [qr/\btext\b/i => 'asc'],
  1435. [qr/^/ => 'dat'], # catchall
  1436. ];
  1437. # MS Windows PE 32-bit Intel 80386 GUI executable not relocatable
  1438. # MS-DOS executable (EXE), OS/2 or MS Windows
  1439. # MS-DOS executable PE for MS Windows (DLL) (GUI) Intel 80386 32-bit
  1440. # MS-DOS executable PE for MS Windows (DLL) (GUI) Alpha 32-bit
  1441. # MS-DOS executable, NE for MS Windows 3.x (driver)
  1442. # MS-DOS executable (built-in) (any file starting with LZ!)
  1443. # PE executable for MS Windows (DLL) (GUI) Intel 80386 32-bit
  1444. # PE executable for MS Windows (GUI) Intel 80386 32-bit
  1445. # NE executable for MS Windows 3.x
  1446. # PA-RISC1.1 executable dynamically linked
  1447. # PA-RISC1.1 shared executable dynamically linked
  1448. # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD),
  1449. # for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped
  1450. # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV),
  1451. # for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped
  1452. # ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD),
  1453. # for FreeBSD 5.0, dynamically linked (uses shared libs), stripped
  1454. # ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped
  1455. # ELF 32-bit LSB executable, Intel 80386, version 1, dynamically`
  1456. # ELF 32-bit MSB executable, SPARC, version 1, dynamically linke`
  1457. # COFF format alpha executable paged stripped - version 3.11-10
  1458. # COFF format alpha executable paged dynamically linked stripped`
  1459. # COFF format alpha demand paged executable or object module
  1460. # stripped - version 3.11-10
  1461. # COFF format alpha paged dynamically linked not stripped shared`
  1462. # executable (RISC System/6000 V3.1) or obj module
  1463. # VMS VAX executable
  1464. # A list of pairs or n-tuples: [short-type, code_ref, optional-args...].
  1465. # Maps short types to a decoding routine, the first match wins.
  1466. # Arguments beyond the first two can be program path string (or a listref of
  1467. # paths to be searched) or a reference to a variable containing such a path,
  1468. # which allows for lazy evaluation, making possible to assign values to
  1469. # legacy configuration variables even after the assignment to @decoders.
  1470. @decoders = (
  1471. ['mail', \&Amavis::Unpackers::do_mime_decode],
  1472. # [[qw(asc uue hqx ync)], \&Amavis::Unpackers::do_ascii], # not safe
  1473. ['F', \&Amavis::Unpackers::do_uncompress, \$unfreeze],
  1474. # ['unfreeze', 'freeze -d', 'melt', 'fcat'] ],
  1475. ['Z', \&Amavis::Unpackers::do_uncompress, \$uncompress],
  1476. # ['uncompress', 'gzip -d', 'zcat'] ],
  1477. ['gz', \&Amavis::Unpackers::do_uncompress, \$gunzip],
  1478. ['gz', \&Amavis::Unpackers::do_gunzip],
  1479. ['bz2', \&Amavis::Unpackers::do_uncompress, \$bunzip2],
  1480. ['xz', \&Amavis::Unpackers::do_uncompress,
  1481. ['xzdec', 'xz -dc', 'unxz -c', 'xzcat'] ],
  1482. ['lzma', \&Amavis::Unpackers::do_uncompress,
  1483. ['lzmadec', 'xz -dc --format=lzma',
  1484. 'lzma -dc', 'unlzma -c', 'lzcat', 'lzmadec'] ],
  1485. ['lrz', \&Amavis::Unpackers::do_uncompress,
  1486. ['lrzip -q -k -d -o -', 'lrzcat -q -k'] ],
  1487. ['lzo', \&Amavis::Unpackers::do_uncompress, \$unlzop],
  1488. ['rpm', \&Amavis::Unpackers::do_uncompress, \$rpm2cpio],
  1489. # ['rpm2cpio.pl', 'rpm2cpio'] ],
  1490. [['cpio','tar'], \&Amavis::Unpackers::do_pax_cpio, \$pax],
  1491. # ['/usr/local/heirloom/usr/5bin/pax', 'pax', 'gcpio', 'cpio']
  1492. # ['tar', \&Amavis::Unpackers::do_tar], # no longer supported
  1493. ['deb', \&Amavis::Unpackers::do_ar, \$ar],
  1494. # ['a', \&Amavis::Unpackers::do_ar, \$ar], #unpacking .a seems an overkill
  1495. ['rar', \&Amavis::Unpackers::do_unrar, \$unrar], # ['unrar', 'rar']
  1496. ['arj', \&Amavis::Unpackers::do_unarj, \$unarj], # ['unarj', 'arj']
  1497. ['arc', \&Amavis::Unpackers::do_arc, \$arc], # ['nomarch', 'arc']
  1498. ['zoo', \&Amavis::Unpackers::do_zoo, \$zoo], # ['zoo', 'unzoo']
  1499. ['doc', \&Amavis::Unpackers::do_ole, \$ripole],
  1500. ['cab', \&Amavis::Unpackers::do_cabextract, \$cabextract],
  1501. ['tnef', \&Amavis::Unpackers::do_tnef_ext, \$tnef],
  1502. ['tnef', \&Amavis::Unpackers::do_tnef],
  1503. # ['lha', \&Amavis::Unpackers::do_lha, \$lha], # not safe, use 7z instead
  1504. # ['sit', \&Amavis::Unpackers::do_unstuff, \$unstuff], # not safe
  1505. [['zip','kmz'], \&Amavis::Unpackers::do_7zip, ['7za', '7z'] ],
  1506. [['zip','kmz'], \&Amavis::Unpackers::do_unzip],
  1507. ['7z', \&Amavis::Unpackers::do_7zip, ['7zr', '7za', '7z'] ],
  1508. [[qw(7z zip gz bz2 Z tar)],
  1509. \&Amavis::Unpackers::do_7zip, ['7za', '7z'] ],
  1510. [[qw(xz lzma jar cpio arj rar swf lha iso cab deb rpm)],
  1511. \&Amavis::Unpackers::do_7zip, '7z' ],
  1512. ['exe', \&Amavis::Unpackers::do_executable, \$unrar, \$lha, \$unarj],
  1513. );
  1514. # build_default_maps
  1515. @local_domains_maps = (
  1516. \%local_domains, \@local_domains_acl, \$local_domains_re);
  1517. @mynetworks_maps = (\@mynetworks);
  1518. @client_ipaddr_policy = map(($_,'MYNETS'), @mynetworks_maps);
  1519. @bypass_virus_checks_maps = (
  1520. \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
  1521. @bypass_spam_checks_maps = (
  1522. \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
  1523. @bypass_banned_checks_maps = (
  1524. \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re);
  1525. @bypass_header_checks_maps = (
  1526. \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re);
  1527. @virus_lovers_maps = (
  1528. \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re);
  1529. @spam_lovers_maps = (
  1530. \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re);
  1531. @banned_files_lovers_maps = (
  1532. \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re);
  1533. @bad_header_lovers_maps = (
  1534. \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re);
  1535. # @unchecked_lovers_maps = (); # empty, new setting, no need for backw compat.
  1536. @warnvirusrecip_maps = (\$warnvirusrecip);
  1537. @warnbannedrecip_maps = (\$warnbannedrecip);
  1538. @warnbadhrecip_maps = (\$warnbadhrecip);
  1539. @newvirus_admin_maps = (\$newvirus_admin);
  1540. @virus_admin_maps = (\%virus_admin, \$virus_admin);
  1541. @banned_admin_maps = (\$banned_admin, \%virus_admin, \$virus_admin);
  1542. @bad_header_admin_maps= (\$bad_header_admin);
  1543. @spam_admin_maps = (\%spam_admin, \$spam_admin);
  1544. @virus_quarantine_to_maps = (\$virus_quarantine_to);
  1545. @banned_quarantine_to_maps = (\$banned_quarantine_to);
  1546. @unchecked_quarantine_to_maps = (\$unchecked_quarantine_to);
  1547. @spam_quarantine_to_maps = (\$spam_quarantine_to);
  1548. @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to);
  1549. @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to);
  1550. @clean_quarantine_to_maps = (\$clean_quarantine_to);
  1551. @archive_quarantine_to_maps = (\$archive_quarantine_to);
  1552. @keep_decoded_original_maps = (\$keep_decoded_original_re);
  1553. @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re);
  1554. # @banned_filename_maps = ( {'.' => [$banned_filename_re]} );
  1555. # @banned_filename_maps = ( {'.' => 'DEFAULT'} );#names mapped by %banned_rules
  1556. @banned_filename_maps = ( 'DEFAULT' ); # same as previous, but shorter
  1557. @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1);
  1558. @spam_tag_level_maps = (\$sa_tag_level_deflt); # CC_CLEAN,1
  1559. @spam_tag2_level_maps = (\$sa_tag2_level_deflt); # CC_SPAMMY
  1560. @spam_tag3_level_maps = (\$sa_tag3_level_deflt); # CC_SPAMMY,1
  1561. @spam_kill_level_maps = (\$sa_kill_level_deflt); # CC_SPAM
  1562. @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level);
  1563. @spam_dsn_cutoff_level_bysender_maps = (\$sa_dsn_cutoff_level);
  1564. @spam_crediblefrom_dsn_cutoff_level_maps =
  1565. (\$sa_crediblefrom_dsn_cutoff_level);
  1566. @spam_crediblefrom_dsn_cutoff_level_bysender_maps =
  1567. (\$sa_crediblefrom_dsn_cutoff_level);
  1568. @spam_quarantine_cutoff_level_maps = (\$sa_quarantine_cutoff_level);
  1569. @spam_subject_tag_maps = (\$sa_spam_subject_tag1); # note: inconsistent name
  1570. @spam_subject_tag2_maps = (\$sa_spam_subject_tag); # note: inconsistent name
  1571. # @spam_subject_tag3_maps = (); # new variable, no backwards compatib. needed
  1572. @whitelist_sender_maps = (
  1573. \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re);
  1574. @blacklist_sender_maps = (
  1575. \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re);
  1576. @addr_extension_virus_maps = (\$addr_extension_virus);
  1577. @addr_extension_spam_maps = (\$addr_extension_spam);
  1578. @addr_extension_banned_maps = (\$addr_extension_banned);
  1579. @addr_extension_bad_header_maps = (\$addr_extension_bad_header);
  1580. @debug_sender_maps = (\@debug_sender_acl);
  1581. # @debug_recipient_maps = ();
  1582. @remove_existing_spam_headers_maps = (\$remove_existing_spam_headers);
  1583. # new variables, no backwards compatibility needed, empty by default
  1584. # @score_sender_maps, @author_to_policy_bank_maps, @signer_reputation_maps,
  1585. # @message_size_limit_maps
  1586. # build backwards-compatible settings hashes
  1587. %final_destiny_by_ccat = (
  1588. CC_VIRUS, sub { c('final_virus_destiny') },
  1589. CC_BANNED, sub { c('final_banned_destiny') },
  1590. CC_UNCHECKED, sub { c('final_unchecked_destiny') },
  1591. CC_SPAM, sub { c('final_spam_destiny') },
  1592. CC_BADH, sub { c('final_bad_header_destiny') },
  1593. CC_MTA.',1', D_TEMPFAIL,
  1594. CC_MTA.',2', D_REJECT,
  1595. CC_OVERSIZED, D_BOUNCE,
  1596. CC_CATCHALL, D_PASS,
  1597. );
  1598. %forward_method_maps_by_ccat = (
  1599. CC_CATCHALL, sub { ca('forward_method_maps') },
  1600. );
  1601. %smtp_reason_by_ccat = (
  1602. # currently only used for blocked messages only, status 5xx
  1603. # a multiline message will produce a valid multiline SMTP response
  1604. CC_VIRUS, 'id=%n - INFECTED: %V',
  1605. CC_BANNED, 'id=%n - BANNED: %F',
  1606. CC_UNCHECKED, 'id=%n - UNCHECKED',
  1607. CC_SPAM, 'id=%n - spam',
  1608. CC_SPAMMY.',1', 'id=%n - spammy (tag3)',
  1609. CC_SPAMMY, 'id=%n - spammy',
  1610. CC_BADH.',1', 'id=%n - BAD HEADER: MIME error',
  1611. CC_BADH.',2', 'id=%n - BAD HEADER: nonencoded 8-bit character',
  1612. CC_BADH.',3', 'id=%n - BAD HEADER: contains invalid control character',
  1613. CC_BADH.',4', 'id=%n - BAD HEADER: line made up entirely of whitespace',
  1614. CC_BADH.',5', 'id=%n - BAD HEADER: line longer than RFC 5322 limit',
  1615. CC_BADH.',6', 'id=%n - BAD HEADER: syntax error',
  1616. CC_BADH.',7', 'id=%n - BAD HEADER: missing required header field',
  1617. CC_BADH.',8', 'id=%n - BAD HEADER: duplicate header field',
  1618. CC_BADH, 'id=%n - BAD HEADER',
  1619. CC_OVERSIZED, 'id=%n - Message size exceeds recipient\'s size limit',
  1620. CC_MTA.',1', 'id=%n - Temporary MTA failure on relaying',
  1621. CC_MTA.',2', 'id=%n - Rejected by next-hop MTA on relaying',
  1622. CC_MTA, 'id=%n - Unable to relay message back to MTA',
  1623. CC_CLEAN, 'id=%n - CLEAN',
  1624. CC_CATCHALL, 'id=%n - OTHER', # should not happen
  1625. );
  1626. %lovers_maps_by_ccat = (
  1627. CC_VIRUS, sub { ca('virus_lovers_maps') },
  1628. CC_BANNED, sub { ca('banned_files_lovers_maps') },
  1629. CC_UNCHECKED, sub { ca('unchecked_lovers_maps') },
  1630. CC_SPAM, sub { ca('spam_lovers_maps') },
  1631. CC_SPAMMY, sub { ca('spam_lovers_maps') },
  1632. CC_BADH, sub { ca('bad_header_lovers_maps') },
  1633. );
  1634. %defang_maps_by_ccat = (
  1635. CC_VIRUS, sub { c('defang_virus') },
  1636. CC_BANNED, sub { c('defang_banned') },
  1637. CC_UNCHECKED, sub { c('defang_undecipherable') },
  1638. CC_SPAM, sub { c('defang_spam') },
  1639. CC_SPAMMY, sub { c('defang_spam') },
  1640. # CC_BADH.',3', 1, # NUL or CR character in header section
  1641. # CC_BADH.',5', 1, # header line longer than 998 characters
  1642. # CC_BADH.',6', 1, # header field syntax error
  1643. CC_BADH, sub { c('defang_bad_header') },
  1644. );
  1645. %subject_tag_maps_by_ccat = (
  1646. CC_VIRUS, [ '***INFECTED*** ' ],
  1647. CC_BANNED, undef,
  1648. CC_UNCHECKED, sub { [ c('undecipherable_subject_tag') ] }, # not by-recip
  1649. CC_SPAM, undef,
  1650. CC_SPAMMY.',1', sub { ca('spam_subject_tag3_maps') },
  1651. CC_SPAMMY, sub { ca('spam_subject_tag2_maps') },
  1652. CC_CLEAN.',1', sub { ca('spam_subject_tag_maps') },
  1653. );
  1654. %quarantine_method_by_ccat = (
  1655. CC_VIRUS, sub { c('virus_quarantine_method') },
  1656. CC_BANNED, sub { c('banned_files_quarantine_method') },
  1657. CC_UNCHECKED, sub { c('unchecked_quarantine_method') },
  1658. CC_SPAM, sub { c('spam_quarantine_method') },
  1659. CC_BADH, sub { c('bad_header_quarantine_method') },
  1660. CC_CLEAN, sub { c('clean_quarantine_method') },
  1661. );
  1662. %quarantine_to_maps_by_ccat = (
  1663. CC_VIRUS, sub { ca('virus_quarantine_to_maps') },
  1664. CC_BANNED, sub { ca('banned_quarantine_to_maps') },
  1665. CC_UNCHECKED, sub { ca('unchecked_quarantine_to_maps') },
  1666. CC_SPAM, sub { ca('spam_quarantine_to_maps') },
  1667. CC_BADH, sub { ca('bad_header_quarantine_to_maps') },
  1668. CC_CLEAN, sub { ca('clean_quarantine_to_maps') },
  1669. );
  1670. %admin_maps_by_ccat = (
  1671. CC_VIRUS, sub { ca('virus_admin_maps') },
  1672. CC_BANNED, sub { ca('banned_admin_maps') },
  1673. CC_UNCHECKED, sub { ca('virus_admin_maps') },
  1674. CC_SPAM, sub { ca('spam_admin_maps') },
  1675. CC_BADH, sub { ca('bad_header_admin_maps') },
  1676. );
  1677. %always_bcc_by_ccat = (
  1678. CC_CATCHALL, sub { c('always_bcc') },
  1679. );
  1680. %dsn_bcc_by_ccat = (
  1681. CC_CATCHALL, sub { c('dsn_bcc') },
  1682. );
  1683. %mailfrom_notify_admin_by_ccat = (
  1684. CC_SPAM, sub { c('mailfrom_notify_spamadmin') },
  1685. CC_CATCHALL, sub { c('mailfrom_notify_admin') },
  1686. );
  1687. %hdrfrom_notify_admin_by_ccat = (
  1688. CC_SPAM, sub { c('hdrfrom_notify_spamadmin') },
  1689. CC_CATCHALL, sub { c('hdrfrom_notify_admin') },
  1690. );
  1691. %mailfrom_notify_recip_by_ccat = (
  1692. CC_CATCHALL, sub { c('mailfrom_notify_recip') },
  1693. );
  1694. %hdrfrom_notify_recip_by_ccat = (
  1695. CC_CATCHALL, sub { c('hdrfrom_notify_recip') },
  1696. );
  1697. %hdrfrom_notify_sender_by_ccat = (
  1698. CC_CATCHALL, sub { c('hdrfrom_notify_sender') },
  1699. );
  1700. %hdrfrom_notify_release_by_ccat = (
  1701. CC_CATCHALL, sub { c('hdrfrom_notify_release') },
  1702. );
  1703. %hdrfrom_notify_report_by_ccat = (
  1704. CC_CATCHALL, sub { c('hdrfrom_notify_report') },
  1705. );
  1706. %notify_admin_templ_by_ccat = (
  1707. CC_SPAM, sub { cr('notify_spam_admin_templ') },
  1708. CC_CATCHALL, sub { cr('notify_virus_admin_templ') },
  1709. );
  1710. %notify_recips_templ_by_ccat = (
  1711. CC_SPAM, sub { cr('notify_spam_recips_templ') }, #usually empty
  1712. CC_CATCHALL, sub { cr('notify_virus_recips_templ') },
  1713. );
  1714. %notify_sender_templ_by_ccat = ( # bounce templates
  1715. CC_VIRUS, sub { cr('notify_virus_sender_templ') },
  1716. CC_BANNED, sub { cr('notify_virus_sender_templ') }, #historical reason
  1717. CC_SPAM, sub { cr('notify_spam_sender_templ') },
  1718. CC_CATCHALL, sub { cr('notify_sender_templ') },
  1719. );
  1720. %notify_release_templ_by_ccat = (
  1721. CC_CATCHALL, sub { cr('notify_release_templ') },
  1722. );
  1723. %notify_report_templ_by_ccat = (
  1724. CC_CATCHALL, sub { cr('notify_report_templ') },
  1725. );
  1726. %notify_autoresp_templ_by_ccat = (
  1727. CC_CATCHALL, sub { cr('notify_autoresp_templ') },
  1728. );
  1729. %warnsender_by_ccat = ( # deprecated use, except perhaps for CC_BADH
  1730. CC_VIRUS, undef,
  1731. CC_BANNED, sub { c('warnbannedsender') },
  1732. CC_SPAM, undef,
  1733. CC_BADH, sub { c('warnbadhsender') },
  1734. );
  1735. %warnrecip_maps_by_ccat = (
  1736. CC_VIRUS, sub { ca('warnvirusrecip_maps') },
  1737. CC_BANNED, sub { ca('warnbannedrecip_maps') },
  1738. CC_SPAM, undef,
  1739. CC_BADH, sub { ca('warnbadhrecip_maps') },
  1740. );
  1741. %addr_extension_maps_by_ccat = (
  1742. CC_VIRUS, sub { ca('addr_extension_virus_maps') },
  1743. CC_BANNED, sub { ca('addr_extension_banned_maps') },
  1744. CC_SPAM, sub { ca('addr_extension_spam_maps') },
  1745. CC_SPAMMY, sub { ca('addr_extension_spam_maps') },
  1746. CC_BADH, sub { ca('addr_extension_bad_header_maps') },
  1747. # CC_OVERSIZED, 'oversized';
  1748. );
  1749. %addr_rewrite_maps_by_ccat = ( );
  1750. 1;
  1751. } # end BEGIN - init_tertiary
  1752. # prototypes
  1753. sub Amavis::Unpackers::do_mime_decode($$);
  1754. sub Amavis::Unpackers::do_ascii($$);
  1755. sub Amavis::Unpackers::do_uncompress($$$);
  1756. sub Amavis::Unpackers::do_gunzip($$);
  1757. sub Amavis::Unpackers::do_pax_cpio($$$);
  1758. #sub Amavis::Unpackers::do_tar($$); # no longer supported
  1759. sub Amavis::Unpackers::do_ar($$$);
  1760. sub Amavis::Unpackers::do_unzip($$;$$);
  1761. sub Amavis::Unpackers::do_7zip($$$;$);
  1762. sub Amavis::Unpackers::do_unrar($$$;$);
  1763. sub Amavis::Unpackers::do_unarj($$$;$);
  1764. sub Amavis::Unpackers::do_arc($$$);
  1765. sub Amavis::Unpackers::do_zoo($$$);
  1766. sub Amavis::Unpackers::do_lha($$$;$);
  1767. sub Amavis::Unpackers::do_ole($$$);
  1768. sub Amavis::Unpackers::do_cabextract($$$);
  1769. sub Amavis::Unpackers::do_tnef($$);
  1770. sub Amavis::Unpackers::do_tnef_ext($$$);
  1771. sub Amavis::Unpackers::do_unstuff($$$);
  1772. sub Amavis::Unpackers::do_executable($$@);
  1773. no warnings 'once';
  1774. # Define alias names or shortcuts in this module to make it simpler
  1775. # to call these routines from amavisd.conf
  1776. *read_l10n_templates = \&Amavis::Util::read_l10n_templates;
  1777. *read_text = \&Amavis::Util::read_text;
  1778. *read_hash = \&Amavis::Util::read_hash;
  1779. *read_array = \&Amavis::Util::read_array;
  1780. *read_cidr = \&Amavis::Util::read_cidr;
  1781. *dump_hash = \&Amavis::Util::dump_hash;
  1782. *dump_array = \&Amavis::Util::dump_array;
  1783. *ask_daemon = \&Amavis::AV::ask_daemon;
  1784. *ask_clamav = \&Amavis::AV::ask_clamav; # deprecated, use ask_daemon
  1785. *do_mime_decode = \&Amavis::Unpackers::do_mime_decode;
  1786. *do_ascii = \&Amavis::Unpackers::do_ascii;
  1787. *do_uncompress = \&Amavis::Unpackers::do_uncompress;
  1788. *do_gunzip = \&Amavis::Unpackers::do_gunzip;
  1789. *do_pax_cpio = \&Amavis::Unpackers::do_pax_cpio;
  1790. *do_tar = \&Amavis::Unpackers::do_tar; # no longer supported
  1791. *do_ar = \&Amavis::Unpackers::do_ar;
  1792. *do_unzip = \&Amavis::Unpackers::do_unzip;
  1793. *do_unrar = \&Amavis::Unpackers::do_unrar;
  1794. *do_7zip = \&Amavis::Unpackers::do_7zip;
  1795. *do_unarj = \&Amavis::Unpackers::do_unarj;
  1796. *do_arc = \&Amavis::Unpackers::do_arc;
  1797. *do_zoo = \&Amavis::Unpackers::do_zoo;
  1798. *do_lha = \&Amavis::Unpackers::do_lha;
  1799. *do_ole = \&Amavis::Unpackers::do_ole;
  1800. *do_cabextract = \&Amavis::Unpackers::do_cabextract;
  1801. *do_tnef_ext = \&Amavis::Unpackers::do_tnef_ext;
  1802. *do_tnef = \&Amavis::Unpackers::do_tnef;
  1803. *do_unstuff = \&Amavis::Unpackers::do_unstuff;
  1804. *do_executable = \&Amavis::Unpackers::do_executable;
  1805. *iso8601_week = \&Amavis::rfc2821_2822_Tools::iso8601_week;
  1806. *iso8601_yearweek = \&Amavis::rfc2821_2822_Tools::iso8601_yearweek;
  1807. *iso8601_year_and_week = \&Amavis::rfc2821_2822_Tools::iso8601_year_and_week;
  1808. *iso8601_weekday = \&Amavis::rfc2821_2822_Tools::iso8601_weekday;
  1809. *iso8601_timestamp = \&Amavis::rfc2821_2822_Tools::iso8601_timestamp;
  1810. *iso8601_utc_timestamp = \&Amavis::rfc2821_2822_Tools::iso8601_utc_timestamp;
  1811. sub new_RE { Amavis::Lookup::RE->new(@_) }
  1812. # shorthand: construct a query object for an SQL field
  1813. sub q_sql_s { Amavis::Lookup::SQLfield->new(undef, $_[0], 'S-') } # string
  1814. sub q_sql_n { Amavis::Lookup::SQLfield->new(undef, $_[0], 'N-') } # numeric
  1815. sub q_sql_b { Amavis::Lookup::SQLfield->new(undef, $_[0], 'B-') } # boolean
  1816. # shorthand: construct a query object for an LDAP attribute
  1817. sub q_ldap_s { Amavis::Lookup::LDAPattr->new(undef, $_[0], 'S-') } # string
  1818. sub q_ldap_n { Amavis::Lookup::LDAPattr->new(undef, $_[0], 'N-') } # numeric
  1819. sub q_ldap_b { Amavis::Lookup::LDAPattr->new(undef, $_[0], 'B-') } # boolean
  1820. sub Opaque { Amavis::Lookup::Opaque->new(@_) }
  1821. sub OpaqueRef { Amavis::Lookup::OpaqueRef->new(@_) }
  1822. #
  1823. # Opaque provides a wrapper to arbitrary data structures, allowing them to be
  1824. # treated as 'constant' pseudo-lookups, i.e. preventing arrays and hashes from
  1825. # being interpreted as lookup lists/tables. In case of $forward_method this
  1826. # allows for a listref of failover methods. Without the protection of Opaque
  1827. # the listref would be interpreted by a lookup() as an acl lookup type instead
  1828. # of a match-always data structure. The Opaque subroutine is not yet available
  1829. # during a BEGIN phase, so this assignment must come after compiling the rest
  1830. # of the code.
  1831. #
  1832. # This is the only case where both an array @*_maps as well as its default
  1833. # element are members of a policy bank. Use lazy evaluation through a sub
  1834. # to make this work as expected.
  1835. #
  1836. # @forward_method_maps = ( OpaqueRef(\$forward_method) );
  1837. @forward_method_maps = ( sub { Opaque(c('forward_method')) } );
  1838. # compatibility with old names
  1839. use vars qw(%defang_by_ccat $sql_partition_tag $DO_SYSLOG $LOGFILE);
  1840. *defang_by_ccat = \%defang_maps_by_ccat;
  1841. *sql_partition_tag = \$partition_tag;
  1842. *DO_SYSLOG = \$do_syslog;
  1843. *LOGFILE = \$logfile;
  1844. @virus_name_to_spam_score_maps =
  1845. (new_RE( # the order matters, first match wins
  1846. [ qr'^Structured\.(SSN|CreditCardNumber)\b' => 0.1 ],
  1847. [ qr'^(Heuristics\.)?Phishing\.' => 0.1 ],
  1848. [ qr'^(Email|HTML)\.Phishing\.(?!.*Sanesecurity)' => 0.1 ],
  1849. [ qr'^Sanesecurity\.(Malware|Rogue|Trojan)\.' => undef ],# keep as infected
  1850. [ qr'^Sanesecurity\.' => 0.1 ],
  1851. [ qr'^Sanesecurity_PhishBar_' => 0 ],
  1852. [ qr'^Sanesecurity.TestSig_' => 0 ],
  1853. [ qr'^Email\.Spam\.Bounce(\.[^., ]*)*\.Sanesecurity\.' => 0 ],
  1854. [ qr'^Email\.Spammail\b' => 0.1 ],
  1855. [ qr'^MSRBL-(Images|SPAM)\b' => 0.1 ],
  1856. [ qr'^VX\.Honeypot-SecuriteInfo\.com\.Joke' => 0.1 ],
  1857. [ qr'^VX\.not-virus_(Hoax|Joke)\..*-SecuriteInfo\.com(\.|\z)' => 0.1 ],
  1858. [ qr'^Email\.Spam.*-SecuriteInfo\.com(\.|\z)' => 0.1 ],
  1859. [ qr'^Safebrowsing\.' => 0.1 ],
  1860. [ qr'^winnow\.(phish|spam)\.' => 0.1 ],
  1861. [ qr'^INetMsg\.SpamDomain' => 0.1 ],
  1862. [ qr'^Doppelstern\.(Scam4|Phishing|Junk)' => 0.1 ],
  1863. [ qr'^ScamNailer\.' => 0.1 ],
  1864. [ qr'^HTML/Bankish' => 0.1 ], # F-Prot
  1865. [ qr'-SecuriteInfo\.com(\.|\z)' => undef ], # keep as infected
  1866. [ qr'^MBL_NA\.UNOFFICIAL' => 0.1 ], # false positives
  1867. [ qr'^MBL_' => undef ], # keep as infected
  1868. ));
  1869. # Sanesecurity http://www.sanesecurity.co.uk/
  1870. # MSRBL- http://www.msrbl.com/site/contact
  1871. # MBL http://www.malware.com.br/index.shtml
  1872. # -SecuriteInfo.com http://clamav.securiteinfo.com/malwares.html
  1873. # prepend a lookup table label object for logging purposes
  1874. #
  1875. sub label_default_maps() {
  1876. for my $varname (qw(
  1877. @disclaimer_options_bysender_maps @dkim_signature_options_bysender_maps
  1878. @local_domains_maps @mynetworks_maps
  1879. @forward_method_maps @newvirus_admin_maps @banned_filename_maps
  1880. @spam_quarantine_bysender_to_maps
  1881. @spam_tag_level_maps @spam_tag2_level_maps @spam_tag3_level_maps
  1882. @spam_kill_level_maps
  1883. @spam_subject_tag_maps @spam_subject_tag2_maps @spam_subject_tag3_maps
  1884. @spam_dsn_cutoff_level_maps @spam_dsn_cutoff_level_bysender_maps
  1885. @spam_crediblefrom_dsn_cutoff_level_maps
  1886. @spam_crediblefrom_dsn_cutoff_level_bysender_maps
  1887. @spam_quarantine_cutoff_level_maps @spam_notifyadmin_cutoff_level_maps
  1888. @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
  1889. @author_to_policy_bank_maps @signer_reputation_maps
  1890. @message_size_limit_maps @debug_sender_maps @debug_recipient_maps
  1891. @bypass_virus_checks_maps @bypass_spam_checks_maps
  1892. @bypass_banned_checks_maps @bypass_header_checks_maps
  1893. @viruses_that_fake_sender_maps
  1894. @virus_name_to_spam_score_maps @virus_name_to_policy_bank_maps
  1895. @remove_existing_spam_headers_maps
  1896. @sa_userconf_maps @sa_username_maps
  1897. @keep_decoded_original_maps @map_full_type_to_short_type_maps
  1898. @virus_lovers_maps @spam_lovers_maps @unchecked_lovers_maps
  1899. @banned_files_lovers_maps @bad_header_lovers_maps
  1900. @virus_quarantine_to_maps @banned_quarantine_to_maps
  1901. @unchecked_quarantine_to_maps @spam_quarantine_to_maps
  1902. @bad_header_quarantine_to_maps @clean_quarantine_to_maps
  1903. @archive_quarantine_to_maps
  1904. @virus_admin_maps @banned_admin_maps
  1905. @spam_admin_maps @bad_header_admin_maps @spam_modifies_subj_maps
  1906. @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
  1907. @addr_extension_virus_maps @addr_extension_spam_maps
  1908. @addr_extension_banned_maps @addr_extension_bad_header_maps
  1909. ))
  1910. {
  1911. my $g = $varname; $g =~ s{\@}{Amavis::Conf::}; # qualified variable name
  1912. my $label = $varname; $label=~s/^\@//; $label=~s/_maps$//;
  1913. { no strict 'refs';
  1914. unshift(@$g, # NOTE: a symbolic reference
  1915. Amavis::Lookup::Label->new($label)) if @$g; # no label if empty
  1916. }
  1917. }
  1918. }
  1919. # return a list of actually read&evaluated configuration files
  1920. sub get_config_files_read() { @actual_config_files }
  1921. # read and evaluate a configuration file, some sanity checking and housekeeping
  1922. #
  1923. sub read_config_file($$) {
  1924. my($config_file,$is_optional) = @_;
  1925. my(@stat_list) = stat($config_file); # symlinks-friendly
  1926. my $errn = @stat_list ? 0 : 0+$!;
  1927. if ($errn == ENOENT && $is_optional) {
  1928. # don't complain if missing
  1929. } else {
  1930. my $owner_uid = $stat_list[4];
  1931. my $msg;
  1932. if ($errn == ENOENT) { $msg = "does not exist" }
  1933. elsif ($errn) { $msg = "is inaccessible: $!" }
  1934. elsif (-d _) { $msg = "is a directory" }
  1935. elsif (!-f _) { $msg = "is not a regular file" }
  1936. elsif ($> && -o _) { $msg = "should not be owned by EUID $>"}
  1937. elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" }
  1938. elsif ($owner_uid) { $msg = "should be owned by root (uid 0) "}
  1939. if (defined $msg) { die "Config file \"$config_file\" $msg," }
  1940. $read_config_files_depth++; push(@actual_config_files, $config_file);
  1941. if ($read_config_files_depth >= 100) {
  1942. print STDERR "read_config_files: recursion depth limit exceeded\n";
  1943. exit 1; # avoid unwinding deep recursion, abort right away
  1944. }
  1945. local($1,$2,$3,$4,$5,$6,$7,$8,$9);
  1946. local $/ = $/; # protect us from a potential change in a config file
  1947. $! = 0;
  1948. if (defined(do $config_file)) {}
  1949. elsif ($@ ne '') { die "Error in config file \"$config_file\": $@" }
  1950. elsif ($! != 0) { die "Error reading config file \"$config_file\": $!" }
  1951. $read_config_files_depth-- if $read_config_files_depth > 0;
  1952. }
  1953. 1;
  1954. }
  1955. sub include_config_files(@) { read_config_file($_,0) for @_; 1 }
  1956. sub include_optional_config_files(@) { read_config_file($_,1) for @_; 1 }
  1957. # supply remaining defaults after config files have already been read/evaluated
  1958. #
  1959. sub supply_after_defaults() {
  1960. $daemon_chroot_dir = ''
  1961. if !defined $daemon_chroot_dir || $daemon_chroot_dir eq '/';
  1962. # provide some sensible defaults for essential settings (post-defaults)
  1963. $TEMPBASE = $MYHOME if !defined $TEMPBASE;
  1964. $helpers_home = $MYHOME if !defined $helpers_home;
  1965. $db_home = "$MYHOME/db" if !defined $db_home;
  1966. @zmq_sockets = ( "ipc://$MYHOME/amavisd-zmq.sock" ) if !@zmq_sockets;
  1967. $pid_file = "$MYHOME/amavisd.pid" if !defined $pid_file;
  1968. # just keep $lock_file undefined by default, a temp file (POSIX::tmpnam) will
  1969. # be provided by Net::Server for 'flock' serialization on a socket accept()
  1970. # $lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file;
  1971. local($1,$2);
  1972. $X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE;
  1973. $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG;
  1974. if ($X_HEADER_TAG =~ /^[!-9;-\176]+\z/) {
  1975. # implicitly add to %allowed_added_header_fields for compatibility,
  1976. # unless the hash entry already exists
  1977. my $allowed_hdrs = cr('allowed_added_header_fields');
  1978. $allowed_hdrs->{lc($X_HEADER_TAG)} = 1
  1979. if $allowed_hdrs && !exists($allowed_hdrs->{lc($X_HEADER_TAG)});
  1980. }
  1981. $gunzip = "$gzip -d" if !defined $gunzip && $gzip ne '';
  1982. $bunzip2 = "$bzip2 -d" if !defined $bunzip2 && $bzip2 ne '';
  1983. $unlzop = "$lzop -d" if !defined $unlzop && $lzop ne '';
  1984. # substring ${myhostname} will be expanded later, just before use
  1985. my $pname = '"Content-filter at ${myhostname}"';
  1986. $hdrfrom_notify_sender = "$pname <postmaster\@\${myhostname}>"
  1987. if !defined $hdrfrom_notify_sender;
  1988. $hdrfrom_notify_recip = $mailfrom_notify_recip ne ''
  1989. ? "$pname <$mailfrom_notify_recip>"
  1990. : $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip;
  1991. $hdrfrom_notify_admin = $mailfrom_notify_admin ne ''
  1992. ? "$pname <$mailfrom_notify_admin>"
  1993. : $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin;
  1994. $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne ''
  1995. ? "$pname <$mailfrom_notify_spamadmin>"
  1996. : $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin;
  1997. $hdrfrom_notify_release = $hdrfrom_notify_sender
  1998. if !defined $hdrfrom_notify_release;
  1999. $hdrfrom_notify_report = $hdrfrom_notify_sender
  2000. if !defined $hdrfrom_notify_report;
  2001. if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') )
  2002. { $final_banned_destiny = D_BOUNCE }
  2003. if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') )
  2004. { $final_bad_header_destiny = D_BOUNCE }
  2005. if (!%banned_rules) {
  2006. # an associative array mapping a rule name
  2007. # to a single 'banned names/types' lookup table
  2008. %banned_rules = ('DEFAULT'=>$banned_filename_re); # backwards compatible
  2009. }
  2010. 1;
  2011. }
  2012. 1;
  2013. #
  2014. package Amavis::Log;
  2015. use strict;
  2016. use re 'taint';
  2017. BEGIN {
  2018. require Exporter;
  2019. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  2020. $VERSION = '2.316';
  2021. @ISA = qw(Exporter);
  2022. @EXPORT_OK = qw(&init &amavis_log_id &collect_log_stats
  2023. &log_to_stderr &log_fd &open_log &close_log &write_log);
  2024. import Amavis::Conf qw(:platform $DEBUG $TEMPBASE c cr ca
  2025. $myversion $logline_maxlen $daemon_user);
  2026. # import Amavis::Util qw(untaint);
  2027. }
  2028. use subs @EXPORT_OK;
  2029. use POSIX qw(locale_h strftime);
  2030. use Fcntl qw(:flock);
  2031. use Unix::Syslog qw(:macros :subs);
  2032. use Time::HiRes ();
  2033. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  2034. # since IO::File 1.10 (comes with perl 5.8.1):
  2035. # If "IO::File::open" is given a mode that includes the ":" character,
  2036. # it passes all the three arguments to a three-argument "open" operator.
  2037. use vars qw($loghandle); # log file handle when logging to a file
  2038. use vars qw($log_to_stderr $log_to_syslog $logfile_name $within_write_log);
  2039. use vars qw($current_amavis_log_id); # tracks am_id() / $msginfo->log_id
  2040. use vars qw($current_actual_syslog_ident $current_actual_syslog_facility);
  2041. use vars qw($log_lines $log_retries %log_entries_by_level %log_status_counts);
  2042. use vars qw($log_prio_debug $log_prio_info $log_prio_notice
  2043. $log_prio_warning $log_prio_err $log_prio_crit);
  2044. BEGIN { # saves a few ms later by avoiding a subroutine call
  2045. $log_prio_debug = LOG_DEBUG;
  2046. $log_prio_info = LOG_INFO;
  2047. $log_prio_notice = LOG_NOTICE;
  2048. $log_prio_warning = LOG_WARNING;
  2049. $log_prio_err = LOG_ERR;
  2050. $log_prio_crit = LOG_CRIT;
  2051. }
  2052. sub init($$) {
  2053. ($log_to_syslog, $logfile_name) = @_;
  2054. $log_lines = 0; %log_entries_by_level = ();
  2055. $log_retries = 0; %log_status_counts = ();
  2056. open_log();
  2057. if (!$log_to_syslog && $logfile_name eq '')
  2058. { print STDERR "Logging to STDERR (no \$logfile and no \$do_syslog)\n" }
  2059. }
  2060. sub collect_log_stats() {
  2061. my(@result) = ($log_lines, {%log_entries_by_level},
  2062. $log_retries, {%log_status_counts});
  2063. $log_lines = 0; %log_entries_by_level = ();
  2064. $log_retries = 0; %log_status_counts = ();
  2065. @result;
  2066. }
  2067. # task id as shown in the log, also known as am_id, tracks $msginfo->log_id
  2068. #
  2069. sub amavis_log_id(;$) {
  2070. $current_amavis_log_id = shift if @_;
  2071. $current_amavis_log_id;
  2072. }
  2073. # turn debug logging to STDERR on or off
  2074. #
  2075. sub log_to_stderr(;$) {
  2076. $log_to_stderr = shift if @_;
  2077. $log_to_stderr;
  2078. }
  2079. # try to obtain file descriptor used by write_log, undef if unknown
  2080. #
  2081. sub log_fd() {
  2082. $log_to_stderr ? fileno(STDERR)
  2083. : $log_to_syslog ? undef # how to obtain fd on syslog?
  2084. : defined $loghandle ? $loghandle->fileno : fileno(STDERR);
  2085. }
  2086. sub open_log() {
  2087. # don't bother to skip opening the log even if $log_to_stderr (debug) is true
  2088. if ($log_to_syslog) {
  2089. my $id = c('syslog_ident'); my $fac = c('syslog_facility');
  2090. $fac =~ /^[A-Za-z0-9_]+\z/
  2091. or die "Suspicious syslog facility name: $fac";
  2092. my $syslog_facility_num = eval("LOG_\U$fac");
  2093. $syslog_facility_num =~ /^\d+\z/
  2094. or die "Unknown syslog facility name: $fac";
  2095. # man syslog(3) on Linux: The argument 'ident' in the call of openlog()
  2096. # is probably stored as-is. Thus, if the string it points to is changed,
  2097. # syslog() may start prepending the changed string, and if the string
  2098. # it points to ceases to exist, the results are undefined. Most portable
  2099. # is to use a string constant. (we use a static variable here)
  2100. $current_actual_syslog_ident = $id; $current_actual_syslog_facility = $fac;
  2101. openlog($id, LOG_PID | LOG_NDELAY, $syslog_facility_num);
  2102. } elsif ($logfile_name ne '') {
  2103. $loghandle = IO::File->new;
  2104. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  2105. $loghandle->open($logfile_name,
  2106. Amavis::Util::untaint(O_CREAT|O_APPEND|O_WRONLY), 0640)
  2107. or die "Failed to open log file $logfile_name: $!";
  2108. binmode($loghandle,':bytes') or die "Can't cancel :utf8 mode: $!";
  2109. $loghandle->autoflush(1);
  2110. if ($> == 0) {
  2111. local($1);
  2112. my $uid = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2];
  2113. if ($uid) {
  2114. chown($uid,-1,$logfile_name)
  2115. or die "Can't chown logfile $logfile_name to $uid: $!";
  2116. }
  2117. }
  2118. } else { # logging to STDERR
  2119. STDERR->autoflush(1); # just in case
  2120. }
  2121. }
  2122. sub close_log() {
  2123. if ($log_to_syslog) {
  2124. closelog();
  2125. $current_actual_syslog_ident = $current_actual_syslog_facility = undef;
  2126. } elsif (defined($loghandle) && $logfile_name ne '') {
  2127. $loghandle->close or die "Error closing log file $logfile_name: $!";
  2128. undef $loghandle;
  2129. }
  2130. }
  2131. # Log either to syslog or to a file
  2132. #
  2133. sub write_log($$) {
  2134. my($level,$errmsg) = @_;
  2135. return if $within_write_log;
  2136. $within_write_log = 1;
  2137. my $am_id = !defined $current_amavis_log_id ? ''
  2138. : "($current_amavis_log_id) ";
  2139. # my $old_locale = POSIX::setlocale(LC_TIME,'C'); # English dates required!
  2140. my $alert_mark = $level >= 0 ? '' : $level >= -1 ? '(!)' : '(!!)';
  2141. # $alert_mark .= '*' if $> == 0;
  2142. $log_entries_by_level{"$level"}++;
  2143. if ($log_to_syslog && !$log_to_stderr) {
  2144. my $prio;
  2145. if ($level >= 3) { $prio = $log_prio_debug } # most frequent first
  2146. elsif ($level >= 2) { $prio = $log_prio_info }
  2147. elsif ($level >= 1) { $prio = $log_prio_info }
  2148. elsif ($level >= 0) { $prio = $log_prio_notice }
  2149. elsif ($level >= -1) { $prio = $log_prio_warning }
  2150. elsif ($level >= -2) { $prio = $log_prio_err }
  2151. else { $prio = $log_prio_crit }
  2152. if ($Amavis::Util::current_config_syslog_ident
  2153. ne $current_actual_syslog_ident ||
  2154. $Amavis::Util::current_config_syslog_facility
  2155. ne $current_actual_syslog_facility) {
  2156. close_log() if defined $current_actual_syslog_ident ||
  2157. defined $current_actual_syslog_facility;
  2158. open_log();
  2159. }
  2160. my $pre = $alert_mark;
  2161. # $logline_maxlen should be less than (1023 - prefix) for a typical syslog,
  2162. # 980 is a suitable length to avoid truncations by the syslogd daemon
  2163. my $logline_size = $logline_maxlen;
  2164. $logline_size = 50 if $logline_size < 50; # let at least something out
  2165. while (length($am_id)+length($pre)+length($errmsg) > $logline_size) {
  2166. my $avail = $logline_size - length($am_id . $pre . '...');
  2167. $log_lines++; $! = 0;
  2168. # syslog($prio, '%s', $am_id . $pre . substr($errmsg,0,$avail) . '...');
  2169. Unix::Syslog::_isyslog($prio,
  2170. $am_id . $pre . substr($errmsg,0,$avail) . '...');
  2171. if ($! != 0) { $log_retries++; $log_status_counts{"$!"}++ }
  2172. $pre = $alert_mark . '...'; $errmsg = substr($errmsg,$avail);
  2173. }
  2174. $log_lines++; $! = 0;
  2175. # syslog($prio, '%s', $am_id . $pre . $errmsg);
  2176. Unix::Syslog::_isyslog($prio, $am_id . $pre . $errmsg);
  2177. if ($! != 0) { $log_retries++; $log_status_counts{"$!"}++ }
  2178. } else {
  2179. $log_lines++;
  2180. my $now = Time::HiRes::time;
  2181. if ($log_to_stderr || !defined $loghandle) {
  2182. my $prefix = sprintf('%s:%06.3f %s %s[%s]: ', # syslog-like prefix
  2183. strftime('%b %e %H:%M',localtime($now)), $now-int($now/60)*60,
  2184. c('myhostname'), c('myprogram_name'), $$); # milliseconds in timestamp
  2185. # avoid multiple calls to write(2), join the string first!
  2186. my $s = $prefix . $am_id . $alert_mark . $errmsg . "\n";
  2187. print STDERR ($s) or die "Error writing to STDERR: $!";
  2188. } else {
  2189. my $prefix = sprintf('%s %s %s[%s]: ', # prepare a syslog-like prefix
  2190. strftime('%b %e %H:%M:%S',localtime($now)),
  2191. c('myhostname'), c('myprogram_name'), $$);
  2192. my $s = $prefix . $am_id . $alert_mark . $errmsg . "\n";
  2193. # NOTE: a lock is on a file, not on a file handle
  2194. flock($loghandle,LOCK_EX) or die "Can't lock a log file: $!";
  2195. seek($loghandle,0,2) or die "Can't position log file to its tail: $!";
  2196. $loghandle->print($s) or die "Error writing to log file: $!";
  2197. flock($loghandle,LOCK_UN) or die "Can't unlock a log file: $!";
  2198. }
  2199. }
  2200. # POSIX::setlocale(LC_TIME, $old_locale);
  2201. $within_write_log = 0;
  2202. }
  2203. 1;
  2204. #
  2205. package Amavis::DbgLog;
  2206. use strict;
  2207. use re 'taint';
  2208. BEGIN {
  2209. use vars qw(@ISA $VERSION);
  2210. $VERSION = '2.316';
  2211. import Amavis::Conf qw(:platform $TEMPBASE);
  2212. import Amavis::Log qw(write_log);
  2213. }
  2214. use POSIX qw(locale_h strftime);
  2215. use IO::File ();
  2216. use Time::HiRes ();
  2217. # use File::Temp ();
  2218. sub new {
  2219. my($class) = @_;
  2220. my($self,$fh);
  2221. # eval { # calls croak() if an error occurs
  2222. # $fh = File::Temp->new(DIR => $TEMPBASE, SUFFIX => '.log',
  2223. # TEMPLATE => sprintf('dbg-%05d-XXXXXXXX',$my_pid));
  2224. # $fh or warn "Can't create a temporary debug log file: $!";
  2225. # 1;
  2226. # } or do {
  2227. # my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  2228. # warn "Can't create a temporary debug log file: $eval_stat";
  2229. # };
  2230. $fh = IO::File->new_tmpfile;
  2231. $fh or warn "Can't create a temporary debug log file: $!";
  2232. $self = bless { fh => $fh }, $class if $fh;
  2233. $self;
  2234. }
  2235. sub DESTROY {
  2236. my($self) = @_;
  2237. undef $self->{fh};
  2238. };
  2239. sub flush {
  2240. my($self) = @_;
  2241. my $fh = $self->{fh};
  2242. !$fh ? 1 : $fh->flush;
  2243. }
  2244. sub reposition_to_end {
  2245. my($self) = @_;
  2246. my $fh = $self->{fh};
  2247. !$fh ? 1 : seek($fh,0,2);
  2248. }
  2249. # Log to a temporary file, to be retrieved later by dump_captured_log()
  2250. #
  2251. sub write_dbg_log {
  2252. my($self, $level,$errmsg) = @_;
  2253. my $fh = $self->{fh};
  2254. # ignoring failures
  2255. $fh->printf("%06.3f %d %s\n", Time::HiRes::time, $level, $errmsg) if $fh;
  2256. 1;
  2257. }
  2258. sub dump_captured_log {
  2259. my($self, $dump_log_level,$enable_log_capture_dump) = @_;
  2260. my $fh = $self->{fh};
  2261. if ($fh) {
  2262. # copy the captured temporary log to a real log if requested
  2263. if ($enable_log_capture_dump) {
  2264. $fh->flush or die "Can't flush debug log file: $!";
  2265. $fh->seek(0,0) or die "Can't rewind debug log file: $!";
  2266. my($ln,$any_logged);
  2267. for ($! = 0; defined($ln=<$fh>); $! = 0) {
  2268. chomp($ln);
  2269. my($timestamp,$level,$errmsg) = split(/ /,$ln,3);
  2270. if (!$any_logged) {
  2271. write_log($dump_log_level, 'CAPTURED DEBUG LOG DUMP BEGINS');
  2272. $any_logged = 1;
  2273. }
  2274. write_log($dump_log_level,
  2275. sprintf('%s:%06.3f %s',
  2276. strftime('%H:%M', localtime($timestamp)),
  2277. $timestamp - int($timestamp/60)*60, $errmsg));
  2278. }
  2279. defined $ln || $! == 0 or die "Error reading from debug log file: $!";
  2280. write_log($dump_log_level, 'CAPTURED DEBUG LOG DUMP ENDS')
  2281. if $any_logged;
  2282. }
  2283. # clear the temporary file, prepare it for re-use
  2284. $fh->seek(0,0) or die "Can't rewind debug log file: $!";
  2285. $fh->truncate(0) or die "Can't truncate debug log file: $!";
  2286. }
  2287. 1;
  2288. }
  2289. 1;
  2290. #
  2291. package Amavis::Timing;
  2292. use strict;
  2293. use re 'taint';
  2294. BEGIN {
  2295. require Exporter;
  2296. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  2297. $VERSION = '2.316';
  2298. @ISA = qw(Exporter);
  2299. @EXPORT_OK = qw(&init &section_time &report &get_time_so_far);
  2300. }
  2301. use subs @EXPORT_OK;
  2302. use vars qw(@timing);
  2303. use Time::HiRes ();
  2304. # clear array @timing and enter start time
  2305. #
  2306. sub init() {
  2307. @timing = (); section_time('init');
  2308. }
  2309. # enter current time reading into array @timing
  2310. #
  2311. sub section_time($) {
  2312. push(@timing, shift, Time::HiRes::time);
  2313. }
  2314. # returns a string - a report of elapsed time by section
  2315. #
  2316. sub report() {
  2317. section_time('rundown');
  2318. my($notneeded, $t0) = (shift(@timing), shift(@timing));
  2319. my $total = $t0 <= 0 ? 0 : $timing[-1] - $t0;
  2320. if ($total < 0.0000001) { $total = 0.0000001 }
  2321. my(@sections); my $t00 = $t0;
  2322. while (@timing) {
  2323. my($section, $t) = (shift(@timing), shift(@timing));
  2324. my $dt = $t <= $t0 ? 0 : $t-$t0; # handle possible clock jumps
  2325. my $dt_c = $t <= $t00 ? 0 : $t-$t00; # handle possible clock jumps
  2326. my $dtp = $dt >= $total ? 100 : $dt*100.0/$total; # this event
  2327. my $dtp_c = $dt_c >= $total ? 100 : $dt_c*100.0/$total; # cumulative
  2328. push(@sections, sprintf('%s: %.0f (%.0f%%)%.0f',
  2329. $section, $dt*1000, $dtp, $dtp_c));
  2330. $t0 = $t;
  2331. }
  2332. sprintf('TIMING [total %.0f ms] - %s', $total * 1000, join(', ',@sections));
  2333. }
  2334. # returns value in seconds of elapsed time for processing of this mail so far
  2335. #
  2336. sub get_time_so_far() {
  2337. my($notneeded, $t0) = @timing;
  2338. my $total = $t0 <= 0 ? 0 : Time::HiRes::time - $t0;
  2339. $total < 0 ? 0 : $total;
  2340. }
  2341. use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0);
  2342. sub idle_proc(@) {
  2343. my $t1 = Time::HiRes::time;
  2344. if (defined $t0) {
  2345. ($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0;
  2346. Amavis::Util::ll(5) && Amavis::Util::do_log(5,
  2347. 'idle_proc, %s: was %s, %.1f ms, total idle %.3f s, busy %.3f s',
  2348. $_[0], $t_was_busy ? 'busy' : 'idle', 1000*($t1 - $t0),
  2349. $t_idle_cum, $t_busy_cum);
  2350. }
  2351. $t0 = $t1;
  2352. }
  2353. sub go_idle(@) {
  2354. if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 }
  2355. }
  2356. sub go_busy(@) {
  2357. if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 }
  2358. }
  2359. sub report_load() {
  2360. $t_busy_cum + $t_idle_cum <= 0 ? undef
  2361. : sprintf('load: %.0f %%, total idle %.3f s, busy %.3f s',
  2362. 100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum);
  2363. }
  2364. 1;
  2365. #
  2366. package Amavis::Util;
  2367. use strict;
  2368. use re 'taint';
  2369. BEGIN {
  2370. require Exporter;
  2371. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  2372. $VERSION = '2.316';
  2373. @ISA = qw(Exporter);
  2374. @EXPORT_OK = qw(&untaint &untaint_inplace
  2375. &min &max &minmax &unique_list &unique_ref
  2376. &safe_encode &safe_encode_ascii &safe_encode_utf8
  2377. &safe_decode &q_encode &orcpt_encode &orcpt_decode
  2378. &xtext_encode &xtext_decode &proto_encode &proto_decode
  2379. &ll &do_log &do_log_safe &snmp_count &snmp_count64
  2380. &snmp_counters_init &snmp_counters_get &snmp_initial_oids
  2381. &debug_oneshot &update_current_log_level
  2382. &flush_captured_log &reposition_captured_log_to_end
  2383. &dump_captured_log &log_capture_enabled
  2384. &am_id &new_am_id &stir_random
  2385. &add_entropy &fetch_entropy_bytes
  2386. &generate_mail_id &make_password
  2387. &crunching_start_time &prolong_timer &get_deadline
  2388. &waiting_for_client &switch_to_my_time &switch_to_client_time
  2389. &sanitize_str &sanitize_str_inplace &fmt_struct
  2390. &freeze &thaw &ccat_split &ccat_maj &cmp_ccat &cmp_ccat_maj
  2391. &setting_by_given_contents_category_all
  2392. &setting_by_given_contents_category &rmdir_recursively
  2393. &read_file &read_text &read_l10n_templates
  2394. &read_hash &read_array &dump_hash &dump_array
  2395. &dynamic_destination &collect_equal_delivery_recips);
  2396. import Amavis::Conf qw(:platform $DEBUG c cr ca $mail_id_size_bits
  2397. $myversion $myhostname $snmp_contact $snmp_location
  2398. $trim_trailing_space_in_lookup_result_fields);
  2399. import Amavis::Log qw(amavis_log_id write_log);
  2400. import Amavis::Timing qw(section_time);
  2401. }
  2402. use subs @EXPORT_OK;
  2403. use Errno qw(ENOENT EACCES EAGAIN ESRCH EBADF);
  2404. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  2405. use Digest::MD5; # 2.22 provides 'clone' method, no longer needed since 2.7.0
  2406. use MIME::Base64;
  2407. use Encode; # Perl 5.8 UTF-8 support
  2408. use Scalar::Util qw(tainted);
  2409. use vars qw($enc_ascii $enc_utf8 $enc_tainted);
  2410. BEGIN {
  2411. $enc_ascii = Encode::find_encoding('ascii');
  2412. $enc_utf8 = Encode::find_encoding('UTF-8');
  2413. $enc_ascii or die "Amavis::Util: unknown encoding 'ascii'";
  2414. $enc_utf8 or die "Amavis::Util: unknown encoding 'UTF-8'";
  2415. $enc_tainted = substr($ENV{PATH}.$ENV{HOME}, 0,0); # tainted empty string
  2416. tainted($enc_tainted) or warn "Amavis::Util: can't obtain a tainted string";
  2417. 1;
  2418. }
  2419. # Return untainted copy of a string (argument can be a string or a string ref)
  2420. #
  2421. sub untaint($) {
  2422. return undef if !defined $_[0]; # must return undef even in a list context!
  2423. no re 'taint';
  2424. local $1; # avoid Perl taint bug: tainted global $1 propagates taintedness
  2425. (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s;
  2426. $1;
  2427. }
  2428. sub untaint_inplace($) {
  2429. return undef if !defined $_[0]; # must return undef even in a list context!
  2430. no re 'taint';
  2431. local $1; # avoid Perl taint bug: tainted global $1 propagates taintedness
  2432. $_[0] =~ /^(.*)\z/s;
  2433. $_[0] = $1;
  2434. }
  2435. # Returns the smallest defined number from the list, or undef
  2436. #
  2437. sub min(@) {
  2438. my $r = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
  2439. my $m; defined $_ && (!defined $m || $_ < $m) && ($m = $_) for @$r;
  2440. $m;
  2441. }
  2442. # Returns the largest defined number from the list, or undef
  2443. #
  2444. sub max(@) {
  2445. my $r = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
  2446. my $m; defined $_ && (!defined $m || $_ > $m) && ($m = $_) for @$r;
  2447. $m;
  2448. }
  2449. # Returns a pair of the smallest and the largest defined number from the list
  2450. #
  2451. sub minmax(@) {
  2452. my $r = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
  2453. my $min; my $max;
  2454. for (@$r) {
  2455. if (defined $_) {
  2456. $min = $_ if !defined $min || $_ < $min;
  2457. $max = $_ if !defined $max || $_ > $max;
  2458. }
  2459. }
  2460. ($min,$max);
  2461. }
  2462. # Returns a sublist of the supplied list of elements in an unchanged order,
  2463. # where only the first occurrence of each defined element is retained
  2464. # and duplicates removed
  2465. #
  2466. sub unique_list(@) {
  2467. my $r = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
  2468. my %seen; my(@result) = grep(defined($_) && !$seen{$_}++, @$r);
  2469. @result;
  2470. }
  2471. # same as unique, except that it returns a ref to the resulting list
  2472. #
  2473. sub unique_ref(@) {
  2474. my $r = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref
  2475. my %seen; my(@result) = grep(defined($_) && !$seen{$_}++, @$r);
  2476. \@result;
  2477. }
  2478. # A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes
  2479. # Encode::encode to loop and fill memory when given a tainted string.
  2480. # Also works around a CPAN bug #64642 in module Encode:
  2481. # Tainted values have the taint flag cleared when encoded (or decoded)
  2482. # https://rt.cpan.org/Public/Bug/Display.html?id=64642
  2483. # (still unresolved with Encode as bundled with Perl 5.14.2)
  2484. #
  2485. sub safe_encode($$;$) {
  2486. # my($encoding,$str,$check) = @_;
  2487. my $encoding = shift;
  2488. return undef if !defined $_[0]; # must return undef even in a list context!
  2489. my $enc = Encode::find_encoding($encoding);
  2490. $enc or die "safe_encode: unknown encoding '$encoding'";
  2491. return $enc->encode(@_) if !tainted($_[0]);
  2492. # propagate taintedness across taint-related bugs in module Encode
  2493. $enc_tainted . $enc->encode(untaint($_[0]), $_[1]);
  2494. }
  2495. sub safe_encode_ascii($) {
  2496. # my($str) = @_;
  2497. return undef if !defined $_[0]; # must return undef even in a list context!
  2498. return $enc_ascii->encode($_[0], 0) if !tainted($_[0]);
  2499. # propagate taintedness across taint-related bugs in module Encode
  2500. $enc_tainted . $enc_ascii->encode(untaint($_[0]), 0);
  2501. }
  2502. sub safe_encode_utf8($) {
  2503. # my($str) = @_;
  2504. return undef if !defined $_[0]; # must return undef even in a list context!
  2505. return $enc_utf8->encode($_[0], 0) if !tainted($_[0]);
  2506. # propagate taintedness across taint-related bugs in module Encode
  2507. $enc_tainted . $enc_utf8->encode(untaint($_[0]), 0);
  2508. }
  2509. sub safe_decode($$;$) {
  2510. # my($encoding,$str,$check) = @_;
  2511. my $encoding = shift;
  2512. return undef if !defined $_[0]; # must return undef even in a list context!
  2513. my $enc = Encode::find_encoding($encoding);
  2514. return $_[0] if !$enc;
  2515. return $enc->decode(@_) if !tainted($_[0]);
  2516. # propagate taintedness across taint-related bugs in module Encode
  2517. $enc_tainted . $enc->decode(untaint($_[0]), $_[1]);
  2518. }
  2519. # Do the Q-encoding manually, the MIME::Words::encode_mimeword does not
  2520. # encode spaces and does not limit to 75 ch, which violates the RFC 2047
  2521. #
  2522. sub q_encode($$$) {
  2523. my($octets,$encoding,$charset) = @_;
  2524. my $prefix = '=?' . $charset . '?' . $encoding . '?';
  2525. my $suffix = '?='; local($1,$2,$3);
  2526. # FWS | utext (= NO-WS-CTL|rest of US-ASCII)
  2527. $octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?)
  2528. ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx;
  2529. my($head,$rest,$tail) = ($1,$2,$3);
  2530. # Q-encode $rest according to RFC 2047 (not for use in comments or phrase)
  2531. $rest =~ s{([\000-\037\177\200-\377=?_])}{sprintf('=%02X',ord($1))}egs;
  2532. $rest =~ tr/ /_/; # turn spaces into _ (RFC 2047 allows it)
  2533. my $s = $head; my $len = 75 - (length($prefix)+length($suffix)) - 2;
  2534. while ($rest ne '') {
  2535. $s .= ' ' if $s !~ /[ \t]\z/; # encoded words must be separated by FWS
  2536. $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx;
  2537. $s .= $prefix.$1.$suffix; $rest = $2;
  2538. }
  2539. $s.$tail;
  2540. }
  2541. # encode "+", "=" and any character outside the range "!" (33) .. "~" (126)
  2542. #
  2543. sub xtext_encode($) { # RFC 3461
  2544. my($str) = @_; local($1);
  2545. # avoid Encode::is_utf8 check, always false on tainted, Perl bug #32687
  2546. $str = safe_encode_utf8($str); # if Encode::is_utf8($str);
  2547. $str =~ s/([^\041-\052\054-\074\076-\176])/sprintf('+%02X',ord($1))/egs;
  2548. $str;
  2549. }
  2550. # decode xtext-encoded string as per RFC 3461
  2551. #
  2552. sub xtext_decode($) {
  2553. my($str) = @_; local($1);
  2554. $str =~ s/\+([0-9a-fA-F]{2})/pack('C',hex($1))/egs;
  2555. $str;
  2556. }
  2557. sub proto_encode($@) {
  2558. my($attribute_name,@strings) = @_; local($1);
  2559. for ($attribute_name,@strings) {
  2560. # just in case, handle non-octet characters:
  2561. s/([^\000-\377])/sprintf('\\x{%04x}',ord($1))/egs and
  2562. do_log(-1,'proto_encode: non-octet character encountered: %s', $_);
  2563. }
  2564. $attribute_name =~ # encode all but alfanumerics, . _ + -
  2565. s/([^0-9a-zA-Z._+-])/sprintf('%%%02x',ord($1))/egs;
  2566. for (@strings) { # encode % and nonprintables
  2567. s/([^\041-\044\046-\176])/sprintf('%%%02x',ord($1))/egs;
  2568. }
  2569. $attribute_name . '=' . join(' ',@strings);
  2570. }
  2571. sub proto_decode($) {
  2572. my($str) = @_; local($1);
  2573. $str =~ s/%([0-9a-fA-F]{2})/pack('C',hex($1))/egs;
  2574. $str;
  2575. }
  2576. # xtext_encode and prepend 'rfc822;' to form a string to be used as ORCPT
  2577. #
  2578. sub orcpt_encode($) { # RFC 3461
  2579. # RFC 3461: Due to limitations in the Delivery Status Notification format,
  2580. # the value of the original recipient address prior to encoding as "xtext"
  2581. # MUST consist entirely of printable (graphic and white space) characters
  2582. # from the US-ASCII [4] repertoire.
  2583. my($str) = @_; local($1); # argument should be SMTP-quoted address
  2584. $str = $1 if $str =~ /^<(.*)>\z/s; # strip-off <>
  2585. $str =~ s/[^\040-\176]/?/gs;
  2586. 'rfc822;' . xtext_encode($str);
  2587. }
  2588. sub orcpt_decode($) { # RFC 3461
  2589. my($str) = @_; # argument should be RFC 3461 -encoded address
  2590. my($addr_type,$orcpt); local($1,$2);
  2591. if (defined $str) {
  2592. if ($str =~ /^([^\000-\040\177()<>\[\]\@\\:;,."]*);(.*\z)/si){ # atom;xtext
  2593. ($addr_type,$orcpt) = ($1,$2);
  2594. } else {
  2595. ($addr_type,$orcpt) = ('rfc822',$str); # RFC 3464 address-type
  2596. }
  2597. $orcpt = xtext_decode($orcpt); # decode
  2598. $orcpt =~ s/[^\040-\176]/?/gs; # some minimal sanitation
  2599. }
  2600. # result in $orcpt is presumably an RFC 5322 -encoded addr, no angle brackets
  2601. ($addr_type,$orcpt);
  2602. }
  2603. # Mostly for debugging and reporting purposes:
  2604. # Convert nonprintable characters in the argument
  2605. # to \[rnftbe], or \octal code, ( and '\' to '\\' ???),
  2606. # and Unicode characters to UTF-8, returning a sanitized string.
  2607. #
  2608. use vars qw(%quote_controls_map);
  2609. BEGIN {
  2610. %quote_controls_map =
  2611. ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t',
  2612. "\b" => '\\b', "\e" => '\\e' ); # "\\" => '\\\\'
  2613. }
  2614. sub sanitize_str {
  2615. my($str, $keep_eol) = @_;
  2616. return '' if !defined $str;
  2617. my $taint = '';
  2618. # Encode::is_utf8 is always false on tainted in Perl 5.8, Perl bug #32687
  2619. if ($] < 5.010 || Encode::is_utf8($_[0])) {
  2620. # inlined: $str = safe_encode_utf8($str);
  2621. # obtain taintedness of the string, with UTF8 flag unconditionally off
  2622. $taint = $enc_ascii->encode(substr($str,0,0));
  2623. # untaint the string to work around a Perl 5.8.0 taint bug
  2624. # where Encode::encode fills up all available memory
  2625. # when given a tainted string with a non-encodeable character
  2626. untaint_inplace($str);
  2627. $str = $enc_utf8->encode($str, 0); # convert to octets
  2628. }
  2629. local($1);
  2630. if ($keep_eol) {
  2631. $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ?
  2632. exists($quote_controls_map{$1}) ? $quote_controls_map{$1} :
  2633. sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/egs;
  2634. } else {
  2635. $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ?
  2636. exists($quote_controls_map{$1}) ? $quote_controls_map{$1} :
  2637. sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/egs;
  2638. }
  2639. $str .= $taint; # preserve taintedness
  2640. $str;
  2641. }
  2642. sub sanitize_str_inplace {
  2643. my $taint = '';
  2644. # Encode::is_utf8 is always false on tainted in Perl 5.8, Perl bug #32687
  2645. if ($] < 5.010 || Encode::is_utf8($_[0])) {
  2646. # inlined: $_[0] = safe_encode_utf8($_[0]);
  2647. # obtain taintedness of the string, with UTF8 flag unconditionally off
  2648. $taint = $enc_ascii->encode(substr($_[0],0,0));
  2649. # untaint the string to work around a Perl 5.8.0 taint bug
  2650. # where Encode::encode fills up all available memory
  2651. # when given a tainted string with a non-encodeable character
  2652. untaint_inplace($_[0]);
  2653. $_[0] = $enc_utf8->encode($_[0], 0); # convert to octets
  2654. }
  2655. local($1);
  2656. $_[0] =~ s/([^\040-\133\135-\176])/ # and \240-\376 ?
  2657. exists($quote_controls_map{$1}) ? $quote_controls_map{$1} :
  2658. sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/egs;
  2659. $_[0] .= $taint;
  2660. 1;
  2661. }
  2662. # Set or get Amavis internal task id (also called: log id).
  2663. # This task id performs a similar function as queue-id in MTA responses.
  2664. # It may only be used in generating text part of SMTP responses,
  2665. # or in generating log entries. It is only unique within a limited timespan.
  2666. use vars qw($amavis_task_id); # internal task id
  2667. # (accessible via am_id() and later also as $msginfo->log_id)
  2668. sub am_id(;$) {
  2669. if (@_) { # set, if argument is present
  2670. $amavis_task_id = shift;
  2671. amavis_log_id($amavis_task_id);
  2672. $0 = c('myprogram_name') .
  2673. (!defined $amavis_task_id ? '' : " ($amavis_task_id)");
  2674. }
  2675. $amavis_task_id; # return current value
  2676. }
  2677. sub new_am_id($;$$) {
  2678. my($str, $cnt, $seq) = @_;
  2679. my $id = defined $str ? $str : sprintf('%05d', $$);
  2680. $id .= sprintf('-%02d', $cnt) if defined $cnt;
  2681. $id .= '-'.$seq if defined $seq && $seq > 1;
  2682. am_id($id);
  2683. }
  2684. use vars qw($entropy); # MD5 ctx (128 bits, 32 hex digits or 22 base64 chars)
  2685. sub add_entropy(@) { # arguments may be strings or array references
  2686. $entropy = Digest::MD5->new if !defined $entropy;
  2687. my $s = join(',', map((!defined $_ ? 'U' : ref eq 'ARRAY' ? @$_ : $_), @_));
  2688. # do_log(5,'add_entropy: %s',$s);
  2689. $entropy->add($s);
  2690. }
  2691. sub fetch_entropy_bytes($) {
  2692. my($n) = @_; # number of bytes to collect
  2693. my $result = '';
  2694. for (; $n > 0; $n--) {
  2695. # collect as few bits per MD5 iteration as possible (RFC 4086 sect 6.2.1)
  2696. # let's settle for 8 bits for practical reasons; fewer would be better
  2697. my $digest = $entropy->digest; # 16 bytes; also destroys accumulator
  2698. $result .= substr($digest,0,1); # take 1 byte
  2699. $entropy->reset; $entropy->add($digest); # cycle it back
  2700. }
  2701. # ll(5) && do_log(5,'fetch_entropy_bytes %s',
  2702. # join(' ', map(sprintf('%02x',$_), unpack('C*',$result))));
  2703. $result;
  2704. }
  2705. # read number of bytes from a /dev/urandom device
  2706. #
  2707. sub read_random($) {
  2708. my($required_bytes) = @_;
  2709. my $result = '';
  2710. my $fname = '/dev/urandom'; # nonblocking device!
  2711. if ($required_bytes > 0) {
  2712. my $fh = IO::File->new;
  2713. $fh->open($fname,'<') or die "Can't open $fname: $!";
  2714. binmode($fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  2715. my $nbytes = $fh->read($result, $required_bytes);
  2716. defined $nbytes or die "Error reading from $fname: $!";
  2717. $nbytes >= $required_bytes or die "Less data than requested: $!";
  2718. $fh->close or die "Error closing $fname: $!";
  2719. }
  2720. $result;
  2721. }
  2722. # stir/initialize perl's random generator and our entropy pool;
  2723. # to be called at startup of the main process and each child processes
  2724. #
  2725. sub stir_random() {
  2726. my $random_bytes;
  2727. eval {
  2728. $random_bytes = read_random(16); 1;
  2729. } or do {
  2730. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  2731. do_log(0, 'read_random error: %s', $eval_stat);
  2732. };
  2733. srand(); # let perl give it a try first, then stir-in some additional bits
  2734. add_entropy($random_bytes, Time::HiRes::gettimeofday, $$, rand());
  2735. #
  2736. # must prevent all child processes working with the same inherited random
  2737. # seed, otherwise modules like File::Temp will step on each other's toes
  2738. my $r = unpack('L', fetch_entropy_bytes(4)) ^ int(rand(0xffffffff));
  2739. srand($r & 0x7fffffff);
  2740. }
  2741. # generate a reasonably unique (long-term) id based on collected entropy.
  2742. # The result is a pair of (mostly public) mail_id, and a secret id,
  2743. # where mail_id == b64(md5(secret_bin)). The secret id could be used to
  2744. # authorize releasing quarantined mail. Both the mail_id and secret id are
  2745. # strings of characters [A-Za-z0-9-_], with an additional restriction
  2746. # for mail_id which must begin and end with an alphanumeric character.
  2747. # The number of bits in a mail_id is configurable through $mail_id_size_bits
  2748. # and defaults to 72, yielding a 12-character base64url-encoded string.
  2749. # The number of bits must be an integral multiple of 24, so that no base64
  2750. # trailing padding characters '=' are needed (RFC 4648).
  2751. # Note the difference in base64-like encodings:
  2752. # amavisd almost-base64: 62 +, 63 - (old, no longer used since 2.7.0)
  2753. # RFC 4648 base64: 62 +, 63 / (not used here)
  2754. # RFC 4648 base64url: 62 -, 63 _
  2755. # Generally, RFC 5322 controls, SP and specials must be avoided: ()<>[]:;@\,."
  2756. # With version 2.7.0 of amavisd we switched from almost-base64 to base64url
  2757. # to avoid having to quote a '+' in regular expressions and in URL.
  2758. #
  2759. sub generate_mail_id() {
  2760. my($id_b64, $secret_bin);
  2761. # 72 bits = 9 bytes = 12 b64 chars
  2762. # 96 bits = 12 bytes = 16 b64 chars
  2763. $mail_id_size_bits > 0 &&
  2764. $mail_id_size_bits == int $mail_id_size_bits &&
  2765. $mail_id_size_bits % 24 == 0
  2766. or die "\$mail_id_size_bits ($mail_id_size_bits) must be a multiple of 24";
  2767. for (my $j=0; $j<100; $j++) { # provide some sanity loop limit just in case
  2768. $secret_bin = fetch_entropy_bytes($mail_id_size_bits/8);
  2769. # mail_id is computed as md5(secret), rely on unidirectionality of md5
  2770. $id_b64 = Digest::MD5->new->add($secret_bin)->b64digest; # b64(md5(sec))
  2771. add_entropy($id_b64,$j); # fold it back into accumulator
  2772. $id_b64 = substr($id_b64, 0, $mail_id_size_bits/6); # b64, crop to size
  2773. # done if it starts and ends with an alfanumeric character
  2774. last if $id_b64 =~ /^[A-Za-z0-9].*[A-Za-z0-9]\z/s;
  2775. # retry on less than 7% of cases
  2776. do_log(5,'generate_mail_id retry: %s', $id_b64);
  2777. }
  2778. my $secret_b64 = encode_base64($secret_bin,''); # $mail_id_size_bits/6 chars
  2779. $secret_bin = 'X' x length($secret_bin); # can't hurt to be conservative
  2780. $id_b64 =~ tr{+/}{-_}; # base64 -> RFC 4648 base64url [A-Za-z0-9-_]
  2781. $secret_b64 =~ tr{+/}{-_}; # base64 -> RFC 4648 base64url [A-Za-z0-9-_]
  2782. # do_log(5,'generate_mail_id: %s %s', $id_b64, $secret_b64);
  2783. ($id_b64, $secret_b64);
  2784. }
  2785. # Returns a password that may be used for scrambling of a message being
  2786. # released from a quarantine or mangled, with intention of preventing an
  2787. # automatic or undesired implicit opening of a potentially dangerous message.
  2788. # The first argument may be: a plain string, which is simply passed on
  2789. # to the result, or: a code reference (to be evaluated in a scalar context),
  2790. # allowing for lazy evaluation of a supplied password generating code,
  2791. # or: undef, which causes a generation of a simple 4-digit PIN-like random
  2792. # password. The second argument is just passed on unchanged to the supplied
  2793. # subroutine and is expected to be a $msginfo object.
  2794. #
  2795. sub make_password($$) {
  2796. my($password,$msginfo) = @_;
  2797. if (ref $password eq 'CODE') {
  2798. eval {
  2799. $password = &$password($msginfo);
  2800. chomp $password; $password =~ s/^[ \t]+//; $password =~ s/[ \t]+\z//;
  2801. untaint_inplace($password) if $password =~ /^[A-Za-z0-9:._=+-]*\z/;
  2802. 1;
  2803. } or do {
  2804. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  2805. do_log(-1, 'password generating subroutine failed, '.
  2806. 'supplying a default: %s', $@);
  2807. $password = undef;
  2808. };
  2809. }
  2810. if (!defined $password) { # create a 4-digit random string
  2811. $password =
  2812. sprintf('%04d', unpack('S',fetch_entropy_bytes(2)) % 10000);
  2813. }
  2814. $password;
  2815. }
  2816. use vars qw(@counter_names);
  2817. # elements may be counter names (increment is 1), or pairs: [name,increment],
  2818. # or triples: [name,value,type], where type can be: C32, C64, INT, TIM or OID
  2819. sub snmp_counters_init() { @counter_names = () }
  2820. sub snmp_count(@) { push(@counter_names, @_) }
  2821. sub snmp_count64(@) { push(@counter_names, map(ref $_ ?$_ :[$_,1,'C64'], @_)) }
  2822. sub snmp_counters_get() { \@counter_names }
  2823. sub snmp_initial_oids() {
  2824. return [
  2825. ['sysDescr', 'STR', $myversion],
  2826. ['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2'],
  2827. # iso.org.dod.internet.private.enterprise.ijs.amavisd-new
  2828. ['sysUpTime', 'INT', int(time)], # to be converted to TIM
  2829. # later it must be converted to timeticks (10ms since start)
  2830. ['sysContact', 'STR', $snmp_contact],
  2831. ['sysName', 'STR', $myhostname],
  2832. ['sysLocation', 'STR', $snmp_location],
  2833. ['sysServices', 'INT', 64], # application
  2834. ];
  2835. }
  2836. use vars qw($debug_oneshot);
  2837. sub debug_oneshot(;$$) {
  2838. if (@_) {
  2839. my $new_debug_oneshot = shift;
  2840. if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) {
  2841. do_log(0, 'DEBUG_ONESHOT: TURNED '.($new_debug_oneshot ? 'ON' : 'OFF'));
  2842. do_log(0, shift) if @_; # caller-provided extra log entry, usually
  2843. # the one that caused debug_oneshot call
  2844. }
  2845. $debug_oneshot = $new_debug_oneshot;
  2846. }
  2847. $debug_oneshot;
  2848. }
  2849. use vars qw($dbg_log);
  2850. sub log_capture_enabled(;$) {
  2851. if (@_) {
  2852. my $new_state = shift;
  2853. if (!$dbg_log && $new_state) {
  2854. $dbg_log = Amavis::DbgLog->new;
  2855. } elsif ($dbg_log && !$new_state) {
  2856. undef $dbg_log; # calls its destructor
  2857. }
  2858. }
  2859. $dbg_log ? 1 : 0;
  2860. }
  2861. use vars qw($current_config_log_level
  2862. $current_config_syslog_ident
  2863. $current_config_syslog_facility);
  2864. # keeping current settings avoids the most frequent calls to c()
  2865. sub update_current_log_level() {
  2866. $current_config_log_level = c('log_level') || 0;
  2867. $current_config_syslog_ident = c('syslog_ident');
  2868. $current_config_syslog_facility = c('syslog_facility');
  2869. }
  2870. # is message log level below the current log level (i.e. eligible for logging)?
  2871. #
  2872. sub ll($) {
  2873. (($DEBUG || $debug_oneshot) && $_[0] > 0 ? 0 : $_[0])
  2874. <= $current_config_log_level
  2875. || $dbg_log;
  2876. }
  2877. # write a log entry
  2878. #
  2879. sub do_log($$;@) { # my($level,$errmsg,@args) = @_;
  2880. my $level = shift;
  2881. # if (ll($level)) { # inline and reorder the ll() call for speed
  2882. if ( $level <= $current_config_log_level ||
  2883. ( ($DEBUG || $debug_oneshot) && $level > 0
  2884. && 0 <= $current_config_log_level ) ||
  2885. $dbg_log ) {
  2886. my $errmsg = shift;
  2887. # treat $errmsg as sprintf format string if additional args are provided
  2888. $errmsg = sprintf($errmsg,@_) if @_;
  2889. sanitize_str_inplace($errmsg);
  2890. $dbg_log->write_dbg_log($level,$errmsg) if $dbg_log;
  2891. $level = 0 if ($DEBUG || $debug_oneshot) && $level > 0;
  2892. if ($level <= $current_config_log_level) {
  2893. write_log($level,$errmsg);
  2894. ### $Amavis::zmq_obj->write_log($level,$errmsg) if $Amavis::zmq_obj;
  2895. }
  2896. }
  2897. 1;
  2898. }
  2899. # equivalent to do_log, but protected by eval so that it can't bail out
  2900. #
  2901. sub do_log_safe($$;@) {
  2902. # ignore failures while keeping perlcritic happy
  2903. eval { do_log(shift,shift,@_) } or 1;
  2904. 1;
  2905. }
  2906. sub flush_captured_log() {
  2907. $dbg_log->flush
  2908. or die "Can't flush debug log file: $!" if $dbg_log;
  2909. }
  2910. sub reposition_captured_log_to_end() {
  2911. $dbg_log->reposition_to_end
  2912. or die "Can't reposition debug log file to its end: $!" if $dbg_log;
  2913. }
  2914. sub dump_captured_log($$) {
  2915. my($dump_log_level, $enable_log_capture_dump) = @_;
  2916. $dbg_log->dump_captured_log($dump_log_level,
  2917. $enable_log_capture_dump && ll($dump_log_level)) if $dbg_log;
  2918. }
  2919. # $timestamp_of_last_reception: a Unix time stamp when an MTA client send the
  2920. # last command to us, the most important of which is the reception of a final
  2921. # dot in SMTP session, which is a time when a client started to wait for our
  2922. # response; this timestamp, along with a c('child_timeout'), make a deadline
  2923. # time for our processing
  2924. #
  2925. # $waiting_for_client: which timeout is running:
  2926. # false: processing is in our courtyard, true: waiting for a client
  2927. #
  2928. use vars qw($timestamp_of_last_reception $waiting_for_client);
  2929. sub waiting_for_client(;$) {
  2930. $waiting_for_client = shift if @_;
  2931. $waiting_for_client;
  2932. }
  2933. sub get_deadline(@) {
  2934. my($which_section, $allowed_share, $reserve, $max_time) = @_;
  2935. # $allowed_share ... factor between 0 and 1 of the remaining time till a
  2936. # deadline, to be allocated to the task that follows
  2937. # $reserve ... try finishing up $reserve seconds before the deadline;
  2938. # $max_time ... upper limit in seconds for the timer interval
  2939. my($timer_interval, $timer_deadline, $time_to_deadline);
  2940. my $child_t_o = c('child_timeout');
  2941. if (!$child_t_o) {
  2942. do_log(2, 'get_deadline %s - ignored, child_timeout not set',
  2943. $which_section);
  2944. } elsif (!defined $timestamp_of_last_reception) {
  2945. do_log(2, 'get_deadline %s - ignored, master deadline not known',
  2946. $which_section);
  2947. } else {
  2948. my $now = Time::HiRes::time;
  2949. $time_to_deadline = $timestamp_of_last_reception + $child_t_o - $now;
  2950. $timer_interval = $time_to_deadline;
  2951. if (!defined $allowed_share) {
  2952. $allowed_share = 0.7;
  2953. $timer_interval *= $allowed_share;
  2954. } elsif ($allowed_share <= 0) {
  2955. $timer_interval = 0;
  2956. } elsif ($allowed_share >= 1) {
  2957. # leave it unchanged
  2958. } else {
  2959. $timer_interval *= $allowed_share;
  2960. }
  2961. $reserve = 3 if !defined $reserve;
  2962. if ($reserve > 0 && $timer_interval > $time_to_deadline - $reserve) {
  2963. $timer_interval = $time_to_deadline - $reserve;
  2964. }
  2965. if ($timer_interval < 8) { # try to be generous
  2966. $timer_interval = max(4, min(8,$time_to_deadline));
  2967. }
  2968. my $j = int($timer_interval);
  2969. $timer_interval = $timer_interval > $j ? $j+1 : $j; # ceiling
  2970. if (defined $max_time && $max_time > 0 && $timer_interval > $max_time) {
  2971. $timer_interval = $max_time;
  2972. }
  2973. ll(5) && do_log(5, 'get_deadline %s - deadline in %.1f s, set to %.3f s',
  2974. $which_section, $time_to_deadline, $timer_interval);
  2975. $timer_deadline = $now + $timer_interval;
  2976. }
  2977. !wantarray ? $timer_interval
  2978. : ($timer_interval, $timer_deadline, $time_to_deadline);
  2979. }
  2980. sub prolong_timer($;$$$) {
  2981. my($which_section, $allowed_share, $reserve, $max_time) = @_;
  2982. my($timer_interval, $timer_deadline, $time_to_deadline) = get_deadline(@_);
  2983. if (defined $timer_interval) {
  2984. my $prev_timer = alarm($timer_interval); # restart/prolong the timer
  2985. ll(5) && do_log(5,'prolong_timer %s: timer %d, was %d, deadline in %.1f s',
  2986. $which_section, $timer_interval, $prev_timer, $time_to_deadline);
  2987. }
  2988. !wantarray ? $timer_interval
  2989. : ($timer_interval, $timer_deadline, $time_to_deadline);
  2990. }
  2991. sub switch_to_my_time($) { # processing is in our courtyard
  2992. my($msg) = @_;
  2993. $waiting_for_client = 0;
  2994. $timestamp_of_last_reception = Time::HiRes::time;
  2995. my $child_t_o = c('child_timeout');
  2996. if (!$child_t_o) {
  2997. alarm(0);
  2998. } else {
  2999. prolong_timer( 'switch_to_my_time(' . $msg . ')' );
  3000. }
  3001. }
  3002. sub switch_to_client_time($) { # processing is now in client's hands
  3003. my($msg) = @_;
  3004. my $interval = c('smtpd_timeout');
  3005. $interval = 5 if $interval < 5;
  3006. ll(5) && do_log(5, 'switch_to_client_time %d s, %s', $interval,$msg);
  3007. undef $timestamp_of_last_reception;
  3008. alarm($interval); $waiting_for_client = 1;
  3009. }
  3010. # pretty-print a structure for logging purposes: returns a string
  3011. #
  3012. sub fmt_struct($); # prototype
  3013. sub fmt_struct($) {
  3014. my($arg) = @_;
  3015. !defined($arg) ? 'undef'
  3016. : !ref($arg) ? '"'.$arg.'"'
  3017. : ref($arg) eq 'ARRAY' ?
  3018. '[' . join(',', map(fmt_struct($_),@$arg)) . ']'
  3019. : ref($arg) eq 'HASH' ?
  3020. '{' . join(',', map($_.'=>'.fmt_struct($arg->{$_}),keys(%$arg))) . '}'
  3021. : $arg;
  3022. };
  3023. # used by freeze: protect % and ~, as well as NUL and \200 for good measure
  3024. #
  3025. sub st_encode($) {
  3026. my($str) = @_; local($1);
  3027. $str =~ s/([%~\000\200])/sprintf('%%%02X',ord($1))/egs;
  3028. $str;
  3029. }
  3030. # simple Storable::freeze lookalike
  3031. #
  3032. sub freeze($); # prototype
  3033. sub freeze($) {
  3034. my($obj) = @_; my $ty = ref($obj);
  3035. if (!defined($obj)) { 'U' }
  3036. elsif (!$ty) { join('~', '', st_encode($obj)) } # string
  3037. elsif ($ty eq 'SCALAR') { join('~', 'S', st_encode(freeze($$obj))) }
  3038. elsif ($ty eq 'REF') { join('~', 'R', st_encode(freeze($$obj))) }
  3039. elsif ($ty eq 'ARRAY') { join('~', 'A', map(st_encode(freeze($_)),@$obj)) }
  3040. elsif ($ty eq 'HASH') {
  3041. join('~', 'H',
  3042. map {(st_encode($_),st_encode(freeze($obj->{$_})))} sort keys %$obj)
  3043. } else { die "Can't freeze object type $ty" }
  3044. }
  3045. # simple Storable::thaw lookalike
  3046. #
  3047. sub thaw($); # prototype
  3048. sub thaw($) {
  3049. my($str) = @_;
  3050. return undef if !defined $str; # must return undef even in a list context!
  3051. my($ty,@val) = split(/~/,$str,-1);
  3052. for (@val) { s/%([0-9a-fA-F]{2})/pack('C',hex($1))/egs }
  3053. if ($ty eq 'U') { undef }
  3054. elsif ($ty eq '') { $val[0] }
  3055. elsif ($ty eq 'S') { my $obj = thaw($val[0]); \$obj }
  3056. elsif ($ty eq 'R') { my $obj = thaw($val[0]); \$obj }
  3057. elsif ($ty eq 'A') { [map(thaw($_),@val)] }
  3058. elsif ($ty eq 'H') {
  3059. my $hr = {};
  3060. while (@val) { my $k = shift @val; $hr->{$k} = thaw(shift @val) }
  3061. $hr;
  3062. } else { die "Can't thaw object type $ty" }
  3063. }
  3064. # accepts either a single contents category (a string: "maj,min" or "maj"),
  3065. # or a list of contents categories, in which case only the first element
  3066. # is considered; returns a passed pair: (major_ccat, minor_ccat)
  3067. #
  3068. sub ccat_split($) {
  3069. my($ccat) = @_; my $major; my $minor;
  3070. $ccat = $ccat->[0] if ref $ccat; # pick the first element if given a list
  3071. ($major,$minor) = split(/,/,$ccat,-1) if defined $ccat;
  3072. !wantarray ? $major : ($major,$minor);
  3073. }
  3074. # accepts either a single contents category (a string: "maj,min" or "maj"),
  3075. # or a list of contents categories, in which case only the first element
  3076. # is considered; returns major_ccat
  3077. #
  3078. sub ccat_maj($) {
  3079. my($ccat) = @_; my $major; my $minor;
  3080. $ccat = $ccat->[0] if ref $ccat; # pick the first element if given a list
  3081. ($major,$minor) = split(/,/,$ccat,-1) if defined $ccat;
  3082. $major;
  3083. }
  3084. # compare numerically two strings of the form "maj,min" or just "maj", where
  3085. # maj and min are numbers, representing major and minor contents category
  3086. #
  3087. sub cmp_ccat($$) {
  3088. my($a_maj,$a_min) = split(/,/, shift, -1);
  3089. my($b_maj,$b_min) = split(/,/, shift, -1);
  3090. $a_maj == $b_maj ? $a_min <=> $b_min : $a_maj <=> $b_maj;
  3091. }
  3092. # similar to cmp_ccat, but consider only the major category of both arguments
  3093. #
  3094. sub cmp_ccat_maj($$) {
  3095. my($a_maj,$a_min) = split(/,/, shift, -1);
  3096. my($b_maj,$b_min) = split(/,/, shift, -1);
  3097. $a_maj <=> $b_maj;
  3098. }
  3099. # get a list of settings corresponding to all listed contents categories,
  3100. # ordered from the most important category to the least; @ccat is a list of
  3101. # relevant contents categories for which a query is made, it MUST already be
  3102. # sorted in descending order; this is a classical subroutine, not a method!
  3103. #
  3104. sub setting_by_given_contents_category_all($@) {
  3105. my($ccat,@settings_href_list) = @_; my(@r);
  3106. if (@settings_href_list) {
  3107. for my $e ((!defined $ccat ? () : ref $ccat ?@$ccat :$ccat), CC_CATCHALL) {
  3108. if (grep(defined($_) && exists($_->{$e}), @settings_href_list)) {
  3109. # supports lazy evaluation (a setting may be a subroutine)
  3110. my(@slist) = map { !defined($_) || !exists($_->{$e}) ? undef :
  3111. do { my $s = $_->{$e}; ref($s) eq 'CODE' ? &$s : $s}
  3112. } @settings_href_list;
  3113. push(@r, [$e,@slist]); # a tuple: [corresponding ccat, settings list]
  3114. }
  3115. }
  3116. }
  3117. @r; # a list of tuples
  3118. }
  3119. # similar to setting_by_given_contents_category_all(), but only the first
  3120. # (the most relevant) setting is returned, without a corresponding ccat
  3121. #
  3122. sub setting_by_given_contents_category($@) {
  3123. my($ccat,@settings_href_list) = @_; my(@slist);
  3124. if (@settings_href_list) {
  3125. for my $e ((!defined $ccat ? () : ref $ccat ?@$ccat :$ccat), CC_CATCHALL) {
  3126. if (grep(defined($_) && exists($_->{$e}), @settings_href_list)) {
  3127. # supports lazy evaluation (setting may be a subroutine)
  3128. @slist = map { !defined($_) || !exists($_->{$e}) ? undef :
  3129. do { my $s = $_->{$e}; ref($s) eq 'CODE' ? &$s : $s }
  3130. } @settings_href_list;
  3131. last;
  3132. }
  3133. }
  3134. }
  3135. !wantarray ? $slist[0] : @slist; # only the first entry
  3136. }
  3137. # Removes a directory, along with its contents
  3138. #
  3139. # The readdir() is entitled to fail if the directory changes underneath,
  3140. # so do the deletions by chunks: read a limited set of filenames into
  3141. # memory, close directory, delete these files, and repeat.
  3142. # The current working directory must not be within directories which are
  3143. # to be deleted, otherwise rmdir can fail with 'Invalid argument' (e.g.
  3144. # on Solaris 10).
  3145. #
  3146. sub rmdir_recursively($;$); # prototype
  3147. sub rmdir_recursively($;$) {
  3148. my($dir, $exclude_itself) = @_;
  3149. ll(4) && do_log(4, 'rmdir_recursively: %s, excl=%s', $dir,$exclude_itself);
  3150. my($f, @rmfiles, @rmdirs); my $more = 1; my $dir_chmoded = 0;
  3151. while ($more) {
  3152. local(*DIR); $more = 0;
  3153. my $errn = opendir(DIR,$dir) ? 0 : 0+$!;
  3154. if ($errn == EACCES && !$dir_chmoded) {
  3155. # relax protection on directory, then try again
  3156. do_log(3,'rmdir_recursively: enabling read access to directory %s',$dir);
  3157. chmod(0750,$dir)
  3158. or do_log(-1, "Can't change protection-1 on dir %s: %s", $dir, $!);
  3159. $dir_chmoded = 1;
  3160. $errn = opendir(DIR,$dir) ? 0 : 0+$!; # try again
  3161. }
  3162. if ($errn) { die "Can't open directory $dir: $!" }
  3163. my $cnt = 0;
  3164. # avoid slurping the whole directory contents into memory
  3165. while (defined($f = readdir(DIR))) {
  3166. next if $f eq '.' || $f eq '..';
  3167. my $fname = $dir . '/' . $f;
  3168. $errn = lstat($fname) ? 0 : 0+$!;
  3169. if ($errn == EACCES && !$dir_chmoded) {
  3170. # relax protection on the directory and retry
  3171. do_log(3,'rmdir_recursively: enabling access to files in dir %s',$dir);
  3172. chmod(0750,$dir)
  3173. or do_log(-1, "Can't change protection-2 on dir %s: %s", $dir, $!);
  3174. $dir_chmoded = 1;
  3175. $errn = lstat($fname) ? 0 : 0+$!; # try again
  3176. }
  3177. if ($errn) { do_log(-1, "Can't access file \"%s\": $!", $fname,$!) }
  3178. if (-d _) { push(@rmdirs,$f) } else { push(@rmfiles,$f) }
  3179. $cnt++;
  3180. if ($cnt >= 1000) {
  3181. do_log(3,'rmdir_recursively: doing %d files and %d dirs for now in %s',
  3182. scalar(@rmfiles), scalar(@rmdirs), $dir);
  3183. $more = 1;
  3184. last;
  3185. }
  3186. }
  3187. closedir(DIR) or die "Error closing directory $dir: $!";
  3188. my $cntf = scalar(@rmfiles);
  3189. for my $f (@rmfiles) {
  3190. my $fname = $dir . '/' . untaint($f);
  3191. if (unlink($fname)) {
  3192. # ok
  3193. } elsif ($! == EACCES && !$dir_chmoded) {
  3194. # relax protection on the directory, then try again
  3195. do_log(3,'rmdir_recursively: enabling write access to dir %s',$dir);
  3196. my $what = -l _ ? 'symlink' :-d _ ? 'directory' :'non-regular file';
  3197. chmod(0750,$dir)
  3198. or do_log(-1, "Can't change protection-3 on dir %s: %s", $dir, $!);
  3199. $dir_chmoded = 1;
  3200. unlink($fname) or die "Can't remove $what $fname: $!";
  3201. }
  3202. }
  3203. undef @rmfiles;
  3204. section_time("unlink-$cntf-files") if $cntf > 0;
  3205. for my $d (@rmdirs) {
  3206. rmdir_recursively($dir . '/' . untaint($d));
  3207. }
  3208. undef @rmdirs;
  3209. }
  3210. if (!$exclude_itself) {
  3211. rmdir($dir) or die "rmdir_recursively: Can't remove directory $dir: $!";
  3212. section_time('rmdir');
  3213. }
  3214. 1;
  3215. }
  3216. # efficiently read a file (binmode) into a provided string;
  3217. # either an open file handle may be given, or a filename
  3218. #
  3219. sub read_file($$) {
  3220. my($fname,$strref) = @_;
  3221. my($fh, $file_size, $nbytes);
  3222. if (ref $fname) {
  3223. $fh = $fname; # assume a file handle was given
  3224. } else { # a filename
  3225. $fh = IO::File->new;
  3226. $fh->open($fname,O_RDONLY) or die "Can't open file $fname for reading: $!";
  3227. $fh->binmode or die "Can't set file $fname to binmode: $!";
  3228. }
  3229. my(@stat_list) = stat($fh);
  3230. @stat_list or die "Failed to access file: $!";
  3231. $file_size = -s _ if -f _;
  3232. if ($file_size) {
  3233. # preallocate exact storage size, avoids realloc/copying while growing
  3234. $$strref = ''; vec($$strref, $file_size + 32768, 8) = 0;
  3235. }
  3236. $$strref = '';
  3237. while (($nbytes = sysread($fh, $$strref, 32768, length $$strref)) > 0) { }
  3238. defined $nbytes or die "Error reading from $fname: $!";
  3239. if (!ref $fname) { $fh->close or die "Error closing $fname: $!" }
  3240. $strref;
  3241. }
  3242. # read a text file, returning its contents as a string - suitable for
  3243. # calling from amavisd.conf
  3244. #
  3245. sub read_text($;$) {
  3246. my($fname, $encoding) = @_;
  3247. my $fh = IO::File->new;
  3248. $fh->open($fname,'<') or die "Can't open file $fname for reading: $!";
  3249. if (defined($encoding) && $encoding ne '') {
  3250. binmode($fh, ":encoding($encoding)")
  3251. or die "Can't set :encoding($encoding) on file $fname: $!";
  3252. }
  3253. my $nbytes; my $str = '';
  3254. while (($nbytes = $fh->read($str, 16384, length($str))) > 0) { }
  3255. defined $nbytes or die "Error reading from $fname: $!";
  3256. $fh->close or die "Error closing $fname: $!";
  3257. my $result = $str; undef $str; # shrink allocated storage to actual size
  3258. $result;
  3259. }
  3260. # attempt to read all user-visible replies from a l10n dir
  3261. # This function auto-fills $notify_sender_templ, $notify_virus_sender_templ,
  3262. # $notify_virus_admin_templ, $notify_virus_recips_templ,
  3263. # $notify_spam_sender_templ and $notify_spam_admin_templ from files named
  3264. # template-dsn.txt, template-virus-sender.txt, template-virus-admin.txt,
  3265. # template-virus-recipient.txt, template-spam-sender.txt,
  3266. # template-spam-admin.txt. If this is available, it uses the charset
  3267. # file to do automatic charset conversion. Used by the Debian distribution.
  3268. #
  3269. sub read_l10n_templates($;$) {
  3270. my($dir) = @_;
  3271. if (@_ > 1) # compatibility with Debian
  3272. { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" }
  3273. my $file_chset = Amavis::Util::read_text("$dir/charset");
  3274. local($1,$2);
  3275. if ($file_chset =~ m{^(?:\#[^\n]*\n)*([^./\n\s]+)(\s*[\#\n].*)?$}s) {
  3276. $file_chset = untaint($1);
  3277. } else {
  3278. die "Invalid charset $file_chset\n";
  3279. }
  3280. $Amavis::Conf::notify_sender_templ =
  3281. Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset);
  3282. $Amavis::Conf::notify_virus_sender_templ =
  3283. Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset);
  3284. $Amavis::Conf::notify_virus_admin_templ =
  3285. Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset);
  3286. $Amavis::Conf::notify_virus_recips_templ =
  3287. Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset);
  3288. $Amavis::Conf::notify_spam_sender_templ =
  3289. Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset);
  3290. $Amavis::Conf::notify_spam_admin_templ =
  3291. Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset);
  3292. }
  3293. # # attempt to read a list of config files to use instead of the default one,
  3294. # # using an external helper script. Used by the Debian/Ubuntu distribution.
  3295. # sub find_config_files(@) {
  3296. # my(@dirs) = @_;
  3297. # local $ENV{PATH} = '/bin:/usr/bin';
  3298. # my(@config_files) = map { `run-parts --list "$_"` } @dirs;
  3299. # chomp(@config_files);
  3300. # # untaint - this data is secure as we check the files themselves later
  3301. # map { untaint($_) } @config_files;
  3302. # }
  3303. #use CDB_File;
  3304. #sub tie_hash($$) {
  3305. # my($hashref, $filename) = @_;
  3306. # CDB_File::create(%$hashref, $filename, "$filename.tmp$$")
  3307. # or die "Can't create cdb $filename: $!";
  3308. # my $cdb = tie(%$hashref,'CDB_File',$filename)
  3309. # or die "Tie to $filename failed: $!";
  3310. # $hashref;
  3311. #}
  3312. # read an associative array (=Perl hash) (as used in lookups) from a file;
  3313. # may be called from amavisd.conf
  3314. #
  3315. # Format: one key per line, anything from '#' to the end of line
  3316. # is considered a comment, but '#' within correctly quoted RFC 5321
  3317. # addresses is not treated as a comment introducer (e.g. a hash sign
  3318. # within "strange # \"foo\" address"@example.com is part of the string).
  3319. # Lines may contain a pair: key value, separated by whitespace,
  3320. # or key only, in which case a value 1 is implied. Trailing whitespace
  3321. # is discarded (iff $trim_trailing_space_in_lookup_result_fields),
  3322. # empty lines (containing only whitespace or comment) are ignored.
  3323. # Addresses (lefthand-side) are converted from RFC 5321 -quoted form
  3324. # into internal (raw) form and inserted as keys into a given hash.
  3325. # NOTE: the format is partly compatible with Postfix maps (not aliases):
  3326. # no continuation lines are honoured, Postfix maps do not allow
  3327. # RFC 5321 -quoted addresses containing whitespace, Postfix only allows
  3328. # comments starting at the beginning of a line.
  3329. #
  3330. # The $hashref argument is returned for convenience, so that one can do
  3331. # for example:
  3332. # $per_recip_whitelist_sender_lookup_tables = {
  3333. # '.my1.example.com' => read_hash({},'/var/amavis/my1-example-com.wl'),
  3334. # '.my2.example.com' => read_hash({},'/var/amavis/my2-example-com.wl') }
  3335. # or even simpler:
  3336. # $per_recip_whitelist_sender_lookup_tables = {
  3337. # '.my1.example.com' => read_hash('/var/amavis/my1-example-com.wl'),
  3338. # '.my2.example.com' => read_hash('/var/amavis/my2-example-com.wl') }
  3339. #
  3340. sub read_hash(@) {
  3341. unshift(@_,{}) if !ref $_[0]; # first argument is optional, defaults to {}
  3342. my($hashref, $filename, $keep_case) = @_;
  3343. my $lpcs = c('localpart_is_case_sensitive');
  3344. my $inp = IO::File->new;
  3345. $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
  3346. my $ln;
  3347. for ($! = 0; defined($ln=$inp->getline); $! = 0) {
  3348. chomp($ln);
  3349. # carefully handle comments, '#' within "" does not count as a comment
  3350. my $lhs = ''; my $rhs = ''; my $at_rhs = 0; my $trailing_comment = 0;
  3351. for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
  3352. [^#" \t]+ | [ \t]+ | . )/gsx) {
  3353. if ($t eq '#') { $trailing_comment = 1; last }
  3354. if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 }
  3355. else { ($at_rhs ? $rhs : $lhs) .= $t }
  3356. }
  3357. $rhs =~ s/[ \t]+\z// if $trailing_comment ||
  3358. $trim_trailing_space_in_lookup_result_fields;
  3359. next if $lhs eq '' && $rhs eq '';
  3360. my($source_route,$localpart,$domain) =
  3361. Amavis::rfc2821_2822_Tools::parse_quoted_rfc2821($lhs,1);
  3362. $localpart = lc($localpart) if !$lpcs;
  3363. my $addr = $localpart . lc($domain);
  3364. $hashref->{$addr} = $rhs eq '' ? 1 : $rhs;
  3365. # do_log(5, 'read_hash: address: <%s>: %s', $addr, $hashref->{$addr});
  3366. }
  3367. defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug
  3368. $! == EBADF ? do_log(0,'Error reading from %s: %s', $filename,$!)
  3369. : die "Error reading from $filename: $!";
  3370. $inp->close or die "Error closing $filename: $!";
  3371. $hashref;
  3372. }
  3373. sub read_array(@) {
  3374. unshift(@_,[]) if !ref $_[0]; # first argument is optional, defaults to []
  3375. my($arrref, $filename, $keep_case) = @_;
  3376. my $inp = IO::File->new;
  3377. $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
  3378. my $ln;
  3379. for ($! = 0; defined($ln=$inp->getline); $! = 0) {
  3380. chomp($ln); my $lhs = '';
  3381. # carefully handle comments, '#' within "" does not count as a comment
  3382. for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " |
  3383. [^#" \t]+ | [ \t]+ | . )/gsx) {
  3384. last if $t eq '#';
  3385. $lhs .= $t;
  3386. }
  3387. $lhs =~ s/[ \t]+\z//; # trim trailing whitespace
  3388. push(@$arrref, Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs))
  3389. if $lhs ne '';
  3390. }
  3391. defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug
  3392. $! == EBADF ? do_log(0,'Error reading from %s: %s', $filename,$!)
  3393. : die "Error reading from $filename: $!";
  3394. $inp->close or die "Error closing $filename: $!";
  3395. $arrref;
  3396. }
  3397. # The read_cidr() reads a Postfix style CIDR file, (see cidr_table(5) man
  3398. # page), with postfix-style interpretation of comments and line continuations,
  3399. # returning a ref to an array or a ref to a hash (associative array ref).
  3400. #
  3401. # Empty or whitespace-only lines are ignored, as are lines whose first
  3402. # non-whitespace character is a '#'. A logical line starts with non-whitespace
  3403. # text. A line that starts with whitespace continues a logical line.
  3404. # The general form is: network_address/network_mask result
  3405. # where 'network_address' is an IPv4 address in a dot-quad form, or an IPv6
  3406. # address optionally enclosed in square brackets. The 'network_mask' along
  3407. # with a preceding slash is optional, as is the 'result' argument.
  3408. #
  3409. # If a network mask is omitted, a host address (not a network address)
  3410. # is assumed (i.e. a mask defaults to /32 for an IPv4 address, and
  3411. # to /128 for an IPv6 address).
  3412. #
  3413. # The read_cidr() returns a ref to an array or a ref to an hash (associative
  3414. # array) of network specifications, directly suitable for use as a lookup
  3415. # table in @client_ipaddr_policy and @mynetworks_maps, or for copying the
  3416. # array into @inet_acl or @mynetworks.
  3417. #
  3418. # When returned as an array the 'result' arguments are ignored, just the
  3419. # presence of a network specification matters. A '!' may precede the network
  3420. # specification, which will be interpreted as by lookup_ip_acl() as a negation,
  3421. # i.e. a match on such entry will return a false.
  3422. #
  3423. # When returned as a hash, the network specification is lowercased and used
  3424. # as a key, and the 'result' is stored as a value of a hash entry. A missing
  3425. # 'result' is replaced by 1.
  3426. #
  3427. # See also the lookup_ip_acl() for details on allowed IP address syntax
  3428. # and on the interpretation of array and hash type IP lookup tables.
  3429. #
  3430. sub read_cidr($;$) {
  3431. my($filename, $result) = @_;
  3432. # the $result arg may be a ref to an existing array or hash, in which case
  3433. # data will be added there - either as key/value pairs, or as array elements;
  3434. $result = [] if !defined $result; # missing $results arg yields an array
  3435. my $have_arry = ref $result eq 'ARRAY';
  3436. my $inp = IO::File->new;
  3437. $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
  3438. my($ln, $curr_line);
  3439. for ($! = 0; defined($ln=$inp->getline); $! = 0) {
  3440. next if $ln =~ /^ [ \t]* (?: \# | $ )/xs;
  3441. chomp($ln);
  3442. if ($ln =~ /^[ \t]/) { # a continuation line
  3443. $curr_line = '' if !defined $curr_line; # first line a continuation??
  3444. $curr_line .= $ln;
  3445. } else { # a new logical line starts
  3446. if (defined $curr_line) { # deal with the previous logical line
  3447. my($key,$val) = split(' ',$curr_line,2);
  3448. # $val is always defined, it is an empty string if missing
  3449. if ($have_arry) { push(@$result,$key) }
  3450. else { $result->{lc $key} = $val eq '' ? 1 : $val }
  3451. }
  3452. $curr_line = $ln;
  3453. }
  3454. }
  3455. if (defined $curr_line) { # deal with the last logical line
  3456. my($key,$val) = split(' ',$curr_line,2);
  3457. if ($have_arry) { push(@$result,$key) }
  3458. else { $result->{lc $key} = $val eq '' ? 1 : $val }
  3459. }
  3460. defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug
  3461. $! == EBADF ? do_log(0,'Error reading from %s: %s', $filename,$!)
  3462. : die "Error reading from $filename: $!";
  3463. $inp->close or die "Error closing $filename: $!";
  3464. $result;
  3465. }
  3466. sub dump_hash($) {
  3467. my($hr) = @_;
  3468. do_log(0, 'dump_hash: %s => %s', $_, $hr->{$_}) for (sort keys %$hr);
  3469. }
  3470. sub dump_array($) {
  3471. my($ar) = @_;
  3472. do_log(0, 'dump_array: %s', $_) for @$ar;
  3473. }
  3474. # (deprecated, only still used with Amavis::OS_Fingerprint)
  3475. sub dynamic_destination($$) {
  3476. my($method,$conn) = @_;
  3477. if ($method =~ /^(?:[a-z][a-z0-9.+-]*)?:/si) {
  3478. my(@list); $list[0] = ''; my $j = 0;
  3479. for ($method =~ /\G \[ (?: \\. | [^\]\\] )* \] | " (?: \\. | [^"\\] )* "
  3480. | : | [ \t]+ | [^:"\[ \t]+ | . /gsx) { # real parsing
  3481. if ($_ eq ':') { $list[++$j] = '' } else { $list[$j] .= $_ }
  3482. };
  3483. if ($list[1] =~ m{^/}) {
  3484. # presumably the second field is a Unix socket name, keep unchanged
  3485. } else {
  3486. my $new_method; my($proto,$relayhost,$relayport) = @list;
  3487. if ($relayhost eq '*') {
  3488. my $client_ip; $client_ip = $conn->client_ip if defined $conn;
  3489. $relayhost = "[$client_ip]" if defined $client_ip && $client_ip ne '';
  3490. }
  3491. if ($relayport eq '*') {
  3492. my $socket_port; $socket_port = $conn->socket_port if defined $conn;
  3493. $relayport = $socket_port + 1
  3494. if defined $socket_port && $socket_port ne '';
  3495. }
  3496. if ($relayhost eq '*' || $relayport eq '*') {
  3497. do_log(0,'dynamic destination expected, no client addr/port info: %s',
  3498. $method);
  3499. }
  3500. $list[1] = $relayhost; $list[2] = $relayport;
  3501. $new_method = join(':',@list);
  3502. if ($new_method ne $method) {
  3503. do_log(3, 'dynamic destination: %s -> %s', $method,$new_method);
  3504. $method = $new_method;
  3505. }
  3506. }
  3507. }
  3508. $method;
  3509. }
  3510. # collect unfinished recipients matching a $filter sub and a delivery
  3511. # method regexp; assumes all list elements of a delivery_method list
  3512. # use the same protocol name, hence only the first one is inspected
  3513. #
  3514. sub collect_equal_delivery_recips($$$) {
  3515. my($msginfo, $filter, $deliv_meth_regexp) = @_;
  3516. my(@per_recip_data_subset, $proto_sockname);
  3517. my(@per_recip_data) =
  3518. grep(!$_->recip_done && (!$filter || &$filter($_)) &&
  3519. grep(/$deliv_meth_regexp/,
  3520. (ref $_->delivery_method ? $_->delivery_method->[0]
  3521. : $_->delivery_method)),
  3522. @{$msginfo->per_recip_data});
  3523. if (@per_recip_data) {
  3524. # take the first remaining recipient as a model
  3525. $proto_sockname = $per_recip_data[0]->delivery_method;
  3526. defined $proto_sockname or die "undefined recipient's delivery_method";
  3527. my $proto_sockname_key = !ref $proto_sockname ? $proto_sockname
  3528. : join("\n", @$proto_sockname);
  3529. # collect recipients with the same delivery method as the first one
  3530. $per_recip_data_subset[0] = shift(@per_recip_data); # always equals self
  3531. push(@per_recip_data_subset,
  3532. grep((ref $_->delivery_method ? join("\n", @{$_->delivery_method})
  3533. : $_->delivery_method)
  3534. eq $proto_sockname_key, @per_recip_data) );
  3535. }
  3536. # return a ref to a filtered list of still-to-be-delivered recipient objects
  3537. # and a single string or a ref to a list of delivery methods common to
  3538. # these recipients
  3539. (\@per_recip_data_subset, $proto_sockname);
  3540. }
  3541. 1;
  3542. #
  3543. package Amavis::ProcControl;
  3544. use strict;
  3545. use re 'taint';
  3546. BEGIN {
  3547. require Exporter;
  3548. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  3549. $VERSION = '2.316';
  3550. @ISA = qw(Exporter);
  3551. @EXPORT_OK = qw(&exit_status_str &proc_status_ok &kill_proc &cloexec
  3552. &run_command &run_command_consumer &run_as_subprocess
  3553. &collect_results &collect_results_structured);
  3554. import Amavis::Conf qw(:platform c cr ca);
  3555. import Amavis::Util qw(ll do_log do_log_safe prolong_timer untaint
  3556. flush_captured_log reposition_captured_log_to_end);
  3557. import Amavis::Log qw(open_log close_log log_fd);
  3558. }
  3559. use subs @EXPORT_OK;
  3560. use POSIX qw(:sys_wait_h WIFEXITED WIFSIGNALED WIFSTOPPED WEXITSTATUS
  3561. WTERMSIG WSTOPSIG);
  3562. use Errno qw(ENOENT EACCES EAGAIN ESRCH);
  3563. use IO::File ();
  3564. use Time::HiRes ();
  3565. # use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC); # used in cloexec, if enabled
  3566. # map process termination status number to an informative string, and
  3567. # append optional message (dual-valued errno or a string or a number),
  3568. # returning the resulting string
  3569. #
  3570. sub exit_status_str($;$) {
  3571. my($stat,$errno) = @_; my $str;
  3572. if (!defined($stat)) {
  3573. $str = '(no status)';
  3574. } elsif (WIFEXITED($stat)) {
  3575. $str = sprintf('exit %d', WEXITSTATUS($stat));
  3576. } elsif (WIFSTOPPED($stat)) {
  3577. $str = sprintf('stopped, signal %d', WSTOPSIG($stat));
  3578. } else { # WIFSIGNALED($stat)
  3579. my $sig = WTERMSIG($stat);
  3580. $str = sprintf('%s, signal %d (%04x)',
  3581. $sig == 1 ? 'HANGUP' : $sig == 2 ? 'INTERRUPTED' :
  3582. $sig == 6 ? 'ABORTED' : $sig == 9 ? 'KILLED' :
  3583. $sig == 15 ? 'TERMINATED' : 'DIED',
  3584. $sig, $stat);
  3585. }
  3586. if (defined $errno) { # deal with dual-valued and plain variables
  3587. $str .= ', '.$errno if (0+$errno) != 0 || ($errno ne '' && $errno ne '0');
  3588. }
  3589. $str;
  3590. }
  3591. # check errno to be 0 and a process exit status to be in the list of success
  3592. # status codes, returning true if both are ok, and false otherwise
  3593. #
  3594. sub proc_status_ok($;$@) {
  3595. my($exit_status,$errno,@success) = @_;
  3596. my $ok = 0;
  3597. if ((!defined $errno || $errno == 0) && WIFEXITED($exit_status)) {
  3598. my $j = WEXITSTATUS($exit_status);
  3599. if (!@success) { $ok = $j==0 } # empty list implies only status 0 is good
  3600. elsif (grep($_==$j, @success)) { $ok = 1 }
  3601. }
  3602. $ok;
  3603. }
  3604. # kill a process, typically a spawned external decoder or checker
  3605. #
  3606. sub kill_proc($;$$$$) {
  3607. my($pid,$what,$timeout,$proc_fh,$reason) = @_;
  3608. $pid >= 0 or die "Shouldn't be killing process groups: [$pid]";
  3609. $pid != 1 or die "Shouldn't be killing process 'init': [$pid]";
  3610. $what = defined $what ? " running $what" : '';
  3611. $reason = defined $reason ? " (reason: $reason)" : '';
  3612. #
  3613. # the following order is a must: SIGTERM first, _then_ close a pipe;
  3614. # otherwise the following can happen: closing a pipe first (explicitly or
  3615. # implicitly by undefining $proc_fh) blocks us so we never send SIGTERM
  3616. # until the external process dies of natural death; on the other hand,
  3617. # not closing the pipe after SIGTERM does not necessarily let the process
  3618. # notice SIGTERM, so SIGKILL is always needed to stop it, which is not nice
  3619. #
  3620. my $n = kill(0,$pid); # does the process really exist?
  3621. if ($n == 0 && $! != ESRCH) {
  3622. die sprintf("Can't send SIG 0 to process [%s]%s: %s", $pid,$what,$!);
  3623. } elsif ($n == 0) {
  3624. do_log(2, 'no need to kill process [%s]%s, already gone', $pid,$what);
  3625. } else {
  3626. do_log(-1,'terminating process [%s]%s%s', $pid,$what,$reason);
  3627. kill('TERM',$pid) or $! == ESRCH # be gentle on the first attempt
  3628. or die sprintf("Can't send SIGTERM to process [%s]%s: %s",$pid,$what,$!);
  3629. }
  3630. # close the pipe if still open, ignoring status
  3631. $proc_fh->close if defined $proc_fh;
  3632. my $child_stat = defined $pid && waitpid($pid,WNOHANG) > 0 ? $? : undef;
  3633. $n = kill(0,$pid); # is the process still there?
  3634. if ($n > 0 && defined($timeout) && $timeout > 0) {
  3635. sleep($timeout); $n = kill(0,$pid); # wait a little and recheck
  3636. }
  3637. if ($n == 0 && $! != ESRCH) {
  3638. die sprintf("Can't send SIG 0 to process [%s]%s: %s", $pid,$what,$!);
  3639. } elsif ($n > 0) { # the process is still there, try a stronger signal
  3640. do_log(-1,'process [%s]%s is still alive, using a bigger hammer (SIGKILL)',
  3641. $pid,$what);
  3642. kill('KILL',$pid) or $! == ESRCH
  3643. or die sprintf("Can't send SIGKILL to process [%s]%s: %s",$pid,$what,$!);
  3644. }
  3645. }
  3646. sub cloexec($;$$) { undef }
  3647. # sub cloexec($;$$) { # supposedly not needed for Perl >= 5.6.0
  3648. # my($fh,$newsetting,$name) = @_; my $flags;
  3649. # $flags = fcntl($fh, F_GETFD, 0)
  3650. # or die "Can't get close-on-exec flag for file handle $fh $name: $!";
  3651. # $flags = 0 + $flags; # turn into numeric, avoid: "0 but true"
  3652. # if (defined $newsetting) { # change requested?
  3653. # my $newflags = $newsetting ? ($flags|FD_CLOEXEC) : ($flags&~FD_CLOEXEC);
  3654. # if ($flags != $newflags) {
  3655. # do_log(4,"cloexec: turning %s flag FD_CLOEXEC for file handle %s %s",
  3656. # $newsetting ? "ON" : "OFF", $fh, $name);
  3657. # fcntl($fh, F_SETFD, $newflags)
  3658. # or die "Can't set FD_CLOEXEC for file handle $fh $name: $!";
  3659. # }
  3660. # }
  3661. # ($flags & FD_CLOEXEC) ? 1 : 0; # returns old setting
  3662. # }
  3663. # POSIX::open a file or dup an existing fd (Perl open syntax), with a
  3664. # requirement that it gets opened on a prescribed file descriptor $fd_target.
  3665. # Returns a file descriptor number (not a Perl file handle, there is no
  3666. # associated file handle). Usually called from a forked process prior to exec.
  3667. #
  3668. sub open_on_specific_fd($$$$) {
  3669. my($fd_target,$fname,$flags,$mode) = @_;
  3670. my $fd_got; # fd directly given as argument, or obtained from POSIX::open
  3671. my $logging_safe = 0;
  3672. if (ll(5)) {
  3673. # crude attempt to prevent a forked process from writing log records
  3674. # to its parent process on STDOUT or STDERR
  3675. my $log_fd = log_fd();
  3676. $logging_safe = 1 if !defined($log_fd) || $log_fd > 2;
  3677. }
  3678. local($1);
  3679. if ($fname =~ /^&=?(\d+)\z/) { $fd_got = $1 } # fd directly specified
  3680. my $flags_displayed = $flags == &POSIX::O_RDONLY ? '<'
  3681. : $flags == &POSIX::O_WRONLY ? '>' : '('.$flags.')';
  3682. if (!defined($fd_got) || $fd_got != $fd_target) {
  3683. # close whatever is on a target descriptor but don't shoot self in the foot
  3684. # with Net::Server <= 0.90 fd0 was main::stdin, but no longer is in 0.91
  3685. do_log_safe(5, "open_on_specific_fd: target fd%s closing, to become %s %s",
  3686. $fd_target, $flags_displayed, $fname)
  3687. if $logging_safe && ll(5);
  3688. # it pays off to close explicitly, with some luck open will get a target fd
  3689. POSIX::close($fd_target); # ignore error; we may have just closed a log
  3690. }
  3691. if (!defined($fd_got)) { # a file name was given, not a descriptor
  3692. $fd_got = POSIX::open($fname,$flags,$mode);
  3693. defined $fd_got or die "Can't open $fname ($flags,$mode): $!";
  3694. $fd_got = 0 + $fd_got; # turn into numeric, avoid: "0 but true"
  3695. }
  3696. if ($fd_got != $fd_target) { # dup, ensuring we get a requested descriptor
  3697. # we may have been left without a log file descriptor, must not die
  3698. do_log_safe(5, "open_on_specific_fd: target fd%s dup2 from fd%s %s %s",
  3699. $fd_target, $fd_got, $flags_displayed, $fname)
  3700. if $logging_safe && ll(5);
  3701. # POSIX mandates we got the lowest fd available (but some kernels have
  3702. # bugs), let's be explicit that we require a specified file descriptor
  3703. defined POSIX::dup2($fd_got,$fd_target)
  3704. or die "Can't dup2 from $fd_got to $fd_target: $!";
  3705. if ($fd_got > 2) { # let's get rid of the original fd, unless 0,1,2
  3706. my $err; defined POSIX::close($fd_got) or $err = $!;
  3707. $err = defined $err ? ": $err" : '';
  3708. # we may have been left without a log file descriptor, don't die
  3709. do_log_safe(5, "open_on_specific_fd: source fd%s closed%s",
  3710. $fd_got,$err) if $logging_safe && ll(5);
  3711. }
  3712. }
  3713. $fd_got;
  3714. }
  3715. sub release_parent_resources() {
  3716. $Amavis::sql_dataset_conn_lookups->dbh_inactive(1)
  3717. if $Amavis::sql_dataset_conn_lookups;
  3718. $Amavis::sql_dataset_conn_storage->dbh_inactive(1)
  3719. if $Amavis::sql_dataset_conn_storage;
  3720. $Amavis::zmq_obj->inactivate
  3721. if $Amavis::zmq_obj;
  3722. # undef $Amavis::sql_dataset_conn_lookups;
  3723. # undef $Amavis::sql_dataset_conn_storage;
  3724. # undef $Amavis::snmp_db;
  3725. # undef $Amavis::db_env;
  3726. }
  3727. # Run specified command as a subprocess (like qx operator, but more careful
  3728. # with error reporting and cancels :utf8 mode). If $stderr_to is undef or
  3729. # an empty string it is converted to '&1', merging stderr to stdout on fd1.
  3730. # Return a file handle open for reading from the subprocess.
  3731. #
  3732. sub run_command($$@) {
  3733. my($stdin_from, $stderr_to, $cmd, @args) = @_;
  3734. my $cmd_text = join(' ', $cmd, @args);
  3735. $stdin_from = '/dev/null' if !defined $stdin_from || $stdin_from eq '';
  3736. $stderr_to = '&1' if !defined $stderr_to || $stderr_to eq ''; # to stdout
  3737. my $msg = join(' ', $cmd, @args, "<$stdin_from", "2>$stderr_to");
  3738. # $^F == 2 or do_log(-1,"run_command: SYSTEM_FD_MAX not 2: %d", $^F);
  3739. my $proc_fh = IO::File->new; # parent reading side of the pipe
  3740. my $child_out_fh = IO::File->new; # child writing side of the pipe
  3741. pipe($proc_fh,$child_out_fh)
  3742. or die "run_command: Can't create a pipe: $!";
  3743. flush_captured_log();
  3744. my $pid;
  3745. eval {
  3746. # Avoid using open('-|') which is just too damn smart: possibly waiting
  3747. # indefinitely when resources are tight, and not catching fork errors as
  3748. # expected but just bailing out of eval; make a pipe explicitly and fork.
  3749. # Note that fork(2) returns ENOMEM on lack of swap space, and EAGAIN when
  3750. # process limit is reached; we want it to fail in both cases and not obey
  3751. # the EAGAIN and keep retrying, as perl open() does.
  3752. $pid = fork(); 1;
  3753. } or do {
  3754. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  3755. die "run_command (forking): $eval_stat";
  3756. };
  3757. defined($pid) or die "run_command: can't fork: $!";
  3758. if (!$pid) { # child
  3759. alarm(0); my $interrupt = '';
  3760. my $h1 = sub { $interrupt = $_[0] };
  3761. my $h2 = sub { die "Received signal ".$_[0] };
  3762. @SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)} = ($h1) x 7;
  3763. eval { # die must be caught, otherwise we end up with two running daemons
  3764. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  3765. # use Devel::Symdump ();
  3766. # my $dumpobj = Devel::Symdump->rnew;
  3767. # for my $k ($dumpobj->ios) {
  3768. # no strict 'refs'; my $fn = fileno($k);
  3769. # if (!defined($fn)) { do_log(2, "not open %s", $k) }
  3770. # elsif ($fn == 1 || $fn == 2) { do_log(2, "KEEP %s, fileno=%s",$k,$fn) }
  3771. # else { $! = 0;
  3772. # close(*{$k}{IO}) and do_log(2, "DID CLOSE %s (fileno=%s)", $k,$fn);
  3773. # }
  3774. # }
  3775. eval { release_parent_resources() };
  3776. $proc_fh->close or die "Child can't close parent side of a pipe: $!";
  3777. if ($interrupt ne '') { my $i = $interrupt; $interrupt = ''; die $i }
  3778. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  3779. my $opt_rdonly = untaint(&POSIX::O_RDONLY);
  3780. my $opt_wronly = untaint(&POSIX::O_WRONLY | &POSIX::O_CREAT);
  3781. open_on_specific_fd(0, $stdin_from, $opt_rdonly, 0);
  3782. open_on_specific_fd(1, '&='.fileno($child_out_fh), $opt_wronly, 0);
  3783. open_on_specific_fd(2, $stderr_to, $opt_wronly, 0);
  3784. # eval { close_log() }; # may have been closed by open_on_specific_fd
  3785. # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
  3786. exec {$cmd} ($cmd,@args);
  3787. die "run_command: failed to exec $cmd_text: $!";
  3788. } or 1; # ignore failures, make perlcritic happy
  3789. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  3790. eval {
  3791. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  3792. if ($interrupt ne '') { my $i = $interrupt; $interrupt = ''; die $i }
  3793. open_log(); # oops, exec failed, we will need logging after all...
  3794. # we're in trouble if stderr was attached to a terminal, but no longer is
  3795. do_log_safe(-1,"run_command: child process [%s]: %s", $$,$err);
  3796. } or 1; # ignore failures, make perlcritic happy
  3797. { # no warnings;
  3798. POSIX::_exit(6); # avoid END and destructor processing
  3799. kill('KILL',$$); exit 1; # still kicking? die!
  3800. }
  3801. }
  3802. # parent
  3803. ll(5) && do_log(5,"run_command: [%s] %s", $pid,$msg);
  3804. $child_out_fh->close
  3805. or die "Parent failed to close child side of the pipe: $!";
  3806. binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1
  3807. ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID
  3808. }
  3809. # Run a specified command as a subprocess. Return a file handle open for
  3810. # WRITING to the subprocess, utf8 mode canceled and autoflush turned OFF !
  3811. # If $stderr_to is undef or is an empty string it is converted to '&1',
  3812. # merging stderr to stdout on fd1.
  3813. #
  3814. sub run_command_consumer($$@) {
  3815. my($stdout_to, $stderr_to, $cmd, @args) = @_;
  3816. my $cmd_text = join(' ', $cmd, @args);
  3817. $stdout_to = '/dev/null' if !defined $stdout_to || $stdout_to eq '';
  3818. $stderr_to = '&1' if !defined $stderr_to || $stderr_to eq ''; # to stdout
  3819. my $msg = join(' ', $cmd, @args, ">$stdout_to", "2>$stderr_to");
  3820. # $^F == 2 or do_log(-1,"run_command_consumer: SYSTEM_FD_MAX not 2: %d", $^F);
  3821. my $proc_fh = IO::File->new; # parent writing side of the pipe
  3822. my $child_in_fh = IO::File->new; # child reading side of the pipe
  3823. pipe($child_in_fh,$proc_fh)
  3824. or die "run_command_consumer: Can't create a pipe: $!";
  3825. flush_captured_log();
  3826. my $pid;
  3827. eval {
  3828. # Avoid using open('|-') which is just too damn smart: possibly waiting
  3829. # indefinitely when resources are tight, and not catching fork errors as
  3830. # expected but just bailing out of eval; make a pipe explicitly and fork.
  3831. # Note that fork(2) returns ENOMEM on lack of swap space, and EAGAIN when
  3832. # process limit is reached; we want it to fail in both cases and not obey
  3833. # the EAGAIN and keep retrying, as perl open() does.
  3834. $pid = fork(); 1;
  3835. } or do {
  3836. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  3837. die "run_command_consumer (fork): $eval_stat";
  3838. };
  3839. defined($pid) or die "run_command_consumer: can't fork: $!";
  3840. if (!$pid) { # child
  3841. alarm(0); my $interrupt = '';
  3842. my $h1 = sub { $interrupt = $_[0] };
  3843. my $h2 = sub { die "Received signal ".$_[0] };
  3844. @SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)} = ($h1) x 7;
  3845. eval { # die must be caught, otherwise we end up with two running daemons
  3846. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  3847. eval { release_parent_resources() };
  3848. $proc_fh->close or die "Child can't close parent side of a pipe: $!";
  3849. if ($interrupt ne '') { my $i = $interrupt; $interrupt = ''; die $i }
  3850. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  3851. my $opt_rdonly = untaint(&POSIX::O_RDONLY);
  3852. my $opt_wronly = untaint(&POSIX::O_WRONLY | &POSIX::O_CREAT);
  3853. open_on_specific_fd(0, '&='.fileno($child_in_fh), $opt_rdonly, 0);
  3854. open_on_specific_fd(1, $stdout_to, $opt_wronly, 0);
  3855. open_on_specific_fd(2, $stderr_to, $opt_wronly, 0);
  3856. # eval { close_log() }; # may have been closed by open_on_specific_fd
  3857. # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
  3858. exec {$cmd} ($cmd,@args);
  3859. die "run_command_consumer: failed to exec $cmd_text: $!";
  3860. } or 1; # ignore failures, make perlcritic happy
  3861. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  3862. eval {
  3863. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  3864. if ($interrupt ne '') { my $i = $interrupt; $interrupt = ''; die $i }
  3865. open_log(); # oops, exec failed, we will need logging after all...
  3866. # we're in trouble if stderr was attached to a terminal, but no longer is
  3867. do_log_safe(-1,"run_command_consumer: child process [%s]: %s", $$,$err);
  3868. } or 1; # ignore failures, make perlcritic happy
  3869. { # no warnings;
  3870. POSIX::_exit(6); # avoid END and destructor processing
  3871. kill('KILL',$$); exit 1; # still kicking? die!
  3872. }
  3873. }
  3874. # parent
  3875. ll(5) && do_log(5,"run_command_consumer: [%s] %s", $pid,$msg);
  3876. $child_in_fh->close
  3877. or die "Parent failed to close child side of the pipe: $!";
  3878. binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1
  3879. $proc_fh->autoflush(0); # turn it off here, must call ->flush when needed
  3880. ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID
  3881. }
  3882. # run a specified subroutine with given arguments as a (forked) subprocess,
  3883. # collecting results (if any) over a pipe from a subprocess and propagating
  3884. # them back to a caller; (useful to prevent a potential process crash from
  3885. # bringing down the main process, and allows cleaner timeout aborts)
  3886. #
  3887. sub run_as_subprocess($@) {
  3888. my($code,@args) = @_;
  3889. alarm(0); # stop the timer
  3890. my $proc_fh = IO::File->new; # parent reading side of the pipe
  3891. my $child_out_fh = IO::File->new; # child writing side of the pipe
  3892. pipe($proc_fh,$child_out_fh)
  3893. or die "run_as_subprocess: Can't create a pipe: $!";
  3894. flush_captured_log();
  3895. my $pid;
  3896. eval {
  3897. # Avoid using open('-|') which is just too damn smart: possibly waiting
  3898. # indefinitely when resources are tight, and not catching fork errors as
  3899. # expected but just bailing out of eval; make a pipe explicitly and fork.
  3900. # Note that fork(2) returns ENOMEM on lack of swap space, and EAGAIN when
  3901. # process limit is reached; we want it to fail in both cases and not obey
  3902. # the EAGAIN and keep retrying, as perl open() does.
  3903. $pid = fork(); 1;
  3904. } or do {
  3905. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  3906. die "run_as_subprocess (forking): $eval_stat";
  3907. };
  3908. defined($pid) or die "run_as_subprocess: can't fork: $!";
  3909. if (!$pid) { # child
  3910. # timeouts will be also be handled by a parent process
  3911. my $t0 = Time::HiRes::time; my(@result); my $interrupt = '';
  3912. my $h1 = sub { $interrupt = $_[0] };
  3913. my $h2 = sub { die "Received signal ".$_[0] };
  3914. @SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)} = ($h1) x 7;
  3915. $SIG{PIPE} = 'IGNORE'; # don't signal on a write to a widowed pipe
  3916. my $myownpid = $$; # fetching $$ is a syscall
  3917. $0 = 'sub-' . c('myprogram_name'); # let it show in ps(1)
  3918. my $eval_stat;
  3919. eval { # die must be caught, otherwise we end up with two running daemons
  3920. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  3921. eval { release_parent_resources() };
  3922. $proc_fh->close or die "Child can't close parent side of a pipe: $!";
  3923. if ($interrupt ne '') { my $i = $interrupt; $interrupt = ''; die $i }
  3924. prolong_timer("child[$myownpid]"); # restart the timer
  3925. binmode($child_out_fh) or die "Can't set pipe to binmode: $!";
  3926. # we don't really need STDOUT here, but just in case the supplied code
  3927. # happens to write there, let's make STDOUT a dup of a pipe
  3928. close STDOUT; # ignoring status
  3929. # prefer dup(2) here instead of fdopen, with some luck this gives us fd1
  3930. open(STDOUT, '>&'.fileno($child_out_fh))
  3931. or die "Child can't dup pipe to STDOUT: $!";
  3932. binmode(STDOUT) or die "Can't set STDOUT to binmode: $!";
  3933. #*** should re-establish ZMQ sockets here without clobbering parent
  3934. ll(5) && do_log(5,'[%s] run_as_subprocess: running as child, '.
  3935. 'stdin=%s, stdout=%s, pipe=%s', $myownpid,
  3936. fileno(STDIN), fileno(STDOUT), fileno($child_out_fh));
  3937. @result = &$code(@args); # invoke a caller-specified subroutine
  3938. 1;
  3939. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  3940. my $dt = Time::HiRes::time - $t0;
  3941. eval { # must not use die in forked process, or we end up with two daemons
  3942. local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7;
  3943. if ($interrupt ne '') { my $i = $interrupt; $interrupt = ''; die $i }
  3944. my $status; my $ll = 3;
  3945. if (defined $eval_stat) { # failure
  3946. chomp $eval_stat; $ll = -2;
  3947. $status = sprintf("STATUS: FAILURE %s", $eval_stat);
  3948. } else { # success
  3949. $status = sprintf("STATUS: SUCCESS, %d results", scalar(@result));
  3950. };
  3951. my $frozen = Amavis::Util::freeze([$status,@result]);
  3952. ll($ll) && do_log($ll, '[%s] run_as_subprocess: child done (%.1f ms), '.
  3953. 'sending results: res_len=%d, %s',
  3954. $myownpid, $dt*1000, length($frozen), $status);
  3955. # write results back to a parent process over a pipe as a frozen struct.
  3956. # writing to broken pipe must return an error, not throw a signal
  3957. local $SIG{PIPE} = sub { die "Broken pipe\n" }; # locale-independent err
  3958. $child_out_fh->print($frozen) or die "Can't write result to pipe: $!";
  3959. $child_out_fh->close or die "Child can't close its side of a pipe: $!";
  3960. flush_captured_log();
  3961. close STDOUT or die "Child can't close its STDOUT: $!";
  3962. POSIX::_exit(0); # normal completion, avoid END and destructor processing
  3963. } or 1; # ignore failures, make perlcritic happy
  3964. my $eval2_stat = $@ ne '' ? $@ : "errno=$!";
  3965. eval {
  3966. chomp $eval2_stat;
  3967. if ($interrupt ne '') { my $i = $interrupt; $interrupt = ''; die $i }
  3968. # broken pipe is common when parent process is shutting down
  3969. my $ll = $eval2_stat =~ /^Broken pipe\b/ ? 1 : -1;
  3970. do_log_safe($ll, 'run_as_subprocess: child process [%s]: %s',
  3971. $myownpid, $eval2_stat);
  3972. } or 1; # ignore failures, make perlcritic happy
  3973. POSIX::_exit(6); # avoid END and destructor processing in a subprocess
  3974. }
  3975. # parent
  3976. ll(5) && do_log(5,"run_as_subprocess: spawned a subprocess [%s]", $pid);
  3977. $child_out_fh->close
  3978. or die "Parent failed to close child side of the pipe: $!";
  3979. binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1
  3980. prolong_timer('run_as_subprocess'); # restart the timer
  3981. ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID
  3982. }
  3983. # read results from a subprocess over a pipe, returns a ref to a results string
  3984. # and a subprocess exit status; close the pipe and dismiss the subprocess,
  3985. # by force if necessary; if $success_list_ref is defined, check also the
  3986. # subprocess exit status against the provided list and log results
  3987. #
  3988. sub collect_results($$;$$$) {
  3989. my($proc_fh,$pid, $what,$results_max_size,$success_list_ref) = @_;
  3990. # $results_max_size is interpreted as follows:
  3991. # undef .. no limit, read and return all data;
  3992. # 0 ... no limit, read and discard all data, returns ref to empty string
  3993. # >= 1 ... read all data, but truncate results string at limit
  3994. my $child_stat; my $close_err = 0; my $pid_orig = $pid;
  3995. my $result = ''; my $result_l = 0; my $skipping = 0; my $eval_stat;
  3996. eval { # read results; could be aborted by a read error or a timeout
  3997. my($nbytes,$buff);
  3998. while (($nbytes=$proc_fh->read($buff,16384)) > 0) {
  3999. if (!defined($results_max_size)) { $result .= $buff } # keep all data
  4000. elsif ($results_max_size == 0 || $skipping) {} # discard data
  4001. elsif ($result_l < $results_max_size) { $result .= $buff }
  4002. else {
  4003. $skipping = 1; # sanity limit exceeded
  4004. do_log(-1,'collect_results from [%s] (%s): results size limit '.
  4005. '(%d bytes) exceeded', $pid_orig,$what,$results_max_size);
  4006. }
  4007. $result_l += $nbytes;
  4008. }
  4009. defined $nbytes or die "Error reading from a subprocess [$pid_orig]: $!";
  4010. ll(5) && do_log(5,'collect_results from [%s] (%s), %d bytes, (limit %s)',
  4011. $pid_orig,$what,$result_l,$results_max_size);
  4012. 1;
  4013. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  4014. if (defined($results_max_size) && $results_max_size > 0 &&
  4015. length($result) > $results_max_size) {
  4016. $result = substr($result,0,$results_max_size) . "...";
  4017. }
  4018. if (defined $eval_stat) { # read error or timeout; abort the subprocess
  4019. chomp $eval_stat;
  4020. undef $_[0]; # release the caller's copy of $proc_fh
  4021. kill_proc($pid,$what,1,$proc_fh, "on reading: $eval_stat") if defined $pid;
  4022. undef $proc_fh; undef $pid;
  4023. die "collect_results - reading aborted: $eval_stat";
  4024. }
  4025. # normal subprocess exit, close pipe, collect exit status
  4026. $eval_stat = undef;
  4027. eval {
  4028. $proc_fh->close or $close_err = $!;
  4029. $child_stat = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  4030. undef $proc_fh; undef $pid;
  4031. undef $_[0]; # release also the caller's copy of $proc_fh
  4032. 1;
  4033. } or do { # just in case a close itself timed out
  4034. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  4035. undef $_[0]; # release the caller's copy of $proc_fh
  4036. kill_proc($pid,$what,1,$proc_fh, "on closing: $eval_stat") if defined $pid;
  4037. undef $proc_fh; undef $pid;
  4038. die "collect_results - closing aborted: $eval_stat";
  4039. };
  4040. reposition_captured_log_to_end();
  4041. if (defined $success_list_ref) {
  4042. proc_status_ok($child_stat,$close_err, @$success_list_ref)
  4043. or do_log(-2, 'collect_results from [%s] (%s): %s %s', $pid_orig, $what,
  4044. exit_status_str($child_stat,$close_err), $result);
  4045. } elsif ($close_err != 0) {
  4046. die "Can't close pipe to subprocess [$pid_orig]: $close_err";
  4047. }
  4048. (\$result,$child_stat);
  4049. }
  4050. # read results from a subprocess over a pipe as a frozen data structure;
  4051. # close the pipe and dismiss the subprocess; returns results as a ref to a list
  4052. #
  4053. sub collect_results_structured($$;$$) {
  4054. my($proc_fh,$pid, $what,$results_max_size) = @_;
  4055. my($result_ref,$child_stat) =
  4056. collect_results($proc_fh,$pid, $what,$results_max_size,[0]);
  4057. my(@result);
  4058. $result_ref = Amavis::Util::thaw($$result_ref);
  4059. @result = @$result_ref if $result_ref;
  4060. @result
  4061. or die "collect_results_structured: no results from subprocess [$pid]";
  4062. my $status = shift(@result);
  4063. $status =~ /^STATUS: (?:SUCCESS|FAILURE)\b/
  4064. or die "collect_results_structured: subprocess [$pid] returned: $status";
  4065. (\@result,$child_stat);
  4066. }
  4067. 1;
  4068. #
  4069. package Amavis::rfc2821_2822_Tools;
  4070. use strict;
  4071. use re 'taint';
  4072. BEGIN {
  4073. require Exporter;
  4074. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  4075. $VERSION = '2.316';
  4076. @ISA = qw(Exporter);
  4077. @EXPORT = qw(
  4078. &rfc2822_timestamp &iso8601_timestamp &iso8601_utc_timestamp
  4079. &iso8601_week &iso8601_yearweek &iso8601_year_and_week &iso8601_weekday
  4080. &format_time_interval &make_received_header_field &parse_received
  4081. &fish_out_ip_from_received &parse_message_id
  4082. &split_address &split_localpart &replace_addr_fields &make_query_keys
  4083. &quote_rfc2821_local &qquote_rfc2821_local
  4084. &parse_quoted_rfc2821 &unquote_rfc2821_local &parse_address_list
  4085. &wrap_string &wrap_smtp_resp &one_response_for_all
  4086. &EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM);
  4087. import Amavis::Conf qw(:platform c cr ca $myproduct_name);
  4088. import Amavis::Util qw(ll do_log unique_ref unique_list);
  4089. }
  4090. use subs @EXPORT;
  4091. use POSIX qw(locale_h strftime);
  4092. BEGIN {
  4093. # try to use the installed version
  4094. eval { require 'sysexits.ph' } or 1; # ignore failure, make perlcritic happy
  4095. # define the most important constants if undefined
  4096. do { sub EX_OK() {0} } unless defined(&EX_OK);
  4097. do { sub EX_NOUSER() {67} } unless defined(&EX_NOUSER);
  4098. do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE);
  4099. do { sub EX_TEMPFAIL() {75} } unless defined(&EX_TEMPFAIL);
  4100. do { sub EX_NOPERM() {77} } unless defined(&EX_NOPERM);
  4101. }
  4102. # Given a Unix time, return the local time zone offset at that time
  4103. # as a string +HHMM or -HHMM, appropriate for the RFC 2822 date format.
  4104. # Works also for non-full-hour zone offsets, and on systems where strftime
  4105. # cannot return TZ offset as a number; (c) Mark Martinec, GPL
  4106. #
  4107. sub get_zone_offset($) {
  4108. my $t = int(shift);
  4109. my $d = 0; # local zone offset in seconds
  4110. for (1..3) { # match the date (with a safety loop limit just in case)
  4111. my $r = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp
  4112. sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]);
  4113. if ($r == 0) { last } else { $d += $r * 24 * 3600 }
  4114. }
  4115. my($sl,$su) = (0,0);
  4116. for ((localtime($t))[2,1,0]) { $sl = $sl * 60 + $_ }
  4117. for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ }
  4118. $d += $sl - $su; # add HMS difference (in seconds)
  4119. my $sign = $d >= 0 ? '+' : '-';
  4120. $d = -$d if $d < 0;
  4121. $d = int(($d + 30) / 60.0); # give minutes, rounded
  4122. sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60);
  4123. }
  4124. # Given a Unix time, provide date-time timestamp as specified in RFC 5322
  4125. # (local time), to be used in header fields such as 'Date:' and 'Received:'
  4126. # See also RFC 3339.
  4127. #
  4128. sub rfc2822_timestamp($) {
  4129. my($t) = @_;
  4130. my(@lt) = localtime(int($t));
  4131. # can't use %z because some systems do not support it (is treated as %Z)
  4132. # my $old_locale = POSIX::setlocale(LC_TIME,'C'); # English dates required!
  4133. my $zone_name = strftime("%Z",@lt);
  4134. my $s = strftime("%a, %e %b %Y %H:%M:%S ", @lt);
  4135. $s .= get_zone_offset($t);
  4136. $s .= " (" . $zone_name . ")" if $zone_name !~ /^\s*\z/;
  4137. # POSIX::setlocale(LC_TIME, $old_locale); # restore the locale
  4138. $s;
  4139. }
  4140. # Given a Unix numeric time (seconds since 1970-01-01T00:00Z),
  4141. # provide date-time timestamp (local time) as specified in ISO 8601 (EN 28601)
  4142. #
  4143. sub iso8601_timestamp($;$$$) {
  4144. my($t,$suppress_zone,$dtseparator,$with_field_separators) = @_;
  4145. # can't use %z because some systems do not support it (is treated as %Z)
  4146. my $fmt = $with_field_separators ? "%Y-%m-%dT%H:%M:%S" : "%Y%m%dT%H%M%S";
  4147. $fmt =~ s/T/$dtseparator/ if defined $dtseparator;
  4148. my $s = strftime($fmt,localtime(int($t)));
  4149. $s .= get_zone_offset($t) unless $suppress_zone;
  4150. $s;
  4151. }
  4152. # Given a Unix numeric time (seconds since 1970-01-01T00:00Z),
  4153. # provide date-time timestamp (UTC) as specified in ISO 8601 (EN 28601)
  4154. #
  4155. sub iso8601_utc_timestamp($;$$$) {
  4156. my($t,$suppress_zone,$dtseparator,$with_field_separators) = @_;
  4157. my $fmt = $with_field_separators ? "%Y-%m-%dT%H:%M:%S" : "%Y%m%dT%H%M%S";
  4158. $fmt =~ s/T/$dtseparator/ if defined $dtseparator;
  4159. my $s = strftime($fmt,gmtime(int($t)));
  4160. $s .= 'Z' unless $suppress_zone;
  4161. $s;
  4162. }
  4163. # Does the given year have 53 weeks? Using a formula by Simon Cassidy.
  4164. #
  4165. sub iso8601_year_is_long($) {
  4166. my($y) = @_;
  4167. my $p = $y + int($y/4) - int($y/100) + int($y/400);
  4168. if (($p % 7) == 4) { return 1 }
  4169. $y--; $p = $y + int($y/4) - int($y/100) + int($y/400);
  4170. if (($p % 7) == 3) { return 1 } else { return 0 }
  4171. }
  4172. # Given a Unix numeric time (seconds since 1970-01-01T00:00Z),
  4173. # provide a week number 1..53 (local time) as specified in ISO 8601 (EN 28601)
  4174. # ( equivalent to PostgreSQL extract(week from ...), and MySQL week(date,3) )
  4175. #
  4176. sub iso8601_year_and_week($) {
  4177. my($unix_time) = @_;
  4178. my($y,$dowm0,$doy0) = (localtime($unix_time))[5,6,7];
  4179. $y += 1900; $dowm0--; $dowm0=6 if $dowm0<0; # normalize, Monday==0
  4180. my $dow0101 = ($dowm0 - $doy0 + 53*7) % 7; # dow Jan 1
  4181. my $wn = int(($doy0 + $dow0101) / 7);
  4182. if ($dow0101 < 4) { $wn++ }
  4183. if ($wn == 0) { $y--; $wn = iso8601_year_is_long($y) ? 53 : 52 }
  4184. elsif ($wn == 53 && !iso8601_year_is_long($y)) { $y++; $wn = 1 }
  4185. ($y,$wn);
  4186. }
  4187. sub iso8601_week($) { # 1..53
  4188. my($y,$wn) = iso8601_year_and_week(shift); $wn;
  4189. }
  4190. sub iso8601_yearweek($) {
  4191. my($y,$wn) = iso8601_year_and_week(shift); $y*100+$wn;
  4192. }
  4193. # Given a Unix numeric time (seconds since 1970-01-01T00:00Z), provide a
  4194. # weekday number (based on local time): a number from 1 through 7, beginning
  4195. # with Monday and ending with Sunday, as specified in ISO 8601 (EN 28601)
  4196. #
  4197. sub iso8601_weekday($) { # 1..7, Mo=1
  4198. my($unix_time) = @_; ((localtime($unix_time))[6] + 6) % 7 + 1;
  4199. }
  4200. sub format_time_interval($) {
  4201. my($t) = @_;
  4202. return 'undefined' if !defined $t;
  4203. my $sign = ''; if ($t < 0) { $sign = '-'; $t = - $t };
  4204. my $dd = int($t / (24*3600)); $t = $t - $dd*(24*3600);
  4205. my $hh = int($t / 3600); $t = $t - $hh*3600;
  4206. my $mm = int($t / 60); $t = $t - $mm*60;
  4207. sprintf("%s%d %d:%02d:%02d", $sign,$dd,$hh,$mm,int($t+0.5));
  4208. }
  4209. sub make_received_header_field($$) {
  4210. my($msginfo, $folded) = @_;
  4211. my $conn = $msginfo->conn_obj;
  4212. my $id = $msginfo->mail_id;
  4213. my($smtp_proto, $recips) = ($conn->appl_proto, $msginfo->recips);
  4214. my($client_ip, $socket_ip) = ($conn->client_ip, $conn->socket_ip);
  4215. for ($client_ip, $socket_ip) {
  4216. $_ = '' if !defined($_);
  4217. # RFC 5321 (ex RFC 2821), section 4.1.3
  4218. $_ = 'IPv6:'.$_ if /:.*:/ && !/^IPv6:/is;
  4219. }
  4220. my $tls = $msginfo->tls_cipher;
  4221. my $s = sprintf("from %s%s%s\n by %s%s (%s, %s)",
  4222. $conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo,
  4223. $client_ip eq '' ? '' : " ([$client_ip])",
  4224. !defined $tls ? '' : " (using TLS with cipher $tls)",
  4225. c('localhost_name'),
  4226. $socket_ip eq '' ? '' : sprintf(" (%s [%s])", c('myhostname'), $socket_ip),
  4227. $myproduct_name,
  4228. $conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port);
  4229. $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; #RFC 3848
  4230. $s .= "\n id $id" if defined $id && $id ne '';
  4231. # do not disclose recipients if more than one
  4232. $s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1;
  4233. $s .= ";\n " . rfc2822_timestamp($msginfo->rx_time);
  4234. $s =~ s/\n//g if !$folded;
  4235. $s;
  4236. }
  4237. # parse Received header field according to RFC 5321, somewhat loosened syntax
  4238. # Stamp = From-domain By-domain [Via] [With] [ID] [For] datetime
  4239. # From-domain = "FROM" FWS Extended-Domain CFWS
  4240. # By-domain = "BY" FWS Extended-Domain CFWS
  4241. # Via = "VIA" FWS ("TCP" / Atom) CFWS
  4242. # With = "WITH" FWS ("ESMTP" / "SMTP" / Atom) CFWS
  4243. # ID = "ID" FWS (Atom / DQUOTE *qcontent DQUOTE / msg-id) CFWS
  4244. # For = "FOR" FWS 1*( Path / Mailbox ) CFWS
  4245. # Path = "<" [ A-d-l ":" ] Mailbox ">"
  4246. # datetime = ";" FWS [ day-of-week "," ] date FWS time [CFWS]
  4247. # Extended-Domain =
  4248. # (Domain / Address-literal) [ FWS "(" [ Domain FWS ] Address-literal ")" ]
  4249. # Avoid regexps like ( \\. | [^"\\] )* which cause recursion trouble / crashes!
  4250. #
  4251. sub parse_received($) {
  4252. local($_) = $_[0]; my(%fld);
  4253. local($1); tr/\n//d; # unfold, chomp
  4254. my $comm_lvl = 0; my $in_option = '';
  4255. my $in_ext_dom = 0; my $in_tcp_info = 0;
  4256. my $in_qcontent = 0; my $in_literal = 0; my $in_angle = 0;
  4257. my $str_l = length($_); my $new_pos;
  4258. for (my $pos=-1; $new_pos=pos($_), $new_pos<$str_l; $pos=$new_pos) {
  4259. $new_pos > $pos or die "parse_received PANIC1 $new_pos"; # just in case
  4260. # comment (may be nested: RFC 5322 section 3.2.2)
  4261. if ($comm_lvl > 0 && /\G( \) )/gcsx) {
  4262. if ($comm_lvl > 1 || $in_tcp_info) { $fld{$in_option} .= $1 } # nested
  4263. if ($comm_lvl == 1 && !$in_tcp_info) { $in_option =~ s/-com\z// }
  4264. $comm_lvl--; next; # pop up one level of comments
  4265. }
  4266. if ($in_tcp_info && /\G( \) )/gcsx) # leaving TCP-info
  4267. { $in_option =~ s/-tcp\z//; $in_tcp_info = 0; $in_ext_dom = 4; next }
  4268. if (!$in_qcontent && !$in_literal && !$comm_lvl &&
  4269. !$in_tcp_info && $in_ext_dom==1 && /\G( \( )/gcsx) {
  4270. # entering TCP-info part, only once after 'from' or 'by'
  4271. $in_option .= '-tcp'; $in_tcp_info = 1; $in_ext_dom = 2; next;
  4272. }
  4273. if (!$in_qcontent && !$in_literal && /\G( \( )/gcsx) {
  4274. $comm_lvl++; # push one level of comments
  4275. if ($comm_lvl > 1 || $in_tcp_info) { $fld{$in_option} .= $1 } # nested
  4276. if ($comm_lvl == 1 && !$in_tcp_info) { # comment starts here
  4277. $in_option .= '-com';
  4278. $fld{$in_option} .= ' ' if defined $fld{$in_option}; # looks better
  4279. }
  4280. next;
  4281. }
  4282. if ($comm_lvl > 0 && /\G( \\. )/gcsx) { $fld{$in_option} .= $1; next }
  4283. if ($comm_lvl > 0 && /\G( [^()\\]+ )/gcsx) { $fld{$in_option} .= $1; next }
  4284. # quoted content
  4285. if ($in_qcontent && /\G( " )/gcsx) # normal exit from qcontent
  4286. { $in_qcontent = 0; $fld{$in_option} .= $1; next }
  4287. if ($in_qcontent && /\G( > )/gcsx) # bail out of qcontent
  4288. { $in_qcontent = 0; $in_angle = 0; $fld{$in_option} .= $1; next }
  4289. if ($in_qcontent && /\G( \\. )/gcsx) { $fld{$in_option} .= $1; next }
  4290. if ($in_qcontent && /\G( [^"\\>]+ )/gcsx) { $fld{$in_option} .= $1; next }
  4291. # address literal
  4292. if ($in_literal && /\G( \] )/gcsx)
  4293. { $in_literal = 0; $fld{$in_option} .= $1; next }
  4294. if ($in_literal && /\G( > )/gcsx) # bail out of address literal
  4295. { $in_literal = 0; $in_angle = 0; $fld{$in_option} .= $1; next }
  4296. if (!$comm_lvl && !$in_qcontent && /\G( \[ )/gcsx)
  4297. { $in_literal = 1; $fld{$in_option} .= $1; next }
  4298. if ($in_literal && /\G( \\. )/gcsx) { $fld{$in_option} .= $1; next }
  4299. if ($in_literal && /\G( [^\]\\>]+ )/gcsx) { $fld{$in_option} .= $1; next }
  4300. if (!$comm_lvl && !$in_qcontent && !$in_literal && !$in_tcp_info) { # top
  4301. if (!$in_angle && /\G( < )/gcsx)
  4302. { $in_angle = 1; $fld{$in_option} .= $1; next }
  4303. if ( $in_angle && /\G( > )/gcsx)
  4304. { $in_angle = 0; $fld{$in_option} .= $1; next }
  4305. if (!$in_angle && /\G (from|by) (?:[ \t]+|\z|(?=[\[\(",;<]))/gcsxi)
  4306. { $in_option = lc($1); $in_ext_dom = 1; next }
  4307. if (!$in_angle && /\G(via|with|id|for)(?:[ \t]+|\z|(?=[\[\(",;<]))/gcsxi)
  4308. { $in_option = lc($1); $in_ext_dom = 0; next }
  4309. if (!$in_angle && /\G( ; )/gcsxi)
  4310. { $in_option = lc($1); $in_ext_dom = 0; next }
  4311. if (/\G( [ \t]+ )/gcsx) { $fld{$in_option} .= $1; next }
  4312. if (/\G( [^ \t,:;\@<>()"\[\]\\]+ )/gcsx) { $fld{$in_option} .= $1; next }
  4313. }
  4314. if (/\G( . )/gcsx) { $fld{$in_option} .= $1; next } # other junk
  4315. die "parse_received PANIC2 $new_pos"; # just in case
  4316. }
  4317. for my $f ('from-tcp','by-tcp') {
  4318. # a tricky part is handling the syntax:
  4319. # (Domain/Addr-literal) [ FWS "(" [ Domain FWS ] Addr-literal ")" ] CFWS
  4320. # where absence of Address-literal in TCP-info means that what looked
  4321. # like a domain in the optional TCP-info, is actually a comment in CFWS
  4322. local($_) = $fld{$f};
  4323. if (!defined($_)) {}
  4324. elsif (/\[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) {}
  4325. elsif (/\[ [^:\]]* : [^\]]* \]/x && # triage, must contain a colon
  4326. /\[ (?: IPv6: )? [0-9a-f]{0,4}
  4327. (?: : [0-9a-f]{0,4} | \. [0-9]{1,3} ){2,9} \]/xi) {}
  4328. # elsif (/ (?: ^ | \D ) ( \d{1,3} (?: \. \d{1,3}){3}) (?! [0-9.] ) /x) {}
  4329. elsif (/^(?: localhost | ( [a-z0-9_\/+-]{1,63} \. )+ [a-z-]{2,} )\b/xi) {}
  4330. else {
  4331. my $fc = $f; $fc =~ s/-tcp\z/-com/;
  4332. $fld{$fc} = '' if !defined $fld{$fc};
  4333. $fld{$fc} = $_ . (/[ \t]\z/||$fld{$fc}=~/^[ \t]/?'':' ') .$fld{$fc};
  4334. delete $fld{$f};
  4335. }
  4336. }
  4337. for (values %fld) { s/[ \t]+\z//; s/^[ \t]+// }
  4338. # for my $f (sort {$fld{$a} cmp $fld{$b}} keys %fld)
  4339. # { do_log(5, "RECVD: %-8s -> /%s/", $f,$fld{$f}) }
  4340. \%fld;
  4341. }
  4342. sub fish_out_ip_from_received($) {
  4343. my($received) = @_;
  4344. my $fields_ref = parse_received($received);
  4345. my $ip; local($1);
  4346. for (@$fields_ref{qw(from-tcp from from-com)}) {
  4347. next if !defined($_);
  4348. if (/ \[ (\d{1,3} (?: \. \d{1,3}){3}) (?: \. \d{4,5} )? \] /x) {
  4349. $ip = $1; last;
  4350. } elsif (/\[ [^:\]]* : [^\]]* \]/x && # triage, must contain a colon
  4351. /\[ ( (?: IPv6: )? [0-9a-f]{0,4}
  4352. (?: : [0-9a-f]{0,4} | \. [0-9]{1,3} ){2,9} ) \]/xi) {
  4353. $ip = $1; last;
  4354. } elsif (/ (?: ^ | \D ) ( \d{1,3} (?: \. \d{1,3}){3}) (?! [0-9.] ) /x) {
  4355. $ip = $1; last;
  4356. }
  4357. }
  4358. return undef if !defined $ip; # must return undef even in a list context!
  4359. $ip =~ s/^IPv6://i; # discard 'IPv6:' prefix if any
  4360. do_log(5, "fish_out_ip_from_received: %s", $ip);
  4361. $ip;
  4362. }
  4363. # Splits unquoted fully qualified e-mail address, or an address
  4364. # with a missing domain part. Returns a pair: (localpart, domain).
  4365. # The domain part (if nonempty) includes the '@' as the first character.
  4366. # If the syntax is badly broken, everything ends up as a localpart.
  4367. # The domain part can be an address literal, as specified by RFC 5322.
  4368. # Does not handle explicit route paths, use parse_quoted_rfc2821 for that.
  4369. #
  4370. sub split_address($) {
  4371. my($mailbox) = @_; local($1,$2);
  4372. $mailbox =~ /^ (.*?) ( \@ (?: \[ (?: \\. | [^\]\\] ){0,999} (?: \] | \z)
  4373. | [^\[\@] )*
  4374. ) \z/xs ? ($1, $2) : ($mailbox, '');
  4375. }
  4376. # split_localpart() splits localpart of an e-mail address at the first
  4377. # occurrence of the address extension delimiter character. (based on
  4378. # equivalent routine in Postfix)
  4379. #
  4380. # Reserved addresses are not split: postmaster, mailer-daemon,
  4381. # double-bounce. Addresses that begin with owner-, or addresses
  4382. # that end in -request are not split when the owner_request_special
  4383. # parameter is set.
  4384. #
  4385. sub split_localpart($$) {
  4386. my($localpart, $delimiter) = @_;
  4387. my $owner_request_special = 1; # configurable ???
  4388. my $extension; local($1,$2);
  4389. if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) {
  4390. # do not split these, regardless of what the delimiter is
  4391. } elsif ($delimiter eq '-' && $owner_request_special &&
  4392. $localpart =~ /^owner-.|.-request\z/si) {
  4393. # don't split owner-foo or foo-request
  4394. } elsif ($localpart =~ /^(.+?)(\Q$delimiter\E.*)\z/s) {
  4395. ($localpart, $extension) = ($1, $2); # extension includes a delimiter
  4396. # do not split the address if the result would have a null localpart
  4397. }
  4398. ($localpart, $extension);
  4399. }
  4400. # replace localpart/extension/domain fields of an original email address
  4401. # with nonempty fields of a replacement
  4402. #
  4403. sub replace_addr_fields($$;$) {
  4404. my($orig_addr, $repl_addr, $delim) = @_;
  4405. my($localpart_o, $domain_o, $ext_o, $localpart_r, $domain_r, $ext_r);
  4406. ($localpart_o,$domain_o) = split_address($orig_addr);
  4407. ($localpart_r,$domain_r) = split_address($repl_addr);
  4408. $localpart_r = $localpart_o if $localpart_r eq '';
  4409. $domain_r = $domain_o if $domain_r eq '';
  4410. if (defined $delim && $delim ne '') {
  4411. ($localpart_o,$ext_o) = split_localpart($localpart_o,$delim);
  4412. ($localpart_r,$ext_r) = split_localpart($localpart_r,$delim);
  4413. $ext_r = $ext_o if !defined $ext_r;
  4414. }
  4415. $localpart_r . (defined $ext_r ? $ext_r : '') . $domain_r;
  4416. }
  4417. # given a (potentially multiline) header field Message-ID, Resent-Message-ID.
  4418. # In-Reply-To, or References, parse the RFC 5322 (RFC 2822) syntax extracting
  4419. # all message IDs while ignoring comments, and return them as a list
  4420. # Note: currently does not handle nested comments.
  4421. # See also: RFC 2392 - Content-ID and Message-ID Uniform Resource Locators
  4422. #
  4423. sub parse_message_id($) {
  4424. my($str) = @_;
  4425. $str =~ tr/\n//d; my(@message_id); my $garbage = 0;
  4426. $str =~ s/[ \t]+/ /g; # compress whitespace as a band aid for regexp trouble
  4427. for my $t ( $str =~ /\G ( [ \t]+ | \( (?: \\. | [^()\\] ){0,999} \) |
  4428. < (?: " (?: \\. | [^"\\>] ){0,999} " |
  4429. \[ (?: \\. | [^\]\\>]){0,999} \] |
  4430. [^"<>\[\]\\]+ )* > |
  4431. [^<( \t]+ | . )/gsx ) {
  4432. if ($t =~ /^<.*>\z/) { push(@message_id,$t) }
  4433. elsif ($t =~ /^[ \t]*\z/) {} # ignore FWS
  4434. elsif ($t =~ /^\(.*\)\z/) # ignore CFWS
  4435. { do_log(2, "parse_message_id ignored comment: /%s/ in %s", $t,$str) }
  4436. else { $garbage = 1 }
  4437. }
  4438. if (@message_id > 1) {
  4439. @message_id = unique_list(\@message_id); # remove possible duplicates
  4440. } elsif ($garbage && !@message_id) {
  4441. local($_) = $str; s/^[ \t]+//; s/[ \t\n]+\z//; # trim and sanitize <...>
  4442. s/^<//; s/>\z//; s/>/_/g; $_ = '<'.$_.'>'; @message_id = ($_);
  4443. do_log(5, "parse_message_id sanitizing garbage: /%s/ to %s", $str,$_);
  4444. }
  4445. @message_id;
  4446. }
  4447. # For a given email address (e.g. for User+Foo@sub.exAMPLE.CoM)
  4448. # prepare and return a list of lookup keys in the following order:
  4449. # User+Foo@sub.exAMPLE.COM (as-is, no lowercasing)
  4450. # user+foo@sub.example.com
  4451. # user@sub.example.com (only if $recipient_delimiter nonempty)
  4452. # user+foo(@) (only if $include_bare_user)
  4453. # user(@) (only if $include_bare_user and $recipient_delimiter nonempty)
  4454. # (@)sub.example.com
  4455. # (@).sub.example.com
  4456. # (@).example.com
  4457. # (@).com
  4458. # (@).
  4459. # Note about (@): if $at_with_user is true the user-only keys (without domain)
  4460. # get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash.
  4461. # If $at_with_user is false the domain-only (without localpart) keys
  4462. # get a '@' prepended (e.g. '@.example.com'). Usual for SQL and LDAP lookups.
  4463. #
  4464. # The domain part is lowercased in all but the first item in the resulting
  4465. # list; the localpart is lowercased iff $localpart_is_case_sensitive is true.
  4466. #
  4467. sub make_query_keys($$$;$) {
  4468. my($addr,$at_with_user,$include_bare_user,$append_string) = @_;
  4469. my($localpart,$domain) = split_address($addr); $domain = lc($domain);
  4470. my $saved_full_localpart = $localpart;
  4471. $localpart = lc($localpart) if !c('localpart_is_case_sensitive');
  4472. # chop off leading @, and trailing dots
  4473. local($1);
  4474. $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
  4475. my $extension; my $delim = c('recipient_delimiter');
  4476. if ($delim ne '') {
  4477. ($localpart,$extension) = split_localpart($localpart,$delim);
  4478. # extension includes a delimiter since amavisd-new-2.5.0!
  4479. }
  4480. $extension = '' if !defined $extension; # mute warnings
  4481. my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@');
  4482. my(@keys); # a list of query keys
  4483. push(@keys, $addr); # as is
  4484. push(@keys, $localpart.$extension.'@'.$domain)
  4485. if $extension ne ''; # user+foo@example.com
  4486. push(@keys, $localpart.'@'.$domain); # user@example.com
  4487. if ($include_bare_user) { # typically enabled for local users only
  4488. push(@keys, $localpart.$extension.$append_to_user)
  4489. if $extension ne ''; # user+foo(@)
  4490. push(@keys, $localpart.$append_to_user); # user(@)
  4491. }
  4492. push(@keys, $prepend_to_domain.$domain); # (@)sub.example.com
  4493. if ($domain =~ /\[/) { # don't split address literals
  4494. push(@keys, $prepend_to_domain.'.'); # (@).
  4495. } else {
  4496. my(@dkeys); my $d = $domain;
  4497. for (;;) { # (@).sub.example.com (@).example.com (@).com (@).
  4498. push(@dkeys, $prepend_to_domain.'.'.$d);
  4499. last if $d eq '';
  4500. $d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : '';
  4501. }
  4502. @dkeys = @dkeys[$#dkeys-19 .. $#dkeys] if @dkeys > 20; # sanity limit
  4503. push(@keys, @dkeys);
  4504. }
  4505. if (defined $append_string && $append_string ne '') {
  4506. $_ .= $append_string for @keys;
  4507. }
  4508. my $keys_ref = unique_ref(\@keys); # remove duplicates
  4509. ll(5) && do_log(5,"query_keys: %s", join(', ',@$keys_ref));
  4510. # the rhs replacement strings are similar to what would be obtained
  4511. # by lookup_re() given the following regular expression:
  4512. # /^( ( ( [^\@]*? ) ( \Q$delim\E [^\@]* )? ) (?: \@ (.*) ) )$/xs
  4513. my $rhs = [ # a list of right-hand side replacement strings
  4514. $addr, # $1 = User+Foo@Sub.Example.COM
  4515. $saved_full_localpart, # $2 = User+Foo
  4516. $localpart, # $3 = user (lc if localpart_is_case_sensitive)
  4517. $extension, # $4 = +foo (lc if localpart_is_case_sensitive)
  4518. $domain, # $5 = sub.example.com (lowercased unconditionally)
  4519. ];
  4520. ($keys_ref, $rhs);
  4521. }
  4522. # quote_rfc2821_local() quotes the local part of a mailbox address
  4523. # (given in internal (unquoted) form), and returns external (quoted)
  4524. # mailbox address, as per RFC 5321 (ex RFC 2821).
  4525. #
  4526. # internal (unquoted) form is used internally by amavisd-new and other mail sw,
  4527. # external (quoted) form is used in SMTP commands and in message header section
  4528. #
  4529. # To re-insert message back via SMTP, the local-part of the address needs
  4530. # to be quoted again if it contains reserved characters or otherwise
  4531. # does not obey the dot-atom syntax, as specified in RFC 5321 (ex RFC 2821).
  4532. #
  4533. sub quote_rfc2821_local($) {
  4534. my($mailbox) = @_;
  4535. # atext: any character except controls, SP, and specials (RFC 5321/RFC 5322)
  4536. my $atext = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-";
  4537. # my $specials = '()<>\[\]\\\\@:;,."';
  4538. my($localpart,$domain) = split_address($mailbox);
  4539. if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { # not dot-atom, needs q.
  4540. local($1); # qcontent = qtext / quoted-pair
  4541. $localpart =~ s/([\000-\037\177-\377"\\])/\\$1/g; # quote non-qtext
  4542. $localpart = '"'.$localpart.'"'; # make it a qcontent
  4543. # Postfix hates ""@domain but is not so harsh on @domain
  4544. # Late breaking news: don't bother, both forms are rejected by Postfix
  4545. # when strict_rfc821_envelopes=yes, and both are accepted otherwise
  4546. }
  4547. # we used to strip off empty domain (just '@') unconditionally, but this
  4548. # leads Postfix to interpret an address with a '@' in the quoted local part
  4549. # e.g. <"h@example.net"@> as <hhh@example.net> (subject to Postfix setting
  4550. # 'resolve_dequoted_address'), which is not what the sender requested;
  4551. # we no longer do that if localpart contains an '@':
  4552. $domain = '' if $domain eq '@' && $localpart =~ /\@/;
  4553. $localpart . $domain;
  4554. }
  4555. # wraps the result of quote_rfc2821_local into angle brackets <...> ;
  4556. # If given a list, it returns a list (possibly converted to
  4557. # comma-separated scalar if invoked in scalar context), quoting each element;
  4558. #
  4559. sub qquote_rfc2821_local(@) {
  4560. my(@r) = map($_ eq '' ? '<>' : ('<'.quote_rfc2821_local($_).'>'), @_);
  4561. wantarray ? @r : join(', ', @r);
  4562. }
  4563. sub parse_quoted_rfc2821($$) {
  4564. my($addr,$unquote) = @_;
  4565. # the angle-bracket stripping is not really a duty of this subroutine,
  4566. # as it should have been already done elsewhere, but we allow it here anyway:
  4567. $addr =~ s/^\s*<//s; $addr =~ s/>\s*\z//s; # tolerate unmatched angle brkts
  4568. local($1,$2); my($source_route,$localpart,$domain) = ('','','');
  4569. # RFC 2821: so-called "source route" MUST BE accepted,
  4570. # SHOULD NOT be generated, and SHOULD be ignored.
  4571. # Path = "<" [ A-d-l ":" ] Mailbox ">"
  4572. # A-d-l = At-domain *( "," A-d-l )
  4573. # At-domain = "@" domain
  4574. if (index($addr,':') >= 0 && # triage before more testing for source route
  4575. $addr =~ m{^ ( [ \t]* \@ (?: [0-9A-Za-z.!\#\$%&*/^{}=_+-]* |
  4576. \[ (?: \\. | [^\]\\] ){0,999} \] ) [ \t]*
  4577. (?: , [ \t]* \@ (?: [0-9A-Za-z.!\#\$%&*/^{}=_+-]* |
  4578. \[ (?: \\. | [^\]\\] ){0,999} \] ) [ \t]* )*
  4579. : [ \t]* ) (.*) \z }xs)
  4580. { # NOTE: we are quite liberal on allowing whitespace around , and : here,
  4581. # and liberal in allowed character set and syntax of domain names,
  4582. # we mainly avoid stop-characters in the domain names of source route
  4583. $source_route = $1; $addr = $2;
  4584. }
  4585. if ($addr =~ m{^ ( .*? )
  4586. ( \@ (?: [^\@\[\]]+ | \[ (?: \\. | [^\]\\] ){0,999} \]
  4587. | [^\@] )* )
  4588. \z}xs) {
  4589. ($localpart,$domain) = ($1,$2);
  4590. } else {
  4591. ($localpart,$domain) = ($addr,'');
  4592. }
  4593. $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg if $unquote; # undo quoted-pairs
  4594. ($source_route, $localpart, $domain);
  4595. }
  4596. # unquote_rfc2821_local() strips away the quoting from the local part
  4597. # of an external (quoted) mailbox address, and returns internal (unquoted)
  4598. # mailbox address, as per RFC 5321 (ex RFC 2821).
  4599. # Internal (unquoted) form is used internally by amavisd-new and other mail sw,
  4600. # external (quoted) form is used in SMTP commands and in message header section
  4601. #
  4602. sub unquote_rfc2821_local($) {
  4603. my($mailbox) = @_;
  4604. my($source_route,$localpart,$domain) = parse_quoted_rfc2821($mailbox,1);
  4605. # make address with '@' in the localpart but no domain (like <"aa@bb.com"> )
  4606. # distinguishable from <aa@bb.com> by representing it as aa@bb.com@ in
  4607. # unquoted form; (it still obeys all regular rules, it is not a dirty trick)
  4608. $domain = '@' if $domain eq '' && $localpart ne '' && $localpart =~ /\@/;
  4609. $localpart . $domain;
  4610. }
  4611. # Parse an rfc2822.address-list, returning a list of RFC 5322 (quoted)
  4612. # addresses. Properly deals with group addresses, nested comments, address
  4613. # literals, qcontent, addresses with source route, discards display
  4614. # names and comments. The following header fields accept address-list:
  4615. # To, Cc, Bcc, Reply-To. A header field 'From' accepts a 'mailbox-list'
  4616. # syntax (which is similar, but does not allow groups); a header field
  4617. # 'Sender' accepts a 'mailbox' syntax, i.e. only one address and not a group.
  4618. #
  4619. use vars qw($s $p @addresses);
  4620. sub flush_a() {
  4621. $s =~ s/^[ \t]+//s; $s =~ s/[ \t]\z//s; # trim
  4622. $p =~ s/^[ \t]+//s; $p =~ s/[ \t]\z//s;
  4623. if ($p ne '') { $p =~ s/^<//; $p =~ s/>\z//; push(@addresses,$p) }
  4624. elsif ($s ne '') { push(@addresses,$s) }
  4625. $p = ''; $s = '';
  4626. }
  4627. sub parse_address_list($) {
  4628. local($_) = $_[0];
  4629. local($1); s/\n(?=[ \t])//gs; s/\n+\z//s; # unfold, chomp
  4630. my $str_l = length($_); $p = ''; $s = ''; @addresses = ();
  4631. my($comm_lvl, $in_qcontent, $in_literal,
  4632. $in_group, $in_angle, $after_at) = (0) x 6;
  4633. my $new_pos;
  4634. for (my $pos=-1; $new_pos=pos($_), $new_pos<$str_l; $pos=$new_pos) {
  4635. $new_pos > $pos or die "parse_address_list PANIC1 $new_pos"; # just in case
  4636. # comment (may be nested: RFC 5322 section 3.2.2)
  4637. if ($comm_lvl > 0 && /\G( \) )/gcsx) { $comm_lvl--; next }
  4638. if (!$in_qcontent && !$in_literal && /\G( \( )/gcsx) { $comm_lvl++; next }
  4639. if ($comm_lvl > 0 && /\G( \\. )/gcsx) { next }
  4640. if ($comm_lvl > 0 && /\G( [^()\\]+ )/gcsx) { next }
  4641. # quoted content
  4642. if ($in_qcontent && /\G( " )/gcsx) # normal exit from qcontent
  4643. { $in_qcontent = 0; ($in_angle?$p:$s) .= $1; next }
  4644. if ($in_qcontent && /\G( > )/gcsx) # bail out of qcontent
  4645. { $in_qcontent = 0; $in_angle = 0; $after_at = 0;
  4646. ($in_angle?$p:$s) .= $1; next }
  4647. if (!$comm_lvl && !$in_qcontent && !$in_literal && /\G( " )/gcsx)
  4648. { $in_qcontent = 1; ($in_angle?$p:$s) .= $1; next }
  4649. if ($in_qcontent && /\G( \\. )/gcsx) { ($in_angle?$p:$s) .= $1; next }
  4650. if ($in_qcontent && /\G( [^"\\>]+ )/gcsx) { ($in_angle?$p:$s) .= $1; next }
  4651. # address literal
  4652. if ($in_literal && /\G( \] )/gcsx)
  4653. { $in_literal = 0; ($in_angle?$p:$s) .= $1; next }
  4654. if ($in_literal && /\G( > )/gcsx) # bail out of address literal
  4655. { $in_literal = 0; $in_angle = 0; $after_at = 0;
  4656. ($in_angle?$p:$s) .= $1; next }
  4657. if (!$comm_lvl && !$in_qcontent && /\G( \[ )/gcsx)
  4658. { $in_literal = 1 if $after_at; ($in_angle?$p:$s) .= $1; next }
  4659. if ($in_literal && /\G( \\. )/gcsx) { ($in_angle?$p:$s) .= $1; next }
  4660. if ($in_literal && /\G( [^\]\\>]+ )/gcsx) { ($in_angle?$p:$s) .= $1; next }
  4661. # normal content
  4662. if (!$comm_lvl && !$in_qcontent && !$in_literal) {
  4663. if (!$in_angle && /\G( < )/gcsx)
  4664. { $in_angle = 1; $after_at = 0; flush_a() if $p ne ''; $p .= $1; next }
  4665. if ( $in_angle && /\G( > )/gcsx)
  4666. { $in_angle = 0; $after_at = 0; $p .= $1; next }
  4667. if (/\G( , )/gcsx) # top-level addr separator or source route delimiter
  4668. { !$in_angle ? flush_a() : ($p.=$1); $after_at = 0; next }
  4669. if (!$in_angle && !$in_group && /\G( : )/gcsx) # group name terminator
  4670. { $in_group = 1; $s .= $1; $p=$s=''; next } # discard group name
  4671. if ($after_at && /\G( : )/gcsx) # source route terminator
  4672. { $after_at = 0; ($in_angle?$p:$s) .= $1; next }
  4673. if ( $in_group && /\G( ; )/gcsx) # group terminator
  4674. { $in_group = 0; $after_at = 0; next }
  4675. if (!$in_group && /\G( ; )/gcsx) # out of place special
  4676. { ($in_angle?$p:$s) .= $1; $after_at = 0; next }
  4677. if (/\G( \@ )/gcsx) { $after_at = 1; ($in_angle?$p:$s) .= $1; next }
  4678. if (/\G( [ \t]+ )/gcsx) { ($in_angle?$p:$s) .= $1; next }
  4679. if (/\G( [^,:;\@<>()"\[\]\\]+ )/gcsx) { ($in_angle?$p:$s) .= $1; next }
  4680. }
  4681. if (/\G( . )/gcsx) { ($in_angle?$p:$s) .= $1; next } # other junk
  4682. die "parse_address_list PANIC2 $new_pos"; # just in case
  4683. }
  4684. flush_a(); @addresses;
  4685. }
  4686. # compute a total displayed line size if a string (possibly containing TAB
  4687. # characters) would be displayed at the given character position (0-based)
  4688. #
  4689. sub displayed_length($$) {
  4690. my($str,$ind) = @_;
  4691. for my $t ($str =~ /\G ( \t | [^\t]+ )/gsx)
  4692. { $ind += $t ne "\t" ? length($t) : 8 - $ind % 8 }
  4693. $ind;
  4694. }
  4695. # Wrap a string into a multiline string, inserting \n as appropriate to keep
  4696. # each line length at $max_len or shorter (not counting \n). A string $prefix
  4697. # is prepended to each line. Continuation lines get their first space or TAB
  4698. # character replaced by a string $indent (unless $indent is undefined, which
  4699. # keeps the leading whitespace character unchanged). Both the $prefix and
  4700. # $indent are included in line size calculation, and for the purpose of line
  4701. # size calculations TABs are treated as an appropriate number of spaces.
  4702. # Parameter $structured indicates where line breaks are permitted: true
  4703. # indicates that line breaks may only occur where a \n character is already
  4704. # present in the source line, indicating possible (tentative) line breaks.
  4705. # If $structured is false, permitted line breaks are chosen within existing
  4706. # whitespace substrings so that all-whitespace lines are never generated
  4707. # (even at the expense of producing longer than allowed lines if necessary),
  4708. # and that each continuation line starts by at least one whitespace character.
  4709. # Whitespace is neither added nor removed, but simply spliced into trailing
  4710. # and leading whitespace of subsequent lines. Typically leading whitespace
  4711. # is a single character, but may include part of the trailing whitespace of
  4712. # the preceding line if it would otherwise be too long. This is appropriate
  4713. # and required for wrapping of mail header fields. An exception to preservation
  4714. # of whitespace is when $indent string is defined but is an empty string,
  4715. # causing leading and trailing whitespace to be trimmed, producing a classical
  4716. # plain text wrapping results. Intricate!
  4717. #
  4718. sub wrap_string($;$$$$) {
  4719. my($str,$max_len,$prefix,$indent,$structured) = @_;
  4720. $max_len = 78 if !defined $max_len;
  4721. $prefix = '' if !defined $prefix;
  4722. $structured = 0 if !defined $structured;
  4723. my(@chunks);
  4724. # split a string into chunks where each chunk starts with exactly one SP or
  4725. # TAB character (except possibly the first chunk), followed by an unbreakable
  4726. # string (consisting typically entirely of non-whitespace characters, at
  4727. # least one character must be non-whitespace), followed by an all-whitespace
  4728. # string consisting of only SP or TAB characters.
  4729. if ($structured) {
  4730. local($1);
  4731. # unfold all-whitespace chunks, just in case
  4732. 1 while $str =~ s/^([ \t]*)\n/$1/; # prefixed?
  4733. $str =~ s/\n(?=[ \t]*(\n|\z))//g; # within and at end
  4734. $str =~ s/\n(?![ \t])/\n /g; # insert a space at line folds if missing
  4735. # unbreakable parts are substrings between newlines, determined by caller
  4736. @chunks = split(/\n/,$str,-1);
  4737. } else {
  4738. $str =~ s/\n(?![ \t])/\n /g; # insert a space at line folds if missing
  4739. $str =~ s/\n//g; # unfold (knowing a space at folds is not missing)
  4740. # unbreakable parts are non- all-whitespace substrings
  4741. @chunks = $str =~ /\G ( (?: ^ .*? | [ \t]) [^ \t]+ [ \t]* )
  4742. (?= \z | [ \t] [^ \t] )/gsx;
  4743. }
  4744. # do_log(5,"wrap_string chunk: <%s>", $_) for @chunks;
  4745. my $result = ''; # wrapped multiline string will accumulate here
  4746. my $s = ''; # collects partially assembled single line
  4747. my $s_displ_ind = # display size of string in $s, including $prefix
  4748. displayed_length($prefix,0);
  4749. my $contin_line = 0; # are we assembling a continuation line?
  4750. while (@chunks) { # walk through input substrings and join shorter sections
  4751. my $chunk = shift(@chunks);
  4752. # replace leading space char with $indent if starting a continuation line
  4753. $chunk =~ s/^[ \t]/$indent/ if defined $indent && $contin_line && $s eq '';
  4754. my $s_displ_l = displayed_length($chunk, $s_displ_ind);
  4755. if ($s_displ_l <= $max_len # collecting in $s while still fits
  4756. || (@chunks==0 && $s =~ /^[ \t]*\z/)) { # or we are out of options
  4757. $s .= $chunk; $s_displ_ind = $s_displ_l; # absorb entire chunk
  4758. } else {
  4759. local($1,$2);
  4760. $chunk =~ /^ ( .* [^ \t] ) ( [ \t]* ) \z/xs # split to head and allwhite
  4761. or die "Assert 1 failed in wrap: /$result/, /$chunk/";
  4762. my($solid,$white_tail) = ($1,$2);
  4763. my $min_displayed_s_len = displayed_length($solid, $s_displ_ind);
  4764. if (@chunks > 0 # not being at the last chunk gives a chance to shove
  4765. # part of the trailing whitespace off to the next chunk
  4766. && ($min_displayed_s_len <= $max_len # non-whitespace part fits
  4767. || $s =~ /^[ \t]*\z/) ) { # or still allwhite even if too long
  4768. $s .= $solid; $s_displ_ind = $min_displayed_s_len; # take nonwhite
  4769. if (defined $indent && $indent eq '') {
  4770. # discard leading whitespace in continuation lines on a plain wrap
  4771. } else {
  4772. # preserve all original whitespace
  4773. while ($white_tail ne '') {
  4774. # stash-in as much trailing whitespace as it fits to the curr. line
  4775. my $c = substr($white_tail,0,1); # one whitespace char. at a time
  4776. my $dlen = displayed_length($c, $s_displ_ind);
  4777. if ($dlen > $max_len) { last }
  4778. else {
  4779. $s .= $c; $s_displ_ind = $dlen; # absorb next whitespace char.
  4780. $white_tail = substr($white_tail,1); # one down, more to go...
  4781. }
  4782. }
  4783. # push remaining trailing whitespace characters back to input
  4784. $chunks[0] = $white_tail . $chunks[0] if $white_tail ne '';
  4785. }
  4786. } elsif ($s =~ /^[ \t]*\z/) {
  4787. die "Assert 2 failed in wrap: /$result/, /$chunk/";
  4788. } else { # nothing more fits to $s, flush it to $result
  4789. if ($contin_line) { $result .= "\n" } else { $contin_line = 1 }
  4790. # trim trailing whitespace when wrapping as a plain text (not headers)
  4791. $s =~ s/[ \t]+\z// if defined $indent && $indent eq '';
  4792. $result .= $prefix.$s; $s = '';
  4793. $s_displ_ind = displayed_length($prefix,0);
  4794. unshift(@chunks,$chunk); # reprocess the chunk
  4795. }
  4796. }
  4797. }
  4798. if ($s !~ /^[ \t]*\z/) { # flush last chunk if nonempty
  4799. if ($contin_line) { $result .= "\n" } else { $contin_line = 1 }
  4800. $s =~ s/[ \t]+\z// if defined $indent && $indent eq ''; # trim plain text
  4801. $result .= $prefix.$s; $s = '';
  4802. }
  4803. $result;
  4804. }
  4805. # wrap an SMTP response at each \n char according to RFC 5321 (ex RFC 2821),
  4806. # returning resulting lines as a listref
  4807. #
  4808. sub wrap_smtp_resp($) {
  4809. my($resp) = @_;
  4810. # RFC 5321: The maximum total length of a reply line including the
  4811. # reply code and the <CRLF> is 512 octets. More information
  4812. # may be conveyed through multiple-line replies.
  4813. my $max_len = 512-2; my(@result_list); local($1,$2,$3,$4);
  4814. if ($resp !~ /^ ([1-5]\d\d) (\ |-|\z)
  4815. ([245] \. \d{1,3} \. \d{1,3} (?: \ |\z) )?
  4816. (.*) \z/xs)
  4817. { die "wrap_smtp_resp: bad SMTP response code: '$resp'" }
  4818. my($resp_code,$more,$enhanced,$tail) = ($1,$2,$3,$4);
  4819. my $lead_len = length($resp_code) + 1 + length($enhanced);
  4820. while (length($tail) > $max_len-$lead_len || $tail =~ /\n/) {
  4821. # RFC 2034: When responses are continued across multiple lines the same
  4822. # status code must appear at the beginning of the text in each line
  4823. # of the response.
  4824. my $head = substr($tail, 0, $max_len-$lead_len);
  4825. if ($head =~ /^([^\n]*\n)/s) { $head = $1 }
  4826. $tail = substr($tail,length($head)); chomp($head);
  4827. push(@result_list, $resp_code.'-'.$enhanced.$head);
  4828. }
  4829. push(@result_list, $resp_code.' '.$enhanced.$tail);
  4830. \@result_list;
  4831. }
  4832. # Prepare a single SMTP response and an exit status as per sysexits.h
  4833. # from individual per-recipient response codes, taking into account
  4834. # sendmail milter specifics. Returns a triple: (smtp response, exit status,
  4835. # an indication whether a non delivery notification (NDN, a form of DSN)
  4836. # is needed).
  4837. #
  4838. sub one_response_for_all($$;$) {
  4839. my($msginfo, $dsn_per_recip_capable, $suppressed) = @_;
  4840. my($smtp_resp, $exit_code, $ndn_needed);
  4841. my $am_id = $msginfo->log_id;
  4842. my $sender = $msginfo->sender;
  4843. my $per_recip_data = $msginfo->per_recip_data;
  4844. my $any_not_done = scalar(grep(!$_->recip_done, @$per_recip_data));
  4845. if (!@$per_recip_data) { # no recipients, nothing to do
  4846. $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK;
  4847. do_log(5, "one_response_for_all <%s>: no recipients, '%s'",
  4848. $sender, $smtp_resp);
  4849. }
  4850. if (!defined $smtp_resp) {
  4851. for my $r (@$per_recip_data) { # any 4xx code ?
  4852. if ($r->recip_smtp_response =~ /^4/) # pick the first 4xx code
  4853. { $smtp_resp = $r->recip_smtp_response; last }
  4854. }
  4855. }
  4856. if (!defined $smtp_resp) {
  4857. for my $r (@$per_recip_data) {
  4858. my $fwd_m = $r->delivery_method;
  4859. if (!defined $fwd_m) {
  4860. die "one_response_for_all: delivery_method not defined";
  4861. } elsif ($fwd_m ne '' && $any_not_done) {
  4862. die "Explicit forwarding, but not all recips done";
  4863. }
  4864. }
  4865. for my $r (@$per_recip_data) { # any invalid code ?
  4866. if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) {
  4867. $smtp_resp = '451 4.5.0 Bad SMTP response code??? "'
  4868. . $r->recip_smtp_response . '"';
  4869. last; # pick the first
  4870. }
  4871. }
  4872. if (defined $smtp_resp) {
  4873. $exit_code = EX_TEMPFAIL;
  4874. do_log(5, "one_response_for_all <%s>: 4xx found, '%s'",
  4875. $sender,$smtp_resp);
  4876. }
  4877. }
  4878. # NOTE: a 2xx SMTP response code is set both by internal Discard
  4879. # and by a genuine successful delivery. To distinguish between the two
  4880. # we need to check $r->recip_destiny as well.
  4881. #
  4882. if (!defined $smtp_resp) {
  4883. # if destiny for _all_ recipients is D_DISCARD, give Discard
  4884. my $notall;
  4885. for my $r (@$per_recip_data) {
  4886. if ($r->recip_destiny == D_DISCARD) # pick the first DISCARD code
  4887. { $smtp_resp = $r->recip_smtp_response if !defined $smtp_resp }
  4888. else { $notall=1; last } # one is not a discard, nogood
  4889. }
  4890. if ($notall) { $smtp_resp = undef }
  4891. if (defined $smtp_resp) {
  4892. $exit_code = 99; # helper program will interpret 99 as discard
  4893. do_log(5, "one_response_for_all <%s>: all DISCARD, '%s'",
  4894. $sender,$smtp_resp);
  4895. }
  4896. }
  4897. if (!defined $smtp_resp) {
  4898. # destiny for _all_ recipients is Discard or Reject, give 5xx
  4899. # (and there is at least one Reject)
  4900. my($notall, $done_level);
  4901. my $bounce_cnt = 0;
  4902. for my $r (@$per_recip_data) {
  4903. my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
  4904. if ($dest == D_DISCARD) {
  4905. # ok, this one is a discard, let's see the rest
  4906. } elsif ($resp =~ /^5/ && $dest != D_BOUNCE) {
  4907. # prefer to report SMTP response code of genuine rejects
  4908. # from MTA, over internal rejects by content filters
  4909. if (!defined $smtp_resp || $r->recip_done > $done_level)
  4910. { $smtp_resp = $resp; $done_level = $r->recip_done }
  4911. } else {
  4912. $notall=1; last; # one is a Pass or Bounce, nogood
  4913. }
  4914. }
  4915. if ($notall) { $smtp_resp = undef }
  4916. if (defined $smtp_resp) {
  4917. $exit_code = EX_UNAVAILABLE;
  4918. do_log(5, "one_response_for_all <%s>: REJECTs, '%s'",$sender,$smtp_resp);
  4919. }
  4920. }
  4921. if (!defined $smtp_resp) {
  4922. # mixed destiny => 2xx, but generate dsn for bounces and rejects
  4923. my($rej_cnt, $bounce_cnt, $drop_cnt) = (0,0,0);
  4924. for my $r (@$per_recip_data) {
  4925. my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
  4926. if ($resp =~ /^2/ && $dest == D_PASS) # genuine successful delivery
  4927. { $smtp_resp = $resp if !defined $smtp_resp }
  4928. $drop_cnt++ if $dest == D_DISCARD;
  4929. if ($resp =~ /^5/)
  4930. { if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } }
  4931. }
  4932. $exit_code = EX_OK;
  4933. if (!defined $smtp_resp) { # no genuine Pass/2xx
  4934. # declare success, we'll handle bounce
  4935. $smtp_resp = "250 2.5.0 Ok, id=$am_id";
  4936. if ($any_not_done) { $smtp_resp .= ", continue delivery" }
  4937. else { $exit_code = 99 } # helper program DISCARD (e.g. milter)
  4938. }
  4939. if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) {
  4940. $smtp_resp .= ", ";
  4941. $smtp_resp .= "but " if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data;
  4942. $smtp_resp .= join ", and ",
  4943. map { my($cnt, $nm) = @$_;
  4944. !$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm"
  4945. } ([$rej_cnt, 'REJECT'],
  4946. [$bounce_cnt, $suppressed ? 'DISCARD(bounce.suppressed)' :'BOUNCE'],
  4947. [$drop_cnt, 'DISCARD']);
  4948. }
  4949. $ndn_needed =
  4950. ($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0;
  4951. ll(5) && do_log(5,
  4952. "one_response_for_all <%s>: %s, r=%d,b=%d,d=%s, ndn_needed=%s, '%s'",
  4953. $sender,
  4954. $rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success',
  4955. $rej_cnt, $bounce_cnt, $drop_cnt, $ndn_needed, $smtp_resp);
  4956. }
  4957. ($smtp_resp, $exit_code, $ndn_needed);
  4958. }
  4959. 1;
  4960. #
  4961. package Amavis::Lookup::RE;
  4962. use strict;
  4963. use re 'taint';
  4964. BEGIN {
  4965. require Exporter;
  4966. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  4967. $VERSION = '2.316';
  4968. @ISA = qw(Exporter);
  4969. import Amavis::Util qw(ll do_log fmt_struct);
  4970. }
  4971. # Make an object out of the supplied lookup list
  4972. # to make it distinguishable from simple ACL array
  4973. sub new($$) { my $class = shift; bless [@_], $class }
  4974. # lookup_re() performs a lookup for an e-mail address or other key string
  4975. # against a list of regular expressions.
  4976. #
  4977. # A full unmodified e-mail address is always used, so splitting to localpart
  4978. # and domain or lowercasing is NOT performed. The regexp is powerful enough
  4979. # that this can be accomplished by its own mechanisms. The routine is useful
  4980. # for other RE tests besides the usual e-mail addresses, such as looking for
  4981. # banned file names.
  4982. #
  4983. # Each element of the list can be a ref to a pair, or directly a regexp
  4984. # ('Regexp' object created by a qr operator, or just a (less efficient)
  4985. # string containing a regular expression). If it is a pair, the first
  4986. # element is treated as a regexp, and the second provides a value in case
  4987. # the regexp matches. If not a pair, the implied result of a match is 1.
  4988. #
  4989. # The regular expression is taken as-is, no implicit anchoring or setting
  4990. # case insensitivity is done, so do use a qr'(?i)^user\@example\.com$',
  4991. # and not a sloppy qr'user@example.com', which can easily backfire.
  4992. # Also, if qr is used with a delimiter other than ' (apostrophe), make sure
  4993. # to quote the @ and $ when they are not introducing a variable name.
  4994. #
  4995. # The pattern allows for capturing of parenthesized substrings, which can
  4996. # then be referenced from the result string using the $1, $2, ... notation,
  4997. # as with a Perl m// operator. The number after a $ may be a multi-digit
  4998. # decimal number. To avoid possible ambiguity a ${n} or $(n) form may be used
  4999. # Substring numbering starts with 1. Nonexistent references evaluate to empty
  5000. # strings. If any substitution is done, the result inherits the taintedness
  5001. # of $addr. Keep in mind that $ and @ characters needs to be backslash-quoted
  5002. # in qq() strings. Example:
  5003. # $virus_quarantine_to = new_RE(
  5004. # [ qr'^(.*)\@example\.com$'i => 'virus-${1}@example.com' ],
  5005. # [ qr'^(.*)(\@[^\@]*)?$'i => 'virus-${1}${2}' ] );
  5006. #
  5007. # Example (equivalent to the example in lookup_acl):
  5008. # $acl_re = Amavis::Lookup::RE->new(
  5009. # qr'\@me\.ac\.uk$'i, [qr'[\@.]ac\.uk$'i=>0], qr'\.uk$'i );
  5010. # ($r,$k) = $acl_re->lookup_re('user@me.ac.uk');
  5011. # or $r = lookup(0, 'user@me.ac.uk', $acl_re);
  5012. #
  5013. # 'user@me.ac.uk' matches me.ac.uk, returns true and search stops
  5014. # 'user@you.ac.uk' matches .ac.uk, returns false (because of =>0)
  5015. # and search stops
  5016. # 'user@them.co.uk' matches .uk, returns true and search stops
  5017. # 'user@some.com' does not match anything, falls through and
  5018. # returns false (undef)
  5019. #
  5020. # As a special allowance, the $addr argument may be a ref to a list of search
  5021. # keys. At each step in traversing the supplied regexp list, all elements of
  5022. # @$addr are tried. If any of them matches, the search stops. This is currently
  5023. # used in banned names lookups, where all attributes of a part are given as a
  5024. # list @$addr, as a loop on attributes must be an inner loop.
  5025. #
  5026. sub lookup_re($$;$%) {
  5027. my($self, $addr,$get_all,%options) = @_;
  5028. local($1,$2,$3,$4); my(@matchingkey,@result);
  5029. $addr .= $options{AppendStr} if defined $options{AppendStr};
  5030. for my $e (@$self) { # try each regexp in the list
  5031. my($key,$r);
  5032. if (ref($e) eq 'ARRAY') { # a pair: (regexp,result)
  5033. ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]);
  5034. } else { # a single regexp (not a pair), implies result 1
  5035. ($key,$r) = ($e, 1);
  5036. }
  5037. # braindamaged Perl: empty string implies the last successfully
  5038. # matched regular expression; we must avoid this:
  5039. $key = qr{(?:)} if !defined $key || $key eq '';
  5040. my(@rhs); # match, capturing parenthesized subpatterns into @rhs
  5041. if (!ref($addr)) { @rhs = $addr =~ /$key/ }
  5042. else { for (@$addr) { @rhs = /$key/; last if @rhs } } # inner loop
  5043. if (@rhs) { # regexp matches
  5044. # do the righthand side replacements if any $n, ${n} or $(n) is specified
  5045. if (defined($r) && !ref($r) && index($r,'$') >= 0) { # triage
  5046. my $any = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
  5047. { my $j=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse;
  5048. # bring taintedness of input to the result
  5049. $r .= substr($addr,0,0) if $any;
  5050. }
  5051. push(@result,$r); push(@matchingkey,$key);
  5052. last if !$get_all;
  5053. }
  5054. }
  5055. if (!ll(5)) {
  5056. # don't bother preparing log report which will not be printed
  5057. } elsif (!@result) {
  5058. do_log(5, "lookup_re(%s), no matches", fmt_struct($addr));
  5059. } else { # pretty logging
  5060. my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
  5061. e => "\e", a => "\a", t => "\t");
  5062. my(@mk) = @matchingkey;
  5063. for my $mk (@mk) # undo the \-quoting, will be redone by logging routines
  5064. { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx }
  5065. if (!$get_all) { # first match wins
  5066. do_log(5, 'lookup_re(%s) matches key "%s", result=%s',
  5067. fmt_struct($addr), $mk[0], fmt_struct($result[0]));
  5068. } else { # want all matches
  5069. do_log(5, "lookup_re(%s) matches keys: %s", fmt_struct($addr),
  5070. join(', ', map {sprintf('"%s"=>%s', $mk[$_],fmt_struct($result[$_]))}
  5071. (0..$#result)));
  5072. }
  5073. }
  5074. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  5075. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  5076. }
  5077. 1;
  5078. #
  5079. package Amavis::Lookup::IP;
  5080. use strict;
  5081. use re 'taint';
  5082. BEGIN {
  5083. require Exporter;
  5084. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION $have_patricia);
  5085. $VERSION = '2.316';
  5086. @ISA = qw(Exporter);
  5087. @EXPORT_OK = qw(&lookup_ip_acl &ip_to_vec &normalize_ip_addr);
  5088. import Amavis::Util qw(ll do_log);
  5089. }
  5090. use subs @EXPORT_OK;
  5091. BEGIN {
  5092. eval {
  5093. require Net::Patricia;
  5094. Net::Patricia->VERSION(1.015); # need AF_INET6 support
  5095. import Net::Patricia;
  5096. $have_patricia = 1;
  5097. } or do {
  5098. undef $have_patricia;
  5099. };
  5100. }
  5101. # ip_to_vec() takes an IPv6 or IPv4 address with optional prefix length
  5102. # (or an IPv4 mask), parses and validates it, and returns it as a 128-bit
  5103. # vector string that can be used as operand to Perl bitwise string operators.
  5104. # Syntax and other errors in the argument throw exception (die).
  5105. # If the second argument $allow_mask is 0, the prefix length or mask
  5106. # specification is not allowed as part of the IP address.
  5107. #
  5108. # The IPv6 syntax parsing and validation adheres to RFC 4291 (ex RFC 3513).
  5109. # All the following IPv6 address forms are supported:
  5110. # x:x:x:x:x:x:x:x preferred form
  5111. # x:x:x:x:x:x:d.d.d.d alternative form
  5112. # ...::... zero-compressed form
  5113. # addr/prefix-length prefix length may be specified (defaults to 128)
  5114. # Optionally an "IPv6:" prefix may be prepended to an IPv6 address
  5115. # as specified by RFC 5321 (ex RFC 2821). Brackets enclosing the address
  5116. # are optional, e.g. [::1]/128 .
  5117. #
  5118. # The following IPv4 forms are allowed:
  5119. # d.d.d.d
  5120. # d.d.d.d/prefix-length CIDR mask length is allowed (defaults to 32)
  5121. # d.d.d.d/m.m.m.m network mask (gets converted to prefix-length)
  5122. # If prefix-length or a mask is specified with an IPv4 address, the address
  5123. # may be shortened to d.d.d/n or d.d/n or d/n. Such truncation is allowed
  5124. # for compatibility with earlier version, but is deprecated and is not
  5125. # allowed for IPv6 addresses.
  5126. #
  5127. # IPv4 addresses and masks are converted to IPv4-mapped IPv6 addresses
  5128. # of the form ::FFFF:d.d.d.d, The CIDR mask length (0..32) is converted
  5129. # to an IPv6 prefix-length (96..128). The returned vector strings resulting
  5130. # from IPv4 and IPv6 forms are indistinguishable.
  5131. #
  5132. # NOTE:
  5133. # d.d.d.d is equivalent to ::FFFF:d.d.d.d (IPv4-mapped IPv6 address)
  5134. # which is not the same as ::d.d.d.d (IPv4-compatible IPv6 address)
  5135. #
  5136. # A triple is returned:
  5137. # - an IP address represented as a 128-bit vector (a string)
  5138. # - network mask derived from prefix length, a 128-bit vector (string)
  5139. # - prefix length as an integer (0..128)
  5140. #
  5141. sub ip_to_vec($;$) {
  5142. my($ip,$allow_mask) = @_;
  5143. my $ip_len; my @ip_fields;
  5144. local($1,$2,$3,$4,$5,$6);
  5145. $ip =~ s/^[ \t]+//; $ip =~ s/[ \t\r\n]+\z//s; # trim
  5146. my $ipa = $ip;
  5147. ($ipa,$ip_len) = ($1,$2) if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s;
  5148. $ipa = $1 if $ipa =~ m{^ \[ (.*) \] \z}xs; # discard optional brackets
  5149. $ipa =~ s/%[A-Z0-9:._-]+\z//si; # discard interface specification
  5150. if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){
  5151. # IPv6 alternative form x:x:x:x:x:x:d.d.d.d
  5152. my(@d) = ($3,$4,$5,$6);
  5153. !grep($_ > 255, @d)
  5154. or die "Invalid decimal field value in IPv6 address: [$ip]\n";
  5155. $ipa = $2 . sprintf('%02X%02X:%02X%02X', @d);
  5156. } elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) { # IPv4 form
  5157. my(@d) = split(/\./,$ipa,-1);
  5158. !grep($_ > 255, @d)
  5159. or die "Invalid field value in IPv4 address: [$ip]\n";
  5160. defined($ip_len) || @d==4
  5161. or die "IPv4 address [$ip] contains fewer than 4 fields\n";
  5162. $ipa = '::FFFF:' . sprintf('%02X%02X:%02X%02X', @d); # IPv4-mapped IPv6
  5163. if (!defined($ip_len)) { $ip_len = 32; # no length, defaults to /32
  5164. } elsif ($ip_len =~ /^\d{1,9}\z/) { # /n, IPv4 CIDR notation
  5165. } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
  5166. !grep($_ > 255, ($1,$2,$3,$4))
  5167. or die "Illegal field value in IPv4 mask: [$ip]\n";
  5168. my $mask1 = pack('C4', $1,$2,$3,$4); # /m.m.m.m
  5169. my $len = unpack('%b*', $mask1); # count ones
  5170. my $mask2 = pack('B32', '1' x $len); # reconstruct mask from count
  5171. $mask1 eq $mask2
  5172. or die "IPv4 mask not representing a valid CIDR mask: [$ip]\n";
  5173. $ip_len = $len;
  5174. } else {
  5175. die "Invalid IPv4 network mask or CIDR prefix length: [$ip]\n";
  5176. }
  5177. $ip_len<=32 or die "IPv4 network prefix length greater than 32: [$ip]\n";
  5178. $ip_len += 128-32; # convert IPv4 net mask length to IPv6 prefix length
  5179. }
  5180. $ipa =~ s/^IPv6://i;
  5181. # now we presumably have an IPv6 compressed or preferred form x:x:x:x:x:x:x:x
  5182. if ($ipa !~ /^(.*?)::(.*)\z/s) { # zero-compressing form used?
  5183. @ip_fields = split(/:/,$ipa,-1); # no, have preferred form
  5184. } else { # expand zero-compressing form
  5185. my($before,$after) = ($1,$2);
  5186. my(@bfr) = split(/:/,$before,-1); my(@aft) = split(/:/,$after,-1);
  5187. my $missing_cnt = 8-(@bfr+@aft); $missing_cnt = 1 if $missing_cnt<1;
  5188. @ip_fields = (@bfr, ('0') x $missing_cnt, @aft);
  5189. }
  5190. @ip_fields<8 and die "IPv6 address [$ip] contains fewer than 8 fields\n";
  5191. @ip_fields>8 and die "IPv6 address [$ip] contains more than 8 fields\n";
  5192. !grep(!/^[0-9a-zA-Z]{1,4}\z/, @ip_fields) # this is quite slow
  5193. or die "Invalid syntax of IPv6 address: [$ip]\n";
  5194. my $vec = pack('n8', map(hex($_),@ip_fields));
  5195. if (!defined($ip_len)) { $ip_len = 128 }
  5196. elsif ($ip_len !~ /^\d{1,3}\z/)
  5197. { die "Invalid prefix length syntax in IP address: [$ip]\n" }
  5198. elsif ($ip_len > 128)
  5199. { die "IPv6 network prefix length greater than 128: [$ip]\n" }
  5200. my $mask = pack('B128', '1' x $ip_len);
  5201. # do_log(5, "ip_to_vec: %s => %s/%d\n", $ip,unpack('B*',$vec),$ip_len);
  5202. ($vec,$mask,$ip_len);
  5203. }
  5204. use vars qw($ip_mapd_vec $ip_mapd_mask $ip_6to4_vec $ip_6to4_mask);
  5205. BEGIN {
  5206. ($ip_mapd_vec, $ip_mapd_mask) = ip_to_vec('::FFFF:0:0/96',1); # IPv4-mapped
  5207. ($ip_6to4_vec, $ip_6to4_mask) = ip_to_vec('2002::/16',1); # 6to4, RFC 3056
  5208. $ip_mapd_vec = $ip_mapd_vec & $ip_mapd_mask; # just in case
  5209. $ip_6to4_vec = $ip_6to4_vec & $ip_6to4_mask; # just in case
  5210. }
  5211. # strip an optional 'IPv6:' prefix, lowercase hex digits,
  5212. # convert an IPv4-mapped IPv6 address into a plain IPv4 dot-quad form;
  5213. # leave unchanged if syntactically incorrect
  5214. #
  5215. sub normalize_ip_addr($) {
  5216. my($ip) = @_;
  5217. my $have_ipv6;
  5218. if ($ip =~ s/^IPv6://i) { $have_ipv6 = 1 }
  5219. elsif ($ip =~ /:.*:/) { $have_ipv6 = 1 }
  5220. if ($have_ipv6 && $ip =~ /^[0:]+:ffff:/i) { # triage for IPv4-mapped
  5221. my($ip_vec,$ip_mask);
  5222. if (eval { ($ip_vec,$ip_mask) = ip_to_vec($ip,0); 1 }) { # valid IP addr
  5223. if (($ip_vec & $ip_mapd_mask) eq $ip_mapd_vec) { # IPv4-mapped?
  5224. my $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); # 32 bits
  5225. do_log(5, "IPv4-mapped: %s -> %s", $ip,$ip_dq);
  5226. $ip = $ip_dq;
  5227. }
  5228. }
  5229. }
  5230. lc $ip;
  5231. }
  5232. # lookup_ip_acl() performs a lookup for an IPv4 or IPv6 address against a list
  5233. # of lookup tables, each may be a constant, or a ref to an access control
  5234. # list or a ref to an associative array (hash) of network or host addresses.
  5235. #
  5236. # IP address is compared to each member of an access list in turn,
  5237. # the first match wins (terminates the search), and its value decides
  5238. # whether the result is true (yes, permit, pass) or false (no, deny, drop).
  5239. # Falling through without a match produces a false (undef).
  5240. #
  5241. # The presence of a character '!' prepended to a list member decides
  5242. # whether the result will be true (without a '!') or false (with '!')
  5243. # in case this list member matches and terminates the search.
  5244. #
  5245. # Because search stops at the first match, it only makes sense
  5246. # to place more specific patterns before the more general ones.
  5247. #
  5248. # For IPv4 a network address can be specified in classless notation
  5249. # n.n.n.n/k, or using a mask n.n.n.n/m.m.m.m . Missing mask implies /32,
  5250. # i.e. a host address. For IPv6 addresses all RFC 3513 forms are allowed.
  5251. # See also comments at ip_to_vec().
  5252. #
  5253. # Although not a special case, it is good to remember that '::/0'
  5254. # always matches any IPv4 or IPv6 address (even syntactically invalid address).
  5255. #
  5256. # The '0/0' is equivalent to '::FFFF:0:0/96' and matches any syntactically
  5257. # valid IPv4 address (including IPv4-mapped IPv6 addresses), but not other
  5258. # IPv6 addresses!
  5259. #
  5260. # Example
  5261. # given: @acl = qw( !192.168.1.12 172.16.3.3 !172.16.3.0/255.255.255.0
  5262. # 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
  5263. # !0.0.0.0/8 !:: 127.0.0.0/8 ::1 );
  5264. # matches RFC 1918 private address space except host 192.168.1.12
  5265. # and net 172.16.3/24 (but host 172.16.3.3 within 172.16.3/24 still matches).
  5266. # In addition, the 'unspecified' (null, i.e. all zeros) IPv4 and IPv6
  5267. # addresses return false, and IPv4 and IPv6 loopback addresses match
  5268. # and return true.
  5269. #
  5270. # If the supplied lookup table is a hash reference, match a canonical
  5271. # IP address: dot-quad IPv4, or preferred IPv6 form, against hash keys.
  5272. # For IPv4 addresses a simple classful subnet specification is allowed in
  5273. # hash keys by truncating trailing bytes from the looked up IPv4 address.
  5274. # A syntactically invalid IP address cannot match any hash entry.
  5275. #
  5276. sub lookup_ip_acl($@) {
  5277. my($ip, @nets_ref) = @_;
  5278. my($ip_vec,$ip_mask); my $eval_stat;
  5279. eval { ($ip_vec,$ip_mask) = ip_to_vec($ip,0); 1 }
  5280. or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  5281. my($label,$fullkey,$result,$lookup_type); my $found = 0;
  5282. for my $tb (@nets_ref) {
  5283. my $t = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
  5284. if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches
  5285. my $r = ref($t) ? $$t : $t; # allow direct or indirect reference
  5286. $result = $r; $fullkey = "(constant:$r)"; $lookup_type = 'const';
  5287. $found=1 if defined $result;
  5288. } elsif (ref($t) eq 'HASH') {
  5289. $lookup_type = 'hash';
  5290. if (!defined $ip_vec) { # syntactically invalid IP address
  5291. $fullkey = undef; $result = $t->{$fullkey}; # only matches undef key
  5292. $found=1 if defined $result;
  5293. } else { # valid IP address
  5294. # match a canonical IP address: dot-quad IPv4, or preferred IPv6 form
  5295. my $ip_c; # IP address in a canonical form: x:x:x:x:x:x:x:x
  5296. $ip_c = join(':', map(sprintf('%04x',$_), unpack('n8',$ip_vec)));
  5297. if (($ip_vec & $ip_mapd_mask) ne $ip_mapd_vec) {
  5298. do_log(5, 'lookup_ip_acl keys: "%s"', $ip_c);
  5299. } else { # is an IPv4-mapped addr
  5300. my $ip_dq; # IPv4 in dotted-quad form
  5301. $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); # 32 bits
  5302. # try dot-quad, stripping off trailing bytes repeatedly
  5303. do_log(5, 'lookup_ip_acl keys: "%s", "%s"', $ip_dq, $ip_c);
  5304. for (my(@f)=split(/\./,$ip_dq); @f && !$found; $#f--) {
  5305. $fullkey = join('.',@f); $result = $t->{$fullkey};
  5306. $found=1 if defined $result;
  5307. }
  5308. }
  5309. # test for 6to4 too? not now
  5310. # if ($ip_vec & $ip_6to4_mask) eq $ip_6to4_vec) {
  5311. # # yields an IPv4 address of a client's 6to4 router
  5312. # $ip_dq = join('.', unpack('C4',substr($ip_vec,2,4)));
  5313. # }
  5314. if (!$found) { # try the 'preferred IPv6 form', lowercase hex letters
  5315. $fullkey = lc $ip_c; $result = $t->{$fullkey};
  5316. $found=1 if defined $result;
  5317. }
  5318. }
  5319. } elsif (ref($t) eq 'ARRAY') {
  5320. $lookup_type = 'array';
  5321. my($key,$acl_ip_vec,$acl_mask,$acl_mask_len); local($1,$2);
  5322. for my $net (@$t) {
  5323. $fullkey = $key = $net; $result = 1;
  5324. if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s)
  5325. $key = $2;
  5326. $result = 1 - $result if (length($1) & 1); # negate if odd
  5327. }
  5328. ($acl_ip_vec, $acl_mask, $acl_mask_len) = ip_to_vec($key,1);
  5329. if ($acl_mask_len == 0) { $found=1 } #even an invalid addr matches ::/0
  5330. elsif (!defined($ip_vec)) {} # no other matches for invalid address
  5331. elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found=1 }
  5332. last if $found;
  5333. }
  5334. } elsif ($t->isa('Net::Patricia::AF_INET6')) { # Patricia Trie
  5335. $lookup_type = 'patricia';
  5336. local($1,$2,$3,$4); local($_) = $ip;
  5337. $_ = $1 if /^ \[ ( [^\]]* ) \] \z/xs; # discard optional brackets
  5338. s/%[A-Z0-9:._-]+\z//si; # discard interface specification
  5339. if (m{^ (\d+) \. (\d+) \. (\d+) \. (\d+) \z}x) {
  5340. $_ = sprintf('::ffff:%d.%d.%d.%d', $1,$2,$3,$4);
  5341. } else {
  5342. s/^IPv6://i; # discard optional 'IPv6:' prefix
  5343. }
  5344. eval { $result = $t->match_string($_); 1 } or $result=undef;
  5345. if (defined $result) {
  5346. $fullkey = $result;
  5347. if ($fullkey =~ s/^!//) { $result = 0 }
  5348. else { $result = 1; $found = 1 }
  5349. }
  5350. } elsif ($t->isa('Amavis::Lookup::IP')) { # pre-parsed IP lookup array obj
  5351. $lookup_type = 'arr.obj';
  5352. my($acl_ip_vec, $acl_mask, $acl_mask_len);
  5353. for my $e (@$t) {
  5354. ($fullkey, $acl_ip_vec, $acl_mask, $acl_mask_len, $result) = @$e;
  5355. if ($acl_mask_len == 0) { $found=1 } #even an invalid addr matches ::/0
  5356. elsif (!defined($ip_vec)) {} # no other matches for invalid address
  5357. elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found=1 }
  5358. last if $found;
  5359. }
  5360. } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label
  5361. # just a convenience for logging purposes, not a real lookup method
  5362. $label = $t->display; # grab the name, and proceed with the next table
  5363. } else {
  5364. die "TROUBLE: lookup table is an unknown object: " . ref($t);
  5365. }
  5366. last if $found;
  5367. }
  5368. $fullkey = $result = undef if !$found;
  5369. if ($label ne '') { $label = " ($label)" }
  5370. ll(4) && do_log(4, 'lookup_ip_acl%s %s: key="%s"%s',
  5371. $label, $lookup_type, $ip,
  5372. !$found ? ", no match"
  5373. : " matches \"$fullkey\", result=$result");
  5374. if (defined $eval_stat) {
  5375. chomp $eval_stat;
  5376. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  5377. $eval_stat = "lookup_ip_acl$label: $eval_stat";
  5378. do_log(2, "%s", $eval_stat);
  5379. }
  5380. !wantarray ? $result : ($result, $fullkey, $eval_stat);
  5381. }
  5382. # create a pre-parsed object from a list of IP networks,
  5383. # which may be used as an argument to lookup_ip_acl to speed up its searches
  5384. #
  5385. sub new($@) {
  5386. my($class,@nets) = @_;
  5387. my $build_patricia_trie = $have_patricia && (@nets > 20);
  5388. if (!$build_patricia_trie) {
  5389. # build a traditional pre-parsed search list for a small number of entries
  5390. my(@list); local($1,$2);
  5391. for my $net (@nets) {
  5392. my $key = $net; my $result = 1;
  5393. if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s)
  5394. $key = $2;
  5395. $result = 1 - $result if (length($1) & 1); # negate if odd
  5396. }
  5397. my($ip_vec, $ip_mask, $ip_mask_len) = ip_to_vec($key,1);
  5398. push(@list, [$net, $ip_vec, $ip_mask, $ip_mask_len, $result]);
  5399. }
  5400. return bless(\@list, $class);
  5401. } else {
  5402. # build a patricia trie, it offers more efficient searching in large sets
  5403. my $pt = Net::Patricia->new(&AF_INET6);
  5404. do_log(5, "building a patricia trie out of %d nets", scalar(@nets));
  5405. for my $net (@nets) {
  5406. local $_ = $net;
  5407. local($1,$2,$3,$4); my $masklen;
  5408. if (s{ / ([0-9.]+) \z }{}x) {
  5409. $masklen = $1;
  5410. $masklen =~ /^\d{1,3}\z/
  5411. or die "Network mask not supported, use a CIDR syntax: $net";
  5412. }
  5413. s/^!//; # strip a negation from a key, it will be retained in data
  5414. $_ = $1 if /^ \[ ( [^\]]* ) \] \z/xs; # discard optional brackets
  5415. s/%[A-Z0-9:._-]+\z//si; # discard interface specification
  5416. if (/^ \d+ (?: \. | \z) /x) { # triage for an IPv4 network address
  5417. if (/^ (\d+) \. (\d+) \. (\d+) \. (\d+) \z/x) {
  5418. $_ = sprintf('::ffff:%d.%d.%d.%d', $1,$2,$3,$4);
  5419. $masklen = 32 if !defined $masklen;
  5420. } elsif (/^ (\d+) \. (\d+) \. (\d+) \.? \z/x) {
  5421. $_ = sprintf('::ffff:%d.%d.%d.0', $1,$2,$3);
  5422. $masklen = 24 if !defined $masklen;
  5423. } elsif (/^ (\d+) \. (\d+) \.? \z/x) {
  5424. $_ = sprintf('::ffff:%d.%d.0.0', $1,$2);
  5425. $masklen = 16 if !defined $masklen;
  5426. } elsif (/^ (\d+) \.? \z/x) {
  5427. $_ = sprintf('::ffff:%d.0.0.0', $1);
  5428. $masklen = 8 if !defined $masklen;
  5429. }
  5430. $masklen += 96 if defined $masklen;
  5431. } else { # looks like an IPv6 network
  5432. s/^IPv6://i; # discard optional 'IPv6:' prefix
  5433. }
  5434. $masklen = 128 if !defined $masklen;
  5435. $_ .= '/' . $masklen;
  5436. eval { $pt->add_string($_, $net); 1 }
  5437. or die "Adding a network $net to a patricia trie failed: $@";
  5438. }
  5439. # ll(5) && $pt->climb(sub { do_log(5,"patricia trie, node $_[0]") });
  5440. return $pt; # a Net::Patricia::AF_INET6 object
  5441. }
  5442. }
  5443. 1;
  5444. #
  5445. package Amavis::Lookup::Opaque;
  5446. use strict;
  5447. use re 'taint';
  5448. # Make an object out of the supplied argument, pretocting it
  5449. # from being interpreted as an acl- or a hash- type lookup.
  5450. #
  5451. sub new($$) { my($class,$obj) = @_; bless \$obj, $class }
  5452. sub get($) { ${$_[0]} }
  5453. 1;
  5454. #
  5455. package Amavis::Lookup::OpaqueRef;
  5456. use strict;
  5457. use re 'taint';
  5458. # Make an object out of the supplied argument, pretocting it
  5459. # from being interpreted as an acl- or a hash- type lookup.
  5460. # The argument to new() is expected to be a ref to a variable,
  5461. # which will be dereferenced by a method get().
  5462. #
  5463. sub new($$) { my($class,$obj) = @_; bless \$obj, $class }
  5464. sub get($) { ${${$_[0]}} }
  5465. 1;
  5466. #
  5467. package Amavis::Lookup::Label;
  5468. use strict;
  5469. use re 'taint';
  5470. # Make an object out of the supplied string, to serve as label
  5471. # in log messages generated by sub lookup
  5472. #
  5473. sub new($$) { my($class,$str) = @_; bless \$str, $class }
  5474. sub display($) { ${$_[0]} }
  5475. 1;
  5476. #
  5477. package Amavis::Lookup::SQLfield;
  5478. use strict;
  5479. use re 'taint';
  5480. sub new($$$;$$) {
  5481. my($class, $sql_query, $fieldname, $fieldtype, $implied_args) = @_;
  5482. my $self =
  5483. bless { fieldname => $fieldname, fieldtype => $fieldtype }, $class;
  5484. $self->{sql_query} = $sql_query if defined $sql_query;
  5485. $self->{args} = ref($implied_args) eq 'ARRAY' ? [@$implied_args] # copy
  5486. : [$implied_args] if defined $implied_args;
  5487. $self;
  5488. }
  5489. 1;
  5490. #
  5491. package Amavis::Lookup::LDAPattr;
  5492. use strict;
  5493. use re 'taint';
  5494. sub new($$$;$) {
  5495. my($class, $ldap_query, $attrname, $attrtype) = @_;
  5496. my $self =
  5497. bless { attrname => $attrname, attrtype => $attrtype }, $class;
  5498. $self->{ldap_query} = $ldap_query if defined $ldap_query;
  5499. $self;
  5500. }
  5501. 1;
  5502. #
  5503. package Amavis::Lookup;
  5504. use strict;
  5505. use re 'taint';
  5506. BEGIN {
  5507. require Exporter;
  5508. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  5509. $VERSION = '2.316';
  5510. @ISA = qw(Exporter);
  5511. @EXPORT_OK = qw(&lookup &lookup2 &lookup_hash &lookup_acl);
  5512. import Amavis::Util qw(ll do_log fmt_struct unique_list);
  5513. import Amavis::Conf qw(:platform c cr ca);
  5514. import Amavis::Timing qw(section_time);
  5515. import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys);
  5516. }
  5517. use subs @EXPORT_OK;
  5518. # lookup_hash() performs a lookup for an e-mail address against a hash map.
  5519. # If a match is found (a hash key exists in the Perl hash) the function returns
  5520. # whatever the map returns, otherwise undef is returned. First match wins,
  5521. # aborting further search sequence.
  5522. #
  5523. sub lookup_hash($$;$%) {
  5524. my($addr, $hash_ref,$get_all,%options) = @_;
  5525. ref($hash_ref) eq 'HASH'
  5526. or die "lookup_hash: arg2 must be a hash ref: $hash_ref";
  5527. local($1,$2,$3,$4); my(@matchingkey,@result); my $append_string;
  5528. $append_string = $options{AppendStr} if defined $options{AppendStr};
  5529. my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1,$append_string);
  5530. for my $key (@$keys_ref) { # do the search
  5531. if (exists $$hash_ref{$key}) { # got it
  5532. push(@result,$$hash_ref{$key}); push(@matchingkey,$key);
  5533. last if !$get_all;
  5534. }
  5535. }
  5536. # do the right-hand side replacements if any $n, ${n} or $(n) is specified
  5537. for my $r (@result) { # $r is just an alias to array elements
  5538. if (defined($r) && !ref($r) && index($r,'$') >= 0) { # plain string with $
  5539. my $any = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
  5540. { my $j = $2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse;
  5541. # bring taintedness of input to the result
  5542. $r .= substr($addr,0,0) if $any;
  5543. }
  5544. }
  5545. if (!ll(5)) {
  5546. # only bother with logging when needed
  5547. } elsif (!@result) {
  5548. do_log(5,"lookup_hash(%s), no matches", $addr);
  5549. } elsif (!$get_all) { # first match wins
  5550. do_log(5,'lookup_hash(%s) matches key "%s", result=%s',
  5551. $addr, $matchingkey[0], !defined($result[0])?'undef':$result[0]);
  5552. } else { # want all matches
  5553. do_log(5,"lookup_hash(%s) matches keys: %s", $addr,
  5554. join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])}
  5555. (0..$#result)) );
  5556. }
  5557. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  5558. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  5559. }
  5560. # lookup_acl() performs a lookup for an e-mail address against
  5561. # access control list.
  5562. #
  5563. # The supplied e-mail address is compared with each member of the
  5564. # lookup list in turn, the first match wins (terminates the search),
  5565. # and its value decides whether the result is true (yes, permit, pass)
  5566. # or false (no, deny, drop). Falling through without a match produces
  5567. # false (undef). Search is always case-insensitive on domain part,
  5568. # local part matching depends on $localpart_is_case_sensitive setting.
  5569. #
  5570. # NOTE: lookup_acl is not aware of address extensions and they are
  5571. # not handled specially!
  5572. #
  5573. # If a list element contains a '@', the full e-mail address is compared,
  5574. # otherwise if a list element has a leading dot, the domain name part is
  5575. # matched only, and the domain as well as its subdomains can match. If there
  5576. # is no leading dot, the domain must match exactly (subdomains do not match).
  5577. #
  5578. # The presence of a character '!' prepended to a list element decides
  5579. # whether the result will be true (without a '!') or false (with '!')
  5580. # in case where this list element matches and terminates the search.
  5581. #
  5582. # Because search stops at the first match, it only makes sense
  5583. # to place more specific patterns before the more general ones.
  5584. #
  5585. # Although not a special case, it is good to remember that '.' always matches,
  5586. # so a '.' would stop the search and return true, whereas '!.' would stop the
  5587. # search and return false (0).
  5588. #
  5589. # Examples:
  5590. #
  5591. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  5592. # 'me.ac.uk' matches me.ac.uk, returns true and search stops
  5593. #
  5594. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  5595. # 'you.ac.uk' matches .ac.uk, returns false (because of '!') and search stops
  5596. #
  5597. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  5598. # 'them.co.uk' matches .uk, returns true and search stops
  5599. #
  5600. # given: @acl = qw( me.ac.uk !.ac.uk .uk )
  5601. # 'some.com' does not match anything, falls through and returns false (undef)
  5602. #
  5603. # given: @acl = qw( me.ac.uk !.ac.uk .uk !. )
  5604. # 'some.com' similar to previous, except it returns 0 instead of undef,
  5605. # which would only make a difference if this ACL is not the last argument
  5606. # in a call to lookup(), because a defined result stops further lookups
  5607. #
  5608. # given: @acl = qw( me.ac.uk !.ac.uk .uk . )
  5609. # 'some.com' matches catchall ".", and returns true. The ".uk" is redundant
  5610. #
  5611. # more complex example: @acl = qw(
  5612. # !The.Boss@dept1.xxx.com .dept1.xxx.com
  5613. # .dept2.xxx.com .dept3.xxx.com lab.dept4.xxx.com
  5614. # sub.xxx.com !.sub.xxx.com
  5615. # me.d.aaa.com him.d.aaa.com !.d.aaa.com .aaa.com
  5616. # );
  5617. #
  5618. sub lookup_acl($$%) {
  5619. my($addr, $acl_ref,%options) = @_;
  5620. ref($acl_ref) eq 'ARRAY'
  5621. or die "lookup_acl: arg2 must be a list ref: $acl_ref";
  5622. return if !@$acl_ref; # empty list can't match anything
  5623. my $lpcs = c('localpart_is_case_sensitive');
  5624. my($localpart,$domain) = split_address($addr); $domain = lc($domain);
  5625. $localpart = lc($localpart) if !$lpcs;
  5626. local($1,$2);
  5627. # chop off leading @ and trailing dots
  5628. $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
  5629. $domain .= $options{AppendStr} if defined $options{AppendStr};
  5630. my($matchingkey, $result); my $found = 0;
  5631. for my $e (@$acl_ref) {
  5632. $result = 1; $matchingkey = $e; my $key = $e;
  5633. if ($key =~ /^(!+)(.*)\z/s) { # starts with an exclamation mark(s)
  5634. $key = $2;
  5635. $result = 1-$result if length($1) & 1; # negate if odd
  5636. }
  5637. if ($key =~ /^(.*?)\@([^\@]*)\z/s) { # contains '@', check full address
  5638. $found=1 if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2);
  5639. } elsif ($key =~ /^\.(.*)\z/s) { # leading dot: domain or subdomain
  5640. my $key_t = lc($1);
  5641. $found=1 if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s;
  5642. } else { # match domain (but not its subdomains)
  5643. $found=1 if $domain eq lc($key);
  5644. }
  5645. last if $found;
  5646. }
  5647. $matchingkey = $result = undef if !$found;
  5648. ll(5) && do_log(5, 'lookup_acl(%s)%s', $addr,
  5649. (!$found ? ", no match"
  5650. : " matches key \"$matchingkey\", result=$result"));
  5651. !wantarray ? $result : ($result, $matchingkey);
  5652. }
  5653. # Perform a lookup for an e-mail address against any number of supplied maps:
  5654. # - SQL map,
  5655. # - LDAP map,
  5656. # - hash map (associative array),
  5657. # - (access control) list,
  5658. # - a list of regular expressions (an Amavis::Lookup::RE object),
  5659. # - a (defined) scalar always matches, and returns itself as the map value
  5660. # (useful as a catchall for a final 'pass' or 'fail');
  5661. # (see lookup_hash, lookup_acl, lookup_sql and lookup_ldap for details).
  5662. #
  5663. # when $get_all is 0 (the common usage):
  5664. # If a match is found (a defined value), returns whatever the map returns,
  5665. # otherwise returns undef. FIRST match aborts further search sequence.
  5666. # when $get_all is true:
  5667. # Collects a list of results from ALL matching tables, and within each
  5668. # table from ALL matching key. Returns a ref to a list of results
  5669. # (and a ref to a list of matching keys if returning a pair).
  5670. # The first element of both lists is supposed to be what lookup() would
  5671. # have returned if $get_all were 0. The order of returned elements
  5672. # corresponds to the order of the search.
  5673. #
  5674. # traditional API, deprecated
  5675. #
  5676. sub lookup($$@) {
  5677. my($get_all, $addr, @tables) = @_;
  5678. lookup2($get_all, $addr, \@tables);
  5679. }
  5680. # generalized API
  5681. #
  5682. sub lookup2($$$%) {
  5683. my($get_all, $addr, $tables_ref, %options) = @_;
  5684. (@_ - 3) % 2 == 0 or die "lookup2: options argument not in pairs (not hash)";
  5685. my($label, @result, @matchingkey);
  5686. for my $tb (!$tables_ref ? () : @$tables_ref) {
  5687. my $t = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
  5688. my $reft = ref($t);
  5689. if ($reft eq 'CODE') { # lazy evaluation
  5690. $t = &$t($addr,$get_all,%options);
  5691. $reft = ref($t);
  5692. }
  5693. if (!$reft || $reft eq 'SCALAR') { # a scalar always matches
  5694. my $r = $reft ? $$t : $t; # allow direct or indirect reference
  5695. if (defined $r) {
  5696. ll(5) && do_log(5, 'lookup: (scalar) matches, result="%s"', $r);
  5697. push(@result,$r); push(@matchingkey,"(constant:$r)");
  5698. }
  5699. } elsif ($reft eq 'HASH') {
  5700. my($r,$mk);
  5701. ($r,$mk) = lookup_hash($addr,$t,$get_all,%options) if %$t;
  5702. if (!defined $r) {}
  5703. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  5704. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  5705. } elsif ($reft eq 'ARRAY') {
  5706. my($r,$mk);
  5707. ($r,$mk) = lookup_acl($addr,$t,%options) if @$t;
  5708. if (defined $r) { push(@result,$r); push(@matchingkey,$mk) }
  5709. } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label
  5710. # just a convenience for logging purposes, not a real lookup method
  5711. $label = $t->display; # grab the name, and proceed with the next table
  5712. } elsif ($t->isa('Amavis::Lookup::Opaque') || # a structured constant
  5713. $t->isa('Amavis::Lookup::OpaqueRef')) { # ref to structured const
  5714. my $r = $t->get; # behaves like a constant pseudo-lookup
  5715. if (defined $r) {
  5716. ll(5) && do_log(5, 'lookup: (opaque) matches, result="%s"', $r);
  5717. push(@result,$r); push(@matchingkey,"(opaque:$r)");
  5718. }
  5719. } elsif ($t->isa('Amavis::Lookup::RE')) {
  5720. my($r,$mk);
  5721. ($r,$mk) = $t->lookup_re($addr,$get_all,%options) if @$t;
  5722. if (!defined $r) {}
  5723. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  5724. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  5725. } elsif ($t->isa('Amavis::Lookup::SQL')) {
  5726. my($r,$mk) = $t->lookup_sql($addr,$get_all,%options);
  5727. if (!defined $r) {}
  5728. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  5729. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  5730. } elsif ($t->isa('Amavis::Lookup::SQLfield')) {
  5731. if ($Amavis::sql_lookups) { # triage
  5732. my($r,$mk) = $t->lookup_sql_field($addr,$get_all,%options);
  5733. if (!defined $r) {}
  5734. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  5735. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  5736. }
  5737. } elsif ($t->isa('Amavis::Lookup::LDAP')) {
  5738. if ($Amavis::ldap_lookups && c('enable_ldap')) { # triage
  5739. my($r,$mk) = $t->lookup_ldap($addr,$get_all,%options);
  5740. if (!defined $r) {}
  5741. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  5742. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  5743. }
  5744. } elsif ($t->isa('Amavis::Lookup::LDAPattr')) {
  5745. if ($Amavis::ldap_lookups && c('enable_ldap')) { # triage
  5746. my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all,%options);
  5747. if (!defined $r) {}
  5748. elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
  5749. elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
  5750. }
  5751. } else {
  5752. die "TROUBLE: lookup table is an unknown object: " . $reft;
  5753. }
  5754. last if @result && !$get_all;
  5755. }
  5756. # pretty logging
  5757. if (ll(4)) { # only bother preparing log report which will be printed
  5758. my $opt_label = $options{Label};
  5759. my(@lbl) = grep(defined $_ && $_ ne '', ($opt_label,$label));
  5760. $label = ' [' . join(',',unique_list(\@lbl)) . ']' if @lbl;
  5761. if (!$tables_ref || !@$tables_ref) {
  5762. do_log(4, "lookup%s => undef, %s, no lookup tables",
  5763. $label, fmt_struct($addr));
  5764. } elsif (!@result) {
  5765. do_log(4, "lookup%s => undef, %s does not match",
  5766. $label, fmt_struct($addr));
  5767. } elsif (!$get_all) { # first match wins
  5768. do_log(4, 'lookup%s => %-6s %s matches, result=%s, matching_key="%s"',
  5769. $label, $result[0] ? 'true,' : 'false,',
  5770. fmt_struct($addr), fmt_struct($result[0]), $matchingkey[0]);
  5771. } else { # want all matches
  5772. do_log(4, 'lookup%s, %d matches for %s, results: %s',
  5773. $label, scalar(@result), fmt_struct($addr),
  5774. join(', ', map { sprintf('"%s"=>%s',
  5775. $matchingkey[$_], fmt_struct($result[$_]))
  5776. } (0 .. $#result) ));
  5777. }
  5778. }
  5779. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  5780. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  5781. }
  5782. 1;
  5783. #
  5784. package Amavis::Expand;
  5785. use strict;
  5786. use re 'taint';
  5787. BEGIN {
  5788. require Exporter;
  5789. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  5790. $VERSION = '2.316';
  5791. @ISA = qw(Exporter);
  5792. @EXPORT_OK = qw(&expand &tokenize);
  5793. import Amavis::Util qw(ll do_log);
  5794. }
  5795. use subs @EXPORT_OK;
  5796. # Given a string reference and a hashref of predefined (builtin) macros,
  5797. # expand() performs a macro expansion and returns a ref to a resulting string.
  5798. #
  5799. # This is a simple, yet fully fledged macro processor with proper lexical
  5800. # analysis, call stack, quoting levels, user supplied and builtin macros,
  5801. # three builtin flow-control macros: selector, regexp selector and iterator,
  5802. # a macro-defining macro and a macro '#' that eats input to the next newline.
  5803. # Also recognized are the usual \c and \nnn forms for specifying special
  5804. # characters, where c can be any of: r, n, f, b, e, a, t.
  5805. # Details are described in file README.customize, practical examples of use
  5806. # are in the supplied notification messages;
  5807. # Author: Mark Martinec <Mark.Martinec@ijs.si>, 2002, 2006
  5808. use vars qw(%builtins_cached %lexmap %esc);
  5809. use vars qw($lx_lb $lx_lbS $lx_lbT $lx_lbA $lx_lbC $lx_lbE $lx_lbQQ
  5810. $lx_rbQQ $lx_rb $lx_sep $lx_h $lx_ph);
  5811. BEGIN {
  5812. no warnings 'qw'; # avoid "Possible attempt to put comments in qw()"
  5813. my(@lx_str) = qw( [ [? [~ [@ [: [= [" "] ] | # %#
  5814. %0 %1 %2 %3 %4 %5 %6 %7 %8 %9); # lexical elem.
  5815. # %lexmap maps string to reference in order to protect lexels
  5816. $lexmap{$_} = \$_ for @lx_str; # maps lexel strings to references
  5817. ($lx_lb, $lx_lbS, $lx_lbT, $lx_lbA, $lx_lbC, $lx_lbE, $lx_lbQQ, $lx_rbQQ,
  5818. $lx_rb, $lx_sep, $lx_h, $lx_ph) = map($lexmap{$_}, @lx_str);
  5819. %esc = (n => \"\n", r => "\r", f => "\f", b => "\b",
  5820. e => "\e", a => "\a", t => "\t");
  5821. # NOTE that \n is specific, it is represented by a ref to a newline and not
  5822. # by a newline itself; this makes it possible for a macro '#' to skip input
  5823. # to a true newline from source, making it possible to comment-out entire
  5824. # lines even if they contain "\n" tokens
  5825. 1;
  5826. }
  5827. # make an object out of the supplied list of tokens
  5828. sub newmacro { my $class = shift; bless [@_], $class }
  5829. # turn a ref to a list of tokens into a single plain string
  5830. sub tokens_list_to_str($) { join('', map(ref($_) ? $$_ : $_, @{$_[0]})) }
  5831. sub tokenize($;$) {
  5832. my($str_ref,$tokens_ref) = @_; local($1);
  5833. $tokens_ref = [] if !defined $tokens_ref;
  5834. # parse lexically, replacing lexical element strings with references,
  5835. # unquoting backslash-quoted characters and %%, and dropping \NL and \_
  5836. @$tokens_ref = map {
  5837. exists $lexmap{$_} ? $lexmap{$_} # replace with ref
  5838. : $_ eq "\\\n" || $_ eq "\\_" ? '' # drop \NEWLINE and \_
  5839. : $_ eq '%%' ? '%' # %% -> %
  5840. : /^(%\#?.)\z/s ? \"$1" # unknown builtins
  5841. : /^\\([0-7]{1,3})\z/ ? chr(oct($1)) # \nnn
  5842. : /^\\(.)\z/s ? (exists($esc{$1}) ? $esc{$1} : $1) # \r, \n, \f, ...
  5843. : /^(_ [A-Z]+ (?: \( [^)]* \) )? _)\z/sx ? \"$1" # SpamAssassin-compatible
  5844. : $_ }
  5845. $$str_ref =~ /\G \# | \[ [?~\@:="]? | "\] | \] | \| | % \#? . | \\ [^0-7] |
  5846. \\ [0-7]{1,3} | _ [A-Z]+ (?: \( [^)]* \) )? _ |
  5847. [^\[\]\\|%\n#"_]+ | [^\n]+? | \n /gsx;
  5848. $tokens_ref;
  5849. }
  5850. sub evalmacro($$;@) {
  5851. my($macro_type,$builtins_href,@args) = @_;
  5852. my @result; local($1,$2);
  5853. if ($macro_type == $lx_lbS) { # selector built-in macro
  5854. my $sel = tokens_list_to_str(shift(@args));
  5855. if ($sel eq '') { $sel = 0 } # quick
  5856. elsif ($sel =~ /^\s*\z/) { $sel = 0 }
  5857. elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 } # decimal to numeric
  5858. else { $sel = 1 }
  5859. # provide an empty second alternative if we only have one specified
  5860. if (@args < 2) {} # keep $sel beyond $#args
  5861. elsif ($sel > $#args) { $sel = $#args } # use last alternative
  5862. @result = @{$args[$sel]} if $sel >= 0 && $sel <= $#args;
  5863. } elsif ($macro_type == $lx_lbT) { # regexp built-in macro
  5864. # args: string, regexp1, then1, regexp2, then2, ... regexpN, thenN[, else]
  5865. my $str = tokens_list_to_str(shift(@args)); # collect the first argument
  5866. my($match,@repl);
  5867. while (@args >= 2) { # at least a regexp and a 'then' argument still there
  5868. @repl = ();
  5869. my $regexp = tokens_list_to_str(shift(@args)); # collect a regexp arg
  5870. if ($regexp eq '') {
  5871. # braindamaged Perl: empty string implies the last successfully
  5872. # matched regular expression; we must avoid this
  5873. $match = 1;
  5874. } else {
  5875. eval { # guard against invalid regular expression
  5876. local($1,$2,$3,$4,$5,$6,$7,$8,$9);
  5877. $match = $str=~/$regexp/ ? 1 : 0;
  5878. @repl = ($1,$2,$3,$4,$5,$6,$7,$8,$9) if $match;
  5879. 1;
  5880. } or do {
  5881. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  5882. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  5883. do_log(2,"invalid macro regexp arg: %s", $eval_stat);
  5884. $match = 0; @repl = ();
  5885. };
  5886. }
  5887. if ($match) { last } else { shift(@args) } # skip 'then' arg if no match
  5888. }
  5889. if (@args > 0) {
  5890. unshift(@repl,$str); # prepend the whole string as a %0
  5891. # formal arg lexels %0, %1, ... %9 are replaced by captured substrings
  5892. @result = map(!ref || $$_!~/^%([0-9])\z/ ? $_ : $repl[$1], @{$args[0]});
  5893. }
  5894. } elsif ($macro_type == $lx_lb) { # iterator macro
  5895. my($cvar_r,$sep_r,$body_r); my $cvar; # give meaning to arguments
  5896. if (@args >= 3) { ($cvar_r,$body_r,$sep_r) = @args }
  5897. else { ($body_r,$sep_r) = @args; $cvar_r = $body_r }
  5898. # find the iterator name
  5899. for (@$cvar_r) { if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last } }
  5900. my $name = $cvar; # macro name is usually the same as the iterator name
  5901. if (@args >= 3 && !defined($name)) {
  5902. # instead of iterator like %x, the first arg may be a long macro name,
  5903. # in which case the iterator name becomes a hard-wired 'x'
  5904. $name = tokens_list_to_str($cvar_r);
  5905. $name =~ s/^[ \t\n]+//; $name =~ s/[ \t\n]+\z//; # trim whitespace
  5906. if ($name eq '') { $name = undef } else { $cvar = 'x' }
  5907. }
  5908. if (exists($builtins_href->{$name})) {
  5909. my $s = $builtins_href->{$name};
  5910. if (ref($s) eq 'Amavis::Expand') { # expand a dynamically defined macro
  5911. my(@margs) = ($name); # no arguments beyond %0
  5912. my(@res) = map(!ref || $$_ !~ /^%([0-9])\z/ ? $_
  5913. : ref($margs[$1]) ? @{$margs[$1]} : (), @$s);
  5914. $s = tokens_list_to_str(\@res);
  5915. } elsif (ref($s) eq 'CODE') {
  5916. if (exists($builtins_cached{$name})) {
  5917. $s = $builtins_cached{$name};
  5918. } else {
  5919. while (ref($s) eq 'CODE') { $s = &$s($name) }
  5920. $builtins_cached{$name} = $s;
  5921. }
  5922. }
  5923. my $ind = 0;
  5924. for my $val (ref($s) ? @$s : $s) { # do substitutions in the body
  5925. push(@result, @$sep_r) if ++$ind > 1 && ref($sep_r);
  5926. push(@result, map(ref && $$_ eq "%$cvar" ? $val : $_, @$body_r));
  5927. }
  5928. }
  5929. } elsif ($macro_type == $lx_lbE) { # define a new macro
  5930. my $name = tokens_list_to_str(shift(@args)); # first arg is a macro name
  5931. $name =~ s/^[ \t\n]+//; $name =~ s/[ \t\n]+\z//; # trim whitespace on name
  5932. delete $builtins_cached{$name};
  5933. $builtins_href->{$name} = Amavis::Expand->newmacro(@{$args[0]});
  5934. } elsif ($macro_type == $lx_lbA || $macro_type == $lx_lbC || # macro call
  5935. $$macro_type =~ /^%(\#)?(.)\z/s) {
  5936. my $name; my $cardinality_only = 0;
  5937. if ($macro_type == $lx_lbA || $macro_type == $lx_lbC) {
  5938. $name = tokens_list_to_str($args[0]); # arg %0 is a macro name
  5939. $name =~ s/^[ \t\n]+//; $name =~ s/[ \t\n]+\z//; # trim whitespace
  5940. } else { # simple macro call %x or %#x
  5941. $name = $2;
  5942. $cardinality_only = 1 if defined $1;
  5943. }
  5944. my $s = $builtins_href->{$name};
  5945. if (!ref($s)) { # macro expands to a plain string
  5946. if (!$cardinality_only) { @result = $s }
  5947. else { @result = $s !~ /^\s*\z/ ? 1 : 0 }; # %#x => nonwhite=1, other 0
  5948. } elsif (ref($s) eq 'Amavis::Expand') { # dynamically defined macro
  5949. $args[0] = $name; # replace name with a stringified and trimmed form
  5950. # expanding a dynamically-defined macro produces a list of tokens;
  5951. # formal argument lexels %0, %1, ... %9 are replaced by actual arguments
  5952. @result = map(!ref || $$_ !~ /^%([0-9])\z/ ? $_
  5953. : ref($args[$1]) ? @{$args[$1]} : (), @$s);
  5954. if ($cardinality_only) { # macro call form %#x
  5955. @result = tokens_list_to_str(\@result) !~ /^\s*\z/ ? 1 : 0;
  5956. }
  5957. } else { # subroutine or array ref
  5958. if (ref($s) eq 'CODE') {
  5959. if (exists($builtins_cached{$name}) && @args <= 1) {
  5960. $s = $builtins_cached{$name};
  5961. } elsif (@args <= 1) {
  5962. while (ref($s) eq 'CODE') { $s = &$s($name) } # callback
  5963. $builtins_cached{$name} = $s;
  5964. } else {
  5965. shift(@args); # discard original form of a macro name
  5966. while (ref($s) eq 'CODE') # subroutine callback
  5967. { $s = &$s($name, map(tokens_list_to_str($_), @args)) }
  5968. }
  5969. }
  5970. if ($cardinality_only) { # macro call form %#x
  5971. # for array: number of elements; for scalar: nonwhite=1, other 0
  5972. @result = ref($s) ? scalar(@$s) : $s !~ /^\s*\z/ ? 1 : 0;
  5973. } else { # macro call %x evaluates to the value of macro x
  5974. @result = ref($s) ? join(', ',@$s) : $s;
  5975. }
  5976. }
  5977. }
  5978. \@result;
  5979. }
  5980. sub expand($$) {
  5981. my $str_ref = shift; # a ref to a source string to be macro expanded;
  5982. my $builtins_href = shift; # a hashref, mapping builtin macro names
  5983. # to macro values: strings or array refs
  5984. my(@tokens);
  5985. if (ref($str_ref) eq 'ARRAY') { @tokens = @$str_ref }
  5986. else { tokenize($str_ref,\@tokens) }
  5987. my $call_level = 0; my $quote_level = 0;
  5988. my(@arg); # stack of arguments lists to nested calls, [0] is top of stack
  5989. my(@macro_type); # call stack of macro types (leading lexels) of nested calls
  5990. my(@implied_q); # call stack: is implied quoting currently active?
  5991. # 0 (not active) or 1 (active); element [0] stack top
  5992. my(@open_quote); # quoting stack: opening quote lexel for each quoting level
  5993. %builtins_cached = (); my $whereto; local($1,$2);
  5994. # preallocate some storage
  5995. my $output_str = ''; vec($output_str,2048,8) = 0; $output_str = '';
  5996. while (@tokens) {
  5997. my $t = shift(@tokens);
  5998. # do_log(5, "TOKEN: %s", ref($t) ? "<$$t>" : "'$t'");
  5999. if (!ref($t)) { # a plain string, no need to check for quoting levels
  6000. if (defined $whereto) { push(@$whereto,$t) } else { $output_str .= $t }
  6001. } elsif ($quote_level > 0 && substr($$t,0,1) eq '[') {
  6002. # go even deeper into quoting
  6003. $quote_level += ($t == $lx_lbQQ) ? 2 : 1; unshift(@open_quote,$t);
  6004. if (defined $whereto) { push(@$whereto,$t) } else { $output_str .= $$t }
  6005. } elsif ($t == $lx_lbQQ) { # just entering a [" ... "] quoting context
  6006. $quote_level += 2; unshift(@open_quote,$t);
  6007. # drop a [" , thus stripping one level of quotes
  6008. } elsif (substr($$t,0,1) eq '[') {
  6009. # $lx_lb $lx_lbS lx_lbT $lx_lbA $lx_lbC $lx_lbE
  6010. $call_level++; # open a macro call, start collecting arguments
  6011. unshift(@arg, [[]]); unshift(@macro_type, $t); unshift(@implied_q, 0);
  6012. $whereto = $arg[0][0];
  6013. if ($t == $lx_lb) { # iterator macro implicitly quotes all arguments
  6014. $quote_level++; unshift(@open_quote,$t); $implied_q[0] = 1;
  6015. }
  6016. } elsif ($quote_level <= 1 && $call_level>0 && $t == $lx_sep) { # next arg
  6017. unshift(@{$arg[0]}, []); $whereto = $arg[0][0];
  6018. if ($macro_type[0]==$lx_lbS && @{$arg[0]} == 2) {
  6019. # selector macro implicitly quotes arguments beyond first argument
  6020. $quote_level++; unshift(@open_quote,$macro_type[0]); $implied_q[0] = 1;
  6021. }
  6022. } elsif ($quote_level > 1 && ($t == $lx_rb || $t == $lx_rbQQ)) {
  6023. $quote_level -= ($open_quote[0] == $lx_lbQQ) ? 2 : 1;
  6024. shift(@open_quote); # pop the quoting stack
  6025. if ($t == $lx_rb || $quote_level > 0) { # pass-on if still quoted
  6026. if (defined $whereto) { push(@$whereto,$t) } else { $output_str .= $$t}
  6027. }
  6028. } elsif ($call_level > 0 && ($t == $lx_rb || $t == $lx_rbQQ)) { # evaluate
  6029. $call_level--; my $m_type = $macro_type[0];
  6030. if ($t == $lx_rbQQ) { # fudge for compatibility: treat "] as two chars
  6031. if (defined $whereto) { push(@$whereto,'"') } else { $output_str.='"' }
  6032. }
  6033. if ($implied_q[0] && $quote_level > 0) {
  6034. $quote_level -= ($open_quote[0] == $lx_lbQQ) ? 2 : 1;
  6035. shift(@open_quote); # pop the quoting stack
  6036. }
  6037. my $result_ref = evalmacro($m_type, $builtins_href, reverse @{$arg[0]});
  6038. shift(@macro_type); shift(@arg); shift(@implied_q); # pop the call stack
  6039. $whereto = $call_level > 0 ? $arg[0][0] : undef;
  6040. if ($m_type == $lx_lbC) { # neutral macro call, result implicitly quoted
  6041. if (defined $whereto) { push(@$whereto, @$result_ref) }
  6042. else { $output_str .= tokens_list_to_str($result_ref) }
  6043. } else { # active macro call, push result back to input for reprocessing
  6044. unshift(@tokens, @$result_ref);
  6045. }
  6046. } elsif ($quote_level > 0 ) { # still protect %x and # macro calls
  6047. if (defined $whereto) { push(@$whereto,$t) } else { $output_str .= $$t }
  6048. } elsif ($t == $lx_h) { # discard tokens up to and including a newline
  6049. while (@tokens) { last if shift(@tokens) eq "\n" }
  6050. } elsif ($$t =~ /^%\#?.\z/s) { # neutral simple macro call %x or %#x
  6051. my $result_ref = evalmacro($t, $builtins_href);
  6052. if (defined $whereto) { push(@$whereto,@$result_ref) }
  6053. # else { $output_str .= tokens_list_to_str($result_ref) }
  6054. else { $output_str .= join('', map(ref($_) ? $$_ : $_, @$result_ref)) }
  6055. } elsif ($$t =~ /^_ ([A-Z]+) (?: \( ( [^)]* ) \) )? _\z/sx) {
  6056. # neutral simple SA-like macro call, $1 is name, $2 is a single! argument
  6057. my $result_ref = evalmacro($lx_lbC, $builtins_href, [$1],
  6058. !defined($2) ? () : [$2] );
  6059. if (defined $whereto) { push(@$whereto, @$result_ref) }
  6060. else { $output_str .= tokens_list_to_str($result_ref) }
  6061. } else { # misplaced top-level lexical element
  6062. if (defined $whereto) { push(@$whereto,$t) } else { $output_str .= $$t }
  6063. }
  6064. }
  6065. %builtins_cached = (); # clear memory
  6066. \$output_str;
  6067. }
  6068. 1;
  6069. #
  6070. package Amavis::TempDir;
  6071. # Handles creation and cleanup of a persistent temporary directory,
  6072. # a file 'email.txt' therein, and a subdirectory 'parts'
  6073. use strict;
  6074. use re 'taint';
  6075. BEGIN {
  6076. require Exporter;
  6077. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  6078. $VERSION = '2.316';
  6079. @ISA = qw(Exporter);
  6080. import Amavis::Conf qw(:platform :confvars c cr ca);
  6081. import Amavis::Timing qw(section_time);
  6082. import Amavis::Util qw(ll do_log do_log_safe add_entropy rmdir_recursively);
  6083. import Amavis::rfc2821_2822_Tools qw(iso8601_timestamp);
  6084. }
  6085. use Errno qw(ENOENT EACCES EEXIST);
  6086. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  6087. use File::Temp ();
  6088. sub new {
  6089. my($class) = @_;
  6090. my $self = bless {}, $class;
  6091. $self->{tempdir_path} = undef;
  6092. undef $self->{tempdir_dev}; undef $self->{tempdir_ino};
  6093. undef $self->{fh_pers}; undef $self->{fh_dev}; undef $self->{fh_ino};
  6094. $self->{empty} = 1; $self->{preserve} = 0;
  6095. $self;
  6096. }
  6097. sub path { # path to a temporary directory
  6098. my $self=shift; !@_ ? $self->{tempdir_path} : ($self->{tempdir_path}=shift)
  6099. }
  6100. sub fh { # email.txt file handle
  6101. my $self=shift; !@_ ? $self->{fh_pers} : ($self->{fh_pers}=shift);
  6102. }
  6103. sub empty { # whether the directory is empty
  6104. my $self=shift; !@_ ? $self->{empty} : ($self->{empty}=shift)
  6105. }
  6106. sub preserve { # whether to preserve directory when current task is done
  6107. my $self=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift);
  6108. }
  6109. # Clean up the tempdir on shutdown
  6110. #
  6111. sub DESTROY {
  6112. my $self = shift;
  6113. local($@,$!,$_); my $myactualpid = $$;
  6114. if (defined($my_pid) && $myactualpid != $my_pid) {
  6115. do_log_safe(5,"TempDir::DESTROY skip, clone [%s] (born as [%s])",
  6116. $myactualpid, $my_pid);
  6117. } else {
  6118. do_log_safe(5,"TempDir::DESTROY called");
  6119. eval {
  6120. # must step out of the directory which is about to be deleted,
  6121. # otherwise rmdir can fail (e.g. on Solaris)
  6122. chdir($TEMPBASE)
  6123. or do_log(-1,"TempDir::DESTROY can't chdir to %s: %s", $TEMPBASE, $!);
  6124. if ($self->{fh_pers}) {
  6125. $self->{fh_pers}->close
  6126. or do_log(-1,"Error closing temp file: %s", $!);
  6127. }
  6128. undef $self->{fh_pers};
  6129. my $dname = $self->{tempdir_path};
  6130. my $errn = !defined($dname) || $dname eq '' ? ENOENT
  6131. : lstat($dname) ? 0 : 0+$!;
  6132. if (defined($dname) && $errn != ENOENT) {
  6133. # this will not be included in the TIMING report,
  6134. # but it only occurs infrequently and doesn't take that long
  6135. if ($self->{preserve} && !$self->{empty}) {
  6136. do_log(-1,"TempDir removal: tempdir is to be PRESERVED: %s", $dname);
  6137. } else {
  6138. do_log(3, "TempDir removal: %s is being removed: %s%s",
  6139. $self->{empty} ? 'empty tempdir' : 'tempdir', $dname,
  6140. $self->{preserve} ? ', nothing to preserve' : '');
  6141. rmdir_recursively($dname);
  6142. }
  6143. };
  6144. 1;
  6145. } or do {
  6146. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  6147. do_log_safe(1,"TempDir removal: %s",$eval_stat);
  6148. };
  6149. }
  6150. }
  6151. # Creates a temporary directory, or checks that inode did not change on reuse
  6152. #
  6153. sub prepare_dir {
  6154. my($self) = @_;
  6155. my(@stat_list); my $errn; my $reuse = 0;
  6156. my $dname = $self->{tempdir_path};
  6157. if (defined $dname) { # hope to reuse existing directory
  6158. @stat_list = lstat($dname); $errn = @stat_list ? 0 : 0+$!;
  6159. if ($errn != ENOENT) {
  6160. $reuse = 1; # good, it exists, try reusing it
  6161. } else {
  6162. do_log(2,"TempDir::prepare_dir: directory %s no longer exists", $dname);
  6163. $self->{tempdir_path} = $dname = undef; $self->{empty} = 1;
  6164. }
  6165. }
  6166. if (!defined $dname) {
  6167. # invent a name of a temporary directory for this child
  6168. my $dirtemplate = sprintf("amavis-%s-%05d-XXXXXXXX",
  6169. iso8601_timestamp(time,1), $my_pid);
  6170. $dname = File::Temp::tempdir($dirtemplate, DIR => $TEMPBASE);
  6171. defined $dname && $dname ne ''
  6172. or die "Can't create a temporary directory $TEMPBASE/$dirtemplate: $!";
  6173. do_log(4,"TempDir::prepare_dir: created directory %s", $dname);
  6174. chmod(0750,$dname)
  6175. or die "Can't change protection on directory $dname: $!";
  6176. @stat_list = lstat($dname);
  6177. @stat_list or die "Failed to access directory $dname: $!";
  6178. $self->{tempdir_path} = $dname;
  6179. ($self->{tempdir_dev}, $self->{tempdir_ino}) = @stat_list;
  6180. $self->{empty} = 1; add_entropy($dname, @stat_list);
  6181. section_time('mkdir tempdir');
  6182. }
  6183. $errn = @stat_list ? 0 : 0+$!;
  6184. if ($errn != 0) {
  6185. die "TempDir::prepare_dir: Can't access temporary directory $dname: $!";
  6186. } elsif (! -d _) { # exists, but is not a directory !?
  6187. die "TempDir::prepare_dir: $dname is not a directory!!!";
  6188. } elsif ($reuse) { # existing directory
  6189. my($dev,$ino,$mode,$nlink) = @stat_list;
  6190. if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) {
  6191. do_log(-1,"TempDir::prepare_dir: %s is no longer the same directory!",
  6192. $dname);
  6193. ($self->{tempdir_dev}, $self->{tempdir_ino}) = ($dev, $ino);
  6194. }
  6195. if ($nlink > 3) {
  6196. # when a directory's link count is > 2, it has "n-2" sub-directories;
  6197. # this does not apply to file systems like AFS, FAT, ISO-9660,
  6198. # but it also seems it does not apply to Mac OS 10 (Leopard)
  6199. do_log(5, "TempDir::prepare_dir: directory %s has %d subdirectories",
  6200. $dname, $nlink-2);
  6201. }
  6202. }
  6203. }
  6204. # Prepares the email.txt temporary file for writing (and reading later)
  6205. #
  6206. sub prepare_file {
  6207. my($self) = @_;
  6208. my $fname = $self->path . '/email.txt';
  6209. my(@stat_list) = lstat($fname); my $errn = @stat_list ? 0 : 0+$!;
  6210. if ($errn == ENOENT) { # no file
  6211. do_log(0,"TempDir::prepare_file: %s no longer exists, can't re-use it",
  6212. $fname) if $self->{fh_pers};
  6213. undef $self->{fh_pers};
  6214. } elsif ($errn != 0) { # some other error
  6215. undef $self->{fh_pers};
  6216. die "TempDir::prepare_file: can't access temporary file $fname: $!";
  6217. } elsif (! -f _) { # not a regular file !?
  6218. undef $self->{fh_pers};
  6219. die "TempDir::prepare_file: $fname is not a regular file!!!";
  6220. } elsif ($self->{fh_pers}) {
  6221. my($dev,$ino) = @stat_list;
  6222. if ($dev != $self->{file_dev} || $ino != $self->{file_ino}) {
  6223. # may happen if some user code has replaced the file, e.g. by altermime
  6224. undef $self->{fh_pers};
  6225. do_log(1,"TempDir::prepare_file: %s is no longer the same file, ".
  6226. "won't re-use it, deleting", $fname);
  6227. unlink($fname) or die "Can't remove file $fname: $!";
  6228. }
  6229. }
  6230. if ($self->{fh_pers} && !$can_truncate) { # just in case clean() retained it
  6231. undef $self->{fh_pers};
  6232. do_log(1,"TempDir::prepare_file: unable to truncate temporary file %s, ".
  6233. "deleting it", $fname);
  6234. unlink($fname) or die "Can't remove file $fname: $!";
  6235. }
  6236. if ($self->{fh_pers}) { # rewind and truncate existing file
  6237. $self->{fh_pers}->flush or die "Can't flush mail file: $!";
  6238. $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!";
  6239. $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!";
  6240. } else {
  6241. do_log(4,"TempDir::prepare_file: creating file %s", $fname);
  6242. # $^F == 2
  6243. # or do_log(-1,"TempDir::prepare_file: SYSTEM_FD_MAX not 2: %d", $^F);
  6244. my $newfh = IO::File->new;
  6245. # this can fail if a previous task of this process just recently stumbled
  6246. # on some error and preserved its evidence, not deleting a file email.txt
  6247. $newfh->open($fname, O_CREAT|O_EXCL|O_RDWR, 0640)
  6248. or die "Can't create file $fname: $!";
  6249. binmode($newfh,':bytes') or die "Can't cancel :utf8 mode on $fname: $!";
  6250. if (ll(5) && $] >= 5.008001) { # get_layers was added with Perl 5.8.1
  6251. my(@layers) = PerlIO::get_layers($newfh);
  6252. do_log(5,"TempDir::prepare_file: layers: %s", join(',',@layers));
  6253. }
  6254. $self->{fh_pers} = $newfh;
  6255. @stat_list = lstat($fname);
  6256. @stat_list or die "Failed to access temporary file $fname: $!";
  6257. add_entropy(@stat_list);
  6258. ($self->{file_dev}, $self->{file_ino}) = @stat_list;
  6259. section_time('create email.txt');
  6260. }
  6261. }
  6262. # Cleans the temporary directory for reuse, unless it is set to be preserved
  6263. #
  6264. sub clean {
  6265. my($self) = @_;
  6266. if ($self->{preserve} && !$self->{empty}) {
  6267. # keep evidence in case of trouble
  6268. do_log(-1,"PRESERVING EVIDENCE in %s", $self->{tempdir_path});
  6269. if ($self->{fh_pers}) {
  6270. $self->{fh_pers}->close or die "Error closing mail file: $!"
  6271. }
  6272. undef $self->{fh_pers}; $self->{tempdir_path} = undef; $self->{empty} = 1;
  6273. }
  6274. # cleanup, but leave directory (and file handle if possible) for reuse
  6275. if ($self->{fh_pers} && !$can_truncate) {
  6276. # truncate is not standard across all Unix variants,
  6277. # it is not Posix, but is XPG4-UNIX.
  6278. # So if we can't truncate a file and leave it open,
  6279. # we have to create it anew later, at some cost.
  6280. #
  6281. $self->{fh_pers}->close or die "Error closing mail file: $!";
  6282. undef $self->{fh_pers};
  6283. unlink($self->{tempdir_path}.'/email.txt')
  6284. or die "Can't delete file ".$self->{tempdir_path}."/email.txt: $!";
  6285. section_time('delete email.txt');
  6286. }
  6287. if (defined $self->{tempdir_path}) { # prepare for the next one
  6288. $self->strip; $self->{empty} = 1;
  6289. }
  6290. $self->{preserve} = 0; # reset
  6291. }
  6292. # Remove files and subdirectories from the temporary directory, leaving only
  6293. # the directory itself, file email.txt, and empty subdirectory ./parts .
  6294. # Leaving directories for reuse can represent an important saving in time,
  6295. # as directory creation + deletion can be an expensive operation,
  6296. # requiring atomic file system operation, including flushing buffers
  6297. # to disk (depending on the file system in use).
  6298. #
  6299. sub strip {
  6300. my $self = shift;
  6301. my $dname = $self->{tempdir_path};
  6302. do_log(4, "TempDir::strip: %s", $dname);
  6303. # must step out of the directory which is about to be deleted,
  6304. # otherwise rmdir can fail (e.g. on Solaris)
  6305. chdir($TEMPBASE) or die "TempDir::strip: can't chdir to $TEMPBASE: $!";
  6306. my(@stat_list) = lstat($dname);
  6307. my $errn = @stat_list ? 0 : 0+$!;
  6308. if ($errn == ENOENT) {
  6309. do_log(-1,"TempDir::strip: directory %s no longer exists", $dname);
  6310. $self->{tempdir_path} = $dname = undef; $self->{empty} = 1;
  6311. } elsif ($errn != 0) {
  6312. die "TempDir::strip: error accessing directory $dname: $!";
  6313. } else {
  6314. my($dev,$ino) = @stat_list;
  6315. if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) {
  6316. do_log(-1,"TempDir::strip: %s is no longer the same directory!",
  6317. $dname);
  6318. ($self->{tempdir_dev}, $self->{tempdir_ino}) = ($dev, $ino);
  6319. }
  6320. # now deal with the 'parts' subdirectory
  6321. my $errn = lstat("$dname/parts") ? 0 : 0+$!;
  6322. if ($errn == ENOENT) {} # fine, no such directory
  6323. elsif ($errn!=0) { die "TempDir::strip: error accessing $dname/parts: $!" }
  6324. elsif ( -l _) { die "TempDir::strip: $dname/parts is a symbolic link" }
  6325. elsif (!-d _) { die "TempDir::strip: $dname/parts is not a directory" }
  6326. else { rmdir_recursively("$dname/parts", 1) }
  6327. $self->check; # check for any remains in the top directory just in case
  6328. }
  6329. 1;
  6330. }
  6331. # Checks tempdir after being cleaned.
  6332. # It may only contain subdirectory 'parts' and file email.txt, nothing else.
  6333. #
  6334. sub check {
  6335. my $self = shift;
  6336. my $eval_stat; my $dname = $self->{tempdir_path};
  6337. local(*DIR); opendir(DIR,$dname) or die "Can't open directory $dname: $!";
  6338. eval {
  6339. # avoid slurping the whole directory contents into memory
  6340. $! = 0; my $f;
  6341. while (defined($f = readdir(DIR))) {
  6342. next if $f eq '.' || $f eq '..';
  6343. my $fname = $dname . '/' . $f;
  6344. my(@stat_list) = lstat($fname);
  6345. my $errn = @stat_list ? 0 : 0+$!;
  6346. if ($errn) {
  6347. die "Inaccessible $fname: $!";
  6348. } elsif (-f _) {
  6349. warn "Unexpected file $fname" if $f ne 'email.txt';
  6350. } elsif (-l _) {
  6351. die "Unexpected link $fname";
  6352. } elsif (-d _) {
  6353. my $nlink = $stat_list[3];
  6354. if ($f ne 'parts') {
  6355. die "Unexpected directory $fname";
  6356. } elsif ($nlink > 2) { # number of hard links
  6357. # when a directory's link count is > 2, it has "n-2" sub-directories;
  6358. # this does not apply to file systems like AFS, FAT, ISO-9660,
  6359. # but it also seems it does not apply to Mac OS 10 (Leopard)
  6360. do_log(5, "TempDir::check: directory %s has %d subdirectories",
  6361. $dname, $nlink-2);
  6362. }
  6363. } else {
  6364. die "Unexpected non-regular file $fname";
  6365. }
  6366. }
  6367. # checking status on directory read ops doesn't work as expected, Perl bug
  6368. # $! == 0 or die "Error reading directory $dname: $!";
  6369. 1;
  6370. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  6371. closedir(DIR) or die "Error closing directory $dname: $!";
  6372. if (defined $eval_stat) {
  6373. chomp $eval_stat;
  6374. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  6375. die "TempDir::check: $eval_stat\n";
  6376. }
  6377. 1;
  6378. }
  6379. 1;
  6380. #
  6381. package Amavis::IO::FileHandle;
  6382. # Provides a virtual file (a filehandle tie - a TIEHANDLE) representing
  6383. # a view to a mail message (accessed on an open file handle) prefixed by
  6384. # a couple of synthesized mail header fields supplied as an array of lines.
  6385. use strict;
  6386. use re 'taint';
  6387. use Errno qw(EAGAIN);
  6388. sub new { shift->TIEHANDLE(@_) }
  6389. sub TIEHANDLE {
  6390. my $class = shift;
  6391. my $self = bless { 'fileno' => undef }, $class;
  6392. if (@_) { $self->OPEN(@_) or return }
  6393. $self;
  6394. }
  6395. sub UNTIE {
  6396. my($self,$count) = @_;
  6397. $self->CLOSE if !$count && defined $self->FILENO;
  6398. 1;
  6399. }
  6400. sub DESTROY {
  6401. my $self = $_[0]; local($@,$!,$_);
  6402. $self->CLOSE if defined $self->FILENO;
  6403. 1;
  6404. }
  6405. sub BINMODE { 1 }
  6406. sub FILENO { my $self = $_[0]; $self->{'fileno'} }
  6407. sub CLOSE { my $self = $_[0]; undef $self->{'fileno'}; 1 }
  6408. sub EOF { my $self = $_[0]; defined $self->{'fileno'} ? $self->{'eof'} : 1 }
  6409. # creates a view on an already open file, prepended by some text
  6410. #
  6411. sub OPEN {
  6412. my($self, $filehandle,$prefix_lines_ref,$size_limit) = @_;
  6413. # $filehandle is a fh of an already open file;
  6414. # $prefix_lines_ref is a ref to an array of lines, to be prepended
  6415. # to a created view on an existing file; these lines must each
  6416. # be terminated by a \n, and must not include other \n characters
  6417. $self->CLOSE if defined $self->FILENO;
  6418. $self->{'fileno'} = 9999; $self->{'eof'} = 0;
  6419. $self->{'prefix'} = $prefix_lines_ref;
  6420. $self->{'prefix_n'} = 0; # number of lines of a prefix
  6421. $self->{'prefix_l'} = 0; # number of characters of a prefix
  6422. $self->{'pos'} = 0; $self->{'rec_ind'} = 0;
  6423. $self->{'size_limit'} = $size_limit; # pretend file ends at the byte limit
  6424. if (ref $prefix_lines_ref) {
  6425. my $len = 0;
  6426. $len += length($_) for @$prefix_lines_ref;
  6427. $self->{'prefix_l'} = $len;
  6428. $self->{'prefix_n'} = @$prefix_lines_ref;
  6429. }
  6430. $self->{'handle'} = $filehandle;
  6431. seek($filehandle, 0,0); # also provides a return value and errno
  6432. };
  6433. sub SEEK {
  6434. my($self,$offset,$whence) = @_;
  6435. $whence == 0 or die "Only absolute SEEK is supported on this file";
  6436. $offset == 0 or die "Only SEEK(0,0) is supported on this file";
  6437. $self->{'eof'} = 0; $self->{'pos'} = 0; $self->{'rec_ind'} = 0;
  6438. seek($self->{'handle'}, 0,0); # also provides a return value and errno
  6439. }
  6440. # sub TELL (not implemented)
  6441. # Returns the current position in bytes for FILEHANDLE, or -1 on error.
  6442. # mixing of READ and READLINE is not supported (without rewinding inbetween)
  6443. #
  6444. sub READLINE {
  6445. my($self) = @_;
  6446. my $size_limit = $self->{'size_limit'};
  6447. my $pos = $self->{'pos'};
  6448. if ($self->{'eof'}) {
  6449. return;
  6450. } elsif (defined $size_limit && $pos >= $size_limit) {
  6451. $self->{'eof'} = 1;
  6452. return;
  6453. } elsif (wantarray) { # return entire file as an array
  6454. my $rec_ind = $self->{'rec_ind'}; $self->{'eof'} = 1;
  6455. my $fh = $self->{'handle'};
  6456. if (!defined $size_limit) {
  6457. $self->{'rec_ind'} = $self->{'prefix_n'}; # just an estimate
  6458. $self->{'pos'} = $self->{'prefix_l'}; # just an estimate
  6459. if ($rec_ind >= $self->{'prefix_n'}) {
  6460. return readline($fh);
  6461. } elsif ($rec_ind == 0) { # common case: get the whole thing
  6462. return ( @{$self->{'prefix'}}, readline($fh) );
  6463. } else {
  6464. return ( @{$self->{'prefix'}}[ $rec_ind .. $#{$self->{'prefix'}} ],
  6465. readline($fh) );
  6466. }
  6467. } else { # take size limit into account
  6468. my(@array);
  6469. if ($rec_ind == 0) {
  6470. @array = @{$self->{'prefix'}};
  6471. } elsif ($rec_ind < $self->{'prefix_n'}) {
  6472. @array = @{$self->{'prefix'}}[ $rec_ind .. $#{$self->{'prefix'}} ];
  6473. }
  6474. for my $j (0..$#array) {
  6475. $pos += length($array[$j]);
  6476. if ($pos >= $size_limit) { # truncate at NL past limit
  6477. $#array = $j; last;
  6478. }
  6479. }
  6480. my $nread = 0;
  6481. if ($pos < $size_limit) {
  6482. my($inbuf,$carry); my $beyond_limit = 0;
  6483. while ( $nread=read($fh,$inbuf,16384) ) { # faster than line-by-line
  6484. if ($pos+$nread >= $size_limit) {
  6485. my $k = index($inbuf, "\n", # find a clean break at next NL
  6486. $pos >= $size_limit ? 0 : $size_limit-$pos);
  6487. $inbuf = substr($inbuf, 0, $k >= 0 ? $k+1 : $size_limit-$pos);
  6488. $beyond_limit = 1;
  6489. }
  6490. $pos += $nread;
  6491. my $k = $#array + 1; # insertion point
  6492. push(@array, split(/^/m, $inbuf, -1));
  6493. if (defined $carry) { $array[$k] = $carry.$array[$k]; $carry=undef }
  6494. $carry = pop(@array) if substr($array[-1],-1,1) ne "\n";
  6495. last if $beyond_limit;
  6496. }
  6497. push(@array,$carry) if defined $carry;
  6498. }
  6499. $self->{'rec_ind'} = $rec_ind + @array;
  6500. $self->{'pos'} = $pos;
  6501. if (!defined $nread) {
  6502. undef @array;
  6503. # errno should still be in $!, caller should be checking it
  6504. # die "error reading: $!";
  6505. }
  6506. return @array;
  6507. }
  6508. } else { # read one line
  6509. if ($self->{'rec_ind'} < $self->{'prefix_n'}) {
  6510. my $line = $self->{'prefix'}->[$self->{'rec_ind'}];
  6511. $self->{'rec_ind'}++; $self->{'pos'} += length($line);
  6512. return $line;
  6513. } else {
  6514. my $line = scalar(readline($self->{'handle'}));
  6515. if (!defined($line)) { $self->{'eof'} = 1 } # errno in $!
  6516. else { $self->{'rec_ind'}++; $self->{'pos'} += length($line) }
  6517. return $line;
  6518. }
  6519. }
  6520. }
  6521. # mixing of READ and READLINE is not supported (without rewinding inbetween)
  6522. #
  6523. sub READ { # SCALAR,LENGTH,OFFSET
  6524. my $self = shift; my $len = $_[1]; my $offset = $_[2];
  6525. my $str = ''; my $nbytes = 0;
  6526. my $pos = $self->{'pos'};
  6527. my $beyond_limit = 0;
  6528. my $size_limit = $self->{'size_limit'};
  6529. if (defined $size_limit && $pos+$len > $size_limit) {
  6530. $len = $pos >= $size_limit ? 0 : $size_limit - $pos;
  6531. $beyond_limit = 1;
  6532. }
  6533. if ($len > 0 && $pos < $self->{'prefix_l'}) {
  6534. # not efficient, but typically only occurs once
  6535. $str = substr(join('',@{$self->{'prefix'}}), $pos, $len);
  6536. $nbytes += length($str); $len -= $nbytes;
  6537. }
  6538. my $msg; my $buff_directly_accessed = 0;
  6539. if ($len > 0) {
  6540. # avoid shuffling data through multiple buffers for a common case
  6541. $buff_directly_accessed = $nbytes == 0;
  6542. my $nb = $buff_directly_accessed
  6543. ? read($self->{'handle'}, $_[0], $len, $offset)
  6544. : read($self->{'handle'}, $str, $len, $nbytes);
  6545. if (!defined $nb) {
  6546. $msg = "Error reading: $!";
  6547. } elsif ($nb < 1) { # read returns 0 at eof
  6548. $self->{'eof'} = 1;
  6549. } else {
  6550. $nbytes += $nb; $len -= $nb;
  6551. }
  6552. }
  6553. if (defined $msg) {
  6554. undef $nbytes; # $! already set by a failed sysread
  6555. } elsif ($beyond_limit && $nbytes == 0) {
  6556. $self->{'eof'} = 1;
  6557. } else {
  6558. if (!$buff_directly_accessed) {
  6559. ($offset ? substr($_[0],$offset) : $_[0]) = $str;
  6560. }
  6561. $pos += $nbytes; $self->{'pos'} = $pos;
  6562. }
  6563. $nbytes; # eof: 0; error: undef
  6564. }
  6565. sub close { shift->CLOSE(@_) }
  6566. sub fileno { shift->FILENO(@_) }
  6567. sub binmode { shift->BINMODE(@_) }
  6568. sub seek { shift->SEEK(@_) }
  6569. #sub tell { shift->TELL(@_) }
  6570. sub read { shift->READ(@_) }
  6571. sub readline { shift->READLINE(@_) }
  6572. sub getlines { shift->READLINE(@_) }
  6573. sub getline { scalar(shift->READLINE(@_)) }
  6574. 1;
  6575. #
  6576. package Amavis::IO::Zlib;
  6577. # A simple IO::File -compatible wrapper around Compress::Zlib,
  6578. # much like IO::Zlib but simpler: does only what we need and does it carefully
  6579. use strict;
  6580. use re 'taint';
  6581. BEGIN {
  6582. require Exporter;
  6583. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  6584. $VERSION = '2.316';
  6585. @ISA = qw(Exporter);
  6586. }
  6587. use Errno qw(EIO);
  6588. use Compress::Zlib;
  6589. sub new {
  6590. my $class = shift; my $self = bless {}, $class;
  6591. if (@_) { $self->open(@_) or return }
  6592. $self;
  6593. }
  6594. sub close {
  6595. my $self = shift;
  6596. my $status; my $eval_stat; local($1,$2);
  6597. eval { $status = $self->{fh}->gzclose; 1 }
  6598. or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  6599. delete $self->{fh};
  6600. if (defined $eval_stat) {
  6601. chomp $eval_stat;
  6602. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  6603. # can't stash arbitrary text into $!
  6604. die "gzclose error: $eval_stat, $gzerrno";
  6605. $! = EIO; return; # not reached
  6606. } elsif ($status != Z_OK) {
  6607. die "gzclose error: $gzerrno"; # can't stash arbitrary text into $!
  6608. $! = EIO; return; # not reached
  6609. }
  6610. 1;
  6611. }
  6612. sub DESTROY {
  6613. my $self = shift; local($@,$!,$_);
  6614. # ignore failure, make perlcritic happy
  6615. if (ref $self && $self->{fh}) { eval { $self->close } or 1 }
  6616. }
  6617. sub open {
  6618. my($self,$fname,$mode) = @_;
  6619. # ignore failure, make perlcritic happy
  6620. if (exists($self->{fh})) { eval { $self->close } or 1; delete $self->{fh} }
  6621. $self->{fname} = $fname; $self->{mode} = $mode; $self->{pos} = 0;
  6622. my $gz = gzopen($fname,$mode);
  6623. if ($gz) {
  6624. $self->{fh} = $gz;
  6625. } else {
  6626. die "gzopen error: $gzerrno"; # can't stash arbitrary text into $!
  6627. $! = EIO; undef $gz; # not reached
  6628. }
  6629. $gz;
  6630. }
  6631. sub seek {
  6632. my($self,$pos,$whence) = @_;
  6633. $whence == 0 or die "Only absolute seek is supported on gzipped file";
  6634. $pos >= 0 or die "Can't seek to a negative absolute position";
  6635. $self->{mode} eq 'rb'
  6636. or die "Seek to $whence,$pos on gzipped file only supported for 'rb' mode";
  6637. if ($pos < $self->{pos}) {
  6638. $self->close or die "seek: can't close gzipped file: $!";
  6639. $self->open($self->{fname},$self->{mode})
  6640. or die "seek: can't reopen gzipped file: $!";
  6641. }
  6642. my $skip = $pos - $self->{pos};
  6643. while ($skip > 0) {
  6644. my $s; my $nbytes = $self->read($s,$skip); # acceptable for small skips
  6645. defined $nbytes && $nbytes > 0
  6646. or die "seek: error skipping $skip bytes on gzipped file: $!";
  6647. $skip -= $nbytes;
  6648. }
  6649. 1; # seek is supposed to return 1 upon success, 0 otherwise
  6650. }
  6651. sub read { # SCALAR,LENGTH,OFFSET
  6652. my $self = shift; my $len = $_[1]; my $offset = $_[2];
  6653. defined $len or die "Amavis::IO::Zlib::read: length argument undefined";
  6654. my $nbytes;
  6655. if (!defined($offset) || $offset == 0) {
  6656. $nbytes = $self->{fh}->gzread($_[0], $len);
  6657. } else {
  6658. my $buff;
  6659. $nbytes = $self->{fh}->gzread($buff, $len);
  6660. substr($_[0],$offset) = $buff;
  6661. }
  6662. if ($nbytes < 0) {
  6663. die "gzread error: $gzerrno"; # can't stash arbitrary text into $!
  6664. $! = EIO; undef $nbytes; # not reached
  6665. } else {
  6666. $self->{pos} += $nbytes;
  6667. }
  6668. $nbytes; # eof: 0; error: undef
  6669. }
  6670. sub getline {
  6671. my $self = shift; my($nbytes,$line);
  6672. $nbytes = $self->{fh}->gzreadline($line);
  6673. if ($nbytes <= 0) { # eof (0) or error (-1)
  6674. $! = 0; $line = undef;
  6675. if ($nbytes < 0 && $gzerrno != Z_STREAM_END) {
  6676. die "gzreadline error: $gzerrno"; # can't stash arbitrary text into $!
  6677. $! = EIO; # not reached
  6678. }
  6679. } else {
  6680. $self->{pos} += $nbytes;
  6681. }
  6682. $line; # eof: undef, $! zero; error: undef, $! nonzero
  6683. }
  6684. sub print {
  6685. my $self = shift;
  6686. my $buff_ref = @_ == 1 ? \$_[0] : \join('',@_);
  6687. my $nbytes; my $len = length($$buff_ref);
  6688. if ($len <= 0) {
  6689. $nbytes = "0 but true";
  6690. } else {
  6691. $nbytes = $self->{fh}->gzwrite($$buff_ref); $self->{pos} += $len;
  6692. if ($nbytes <= 0) {
  6693. die "gzwrite error: $gzerrno"; # can't stash arbitrary text into $!
  6694. $! = EIO; undef $nbytes; # not reached
  6695. }
  6696. }
  6697. $nbytes;
  6698. }
  6699. sub printf { shift->print(sprintf(shift,@_)) }
  6700. 1;
  6701. #
  6702. package Amavis::IO::RW;
  6703. use strict;
  6704. use re 'taint';
  6705. BEGIN {
  6706. require Exporter;
  6707. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  6708. $VERSION = '2.316';
  6709. @ISA = qw(Exporter);
  6710. import Amavis::Conf qw(:platform);
  6711. import Amavis::Util qw(ll do_log min max minmax);
  6712. }
  6713. use Errno qw(EIO EINTR EAGAIN EPIPE ENOTCONN ECONNRESET);
  6714. use Time::HiRes ();
  6715. use IO::Socket;
  6716. use IO::Socket::UNIX;
  6717. #use IO::Socket::SSL;
  6718. # Connect to one of the specified sockets. The $socket_specs may be a
  6719. # simple string ([inet-host]:port, [inet6-host]:port, or a unix socket name),
  6720. # optionally prefixed by a protocol name (scheme) and a colon (the prefix is
  6721. # ignored here, just avoids a need for parsing by a caller); or it can be
  6722. # a ref to a list of such socket specifications, which are tried one after
  6723. # another until a connection is successful. In case of a listref, it leaves
  6724. # a good socket as the first entry in the list so that it will be tried first
  6725. # on a next call.
  6726. # The 'Timeout' argument controls both the connect timeout as well as the
  6727. # timeout of a select() call in rw_loop() - but may be changed through a
  6728. # timeout() method.
  6729. #
  6730. sub new {
  6731. my($class, $socket_specs, %arg) = @_;
  6732. my $self = bless {}, $class;
  6733. $self->timeout($arg{Timeout});
  6734. $self->{eol_str} = !defined $arg{Eol} ? "\n" : $arg{Eol};
  6735. $self->{inp_sane_size} = !$arg{InpSaneSize} ? 500000 : $arg{InpSaneSize};
  6736. $self->{last_event_time} = 0; $self->{last_event_tx_time} = 0;
  6737. $self->{inp} = ''; $self->{out} = '';
  6738. $self->{inpeof} = 0; $self->{ssl_active} = 0;
  6739. $socket_specs = [ $socket_specs ] if !ref $socket_specs;
  6740. my($protocol,$socketname,$sock,$eval_stat);
  6741. my $attempts = 0; my(@failures);
  6742. my $n_candidates = scalar @$socket_specs;
  6743. $n_candidates > 0 or die "Can't connect, no sockets specified!?"; # sanity
  6744. for (;;) {
  6745. if ($n_candidates > 1) { # pick one at random, put it to head of the list
  6746. my $j = int(rand($n_candidates));
  6747. ll(5) && do_log(5, "picking candidate #%d (of %d) in %s",
  6748. $j+1, $n_candidates, join(', ',@$socket_specs));
  6749. @$socket_specs[0,$j] = @$socket_specs[$j,0] if $j != 0;
  6750. }
  6751. $socketname = $socket_specs->[0]; # try the first on the list
  6752. local($1);
  6753. $socketname =~ s/^([a-z][a-z0-9.+-]*)?://si; # strip protocol name
  6754. $protocol = lc($1); # kept for the benefit of a caller
  6755. $self->{socketname} = undef;
  6756. $attempts++;
  6757. eval {
  6758. $sock = $self->connect_attempt($socketname, %arg);
  6759. $sock or die "Error connecting to socket $socketname\n";
  6760. 1;
  6761. } or do {
  6762. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  6763. undef $sock;
  6764. };
  6765. if ($sock) { # mission accomplished
  6766. if (!@failures) {
  6767. do_log(5, "connected to %s successfully%s", $self->{socketname});
  6768. } else {
  6769. do_log(1, "connected to %s successfully after %d failures on: %s",
  6770. $self->{socketname}, scalar(@failures), join(', ',@failures));
  6771. }
  6772. last;
  6773. } else { # failure, prepare for a retry with a next entry if any
  6774. $n_candidates--;
  6775. my $ll = $attempts > 1 || $n_candidates <= 0 ? -1 : 1;
  6776. ll($ll) && do_log($ll, "connect to %s failed, attempt #%d: %s%s",
  6777. $socketname, $attempts, $eval_stat,
  6778. $n_candidates <= 0 ? '' : ', trying next');
  6779. push(@failures, $socketname);
  6780. # circular shift left, move a bad candidate to the end of the list
  6781. push(@$socket_specs, shift @$socket_specs) if @$socket_specs > 1;
  6782. last if $n_candidates <= 0;
  6783. }
  6784. }
  6785. $sock or die("All attempts ($attempts) failed connecting to ".
  6786. join(', ',@$socket_specs) . "\n");
  6787. $self->{socket} = $sock;
  6788. $self->{protocol} = $protocol;
  6789. $self;
  6790. }
  6791. sub connect_attempt {
  6792. my($self, $socketname, %arg) = @_;
  6793. my $sock;
  6794. my($localaddr, $localport) = ($arg{LocalAddr}, $arg{LocalPort});
  6795. my $blocking = 1; # blocking mode defaults to on
  6796. $blocking = 0 if defined $arg{Blocking} && !$arg{Blocking};
  6797. my $timeout = $self->timeout;
  6798. my $timeout_displ = !defined $timeout ? 'undef'
  6799. : int($timeout) == $timeout ? "$timeout"
  6800. : sprintf("%.3f",$timeout);
  6801. my($peeraddress, $peerport, $is_inet); local($1,$2,$3);
  6802. if ($socketname =~ m{^/}) { # simpleminded: unix vs. inet
  6803. $is_inet = 0;
  6804. } elsif ($socketname =~ /^(?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*)/sx) {
  6805. # ignore possible further fields after the "proto:addr:port:..." last colon
  6806. $peeraddress = defined $1 ? $1 : $2; $peerport = $3; $is_inet = 1;
  6807. } elsif ($socketname =~ /^(?: \[ ([^\]]*) \] | ([0-9a-fA-F.:]+) ) \z/sx) {
  6808. $peeraddress = defined $1 ? $1 : $2; $is_inet = 1;
  6809. } else { # probably a syntax error, but let's assume it is a Unix socket
  6810. $is_inet = 0;
  6811. }
  6812. if ($is_inet) {
  6813. if (defined $peeraddress && $peeraddress eq '*') {
  6814. $peeraddress = $arg{WildcardImpliedHost};
  6815. defined $peeraddress
  6816. or die "Wildcarded host, but client's address not known: $socketname";
  6817. }
  6818. if (!defined $peeraddress || $peeraddress eq '') {
  6819. die "Empty/unknown host address in socket specification: $socketname";
  6820. }
  6821. $peerport = $arg{Port} if !defined $peerport || $peerport eq '';
  6822. if (defined $peerport && $peerport eq '*') {
  6823. $peerport = $arg{WildcardImpliedPort};
  6824. defined $peerport
  6825. or die "Wildcarded port, but client's port not known: $socketname";
  6826. }
  6827. if (!defined $peerport || $peerport eq '') {
  6828. die "Empty/unknown port number in socket specification: $socketname";
  6829. } elsif ($peerport !~ /^\d{1,5}\z/ || $peerport < 1 || $peerport > 65535) {
  6830. die "Invalid port number in socket specification: $socketname";
  6831. }
  6832. }
  6833. $self->{last_event_time} = $self->{last_event_tx_time} = Time::HiRes::time;
  6834. if (!$is_inet) {
  6835. # unix socket
  6836. ll(3) && do_log(3, "new socket by IO::Socket::UNIX to %s, timeout %s",
  6837. $socketname, $timeout_displ);
  6838. $sock = IO::Socket::UNIX->new(
  6839. # Domain => AF_UNIX,
  6840. Type => SOCK_STREAM, Timeout => $timeout);
  6841. $sock or die "Can't create UNIX socket: $!\n";
  6842. $sock->connect( pack_sockaddr_un($socketname) )
  6843. or die "Can't connect to UNIX socket $socketname: $!\n";
  6844. $self->{last_event} = 'new-unix';
  6845. } else {
  6846. my $module = $have_socket_ip ? 'IO::Socket::IP'
  6847. : $have_inet4 && (!$have_inet6 ||
  6848. $peeraddress=~/^\d+\.\d+\.\d+\.\d+\z/) ? 'IO::Socket::INET'
  6849. : 'IO::Socket::INET6';
  6850. my $local_sock_displ = '';
  6851. my(%args) = (Type => SOCK_STREAM, Proto => 'tcp', Blocking => $blocking,
  6852. PeerAddr => $peeraddress, PeerPort => $peerport);
  6853. # Timeout => $timeout, # produces: Invalid argument
  6854. if (defined $localaddr && $localaddr ne '') {
  6855. $args{LocalAddr} = $localaddr;
  6856. $local_sock_displ .= '[' . $localaddr . ']';
  6857. }
  6858. if (defined $localport && $localport ne '') {
  6859. $args{LocalPort} = $localport;
  6860. $local_sock_displ .= ':' . $localport;
  6861. }
  6862. ll(3) && do_log(3,"new socket using %s to [%s]:%s, timeout %s%s%s",
  6863. $module, $peeraddress, $peerport, $timeout_displ,
  6864. $blocking ? '' : ', nonblocking',
  6865. $local_sock_displ eq '' ? ''
  6866. : ', local '.$local_sock_displ);
  6867. if ($have_socket_ip) { # $module eq 'IO::Socket::IP'
  6868. # inet or inet6 socket, let IO::Socket::IP handle dirty details
  6869. $sock = IO::Socket::IP->new(%args);
  6870. # note: the IO::Socket::IP constructor provides error message in $@
  6871. $sock or die "Can't connect to socket $socketname using $module: $@\n";
  6872. } elsif ($module eq 'IO::Socket::INET') { # inet socket (IPv4)
  6873. $sock = IO::Socket::INET->new(%args);
  6874. $sock or die "Can't connect to socket $socketname using $module: $!\n";
  6875. } else { # inet6 socket: no inet or IPv6 or unknown addr family
  6876. $sock = IO::Socket::INET6->new(%args);
  6877. $sock or die "Can't connect to socket $socketname using $module: $!\n";
  6878. }
  6879. $self->{last_event} = 'new-'.$module;
  6880. }
  6881. if ($sock) {
  6882. $self->{socketname} = $is_inet ? "[$peeraddress]:$peerport" : $socketname;
  6883. }
  6884. $sock;
  6885. }
  6886. sub internal_close {
  6887. my($self, $destroying) = @_;
  6888. my $sock = $self->{socket};
  6889. my $status = 1; # ok
  6890. if (!defined($sock)) {
  6891. # nothing to do
  6892. } elsif (!defined(fileno($sock))) { # not really open
  6893. $sock->close; # ignoring errors
  6894. } else {
  6895. my $flush_status = 1; # ok
  6896. eval { # don't let errors during flush prevent us from closing a socket
  6897. $flush_status = $self->flush;
  6898. } or do {
  6899. undef $flush_status; # false, indicates a signalled failure
  6900. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  6901. do_log($destroying ? 5 : 1,
  6902. "Error flushing socket on Amavis::IO::RW::%s: %s",
  6903. $destroying?'DESTROY':'close', $eval_stat);
  6904. };
  6905. $self->{last_event} = 'close';
  6906. $self->{last_event_time} = $self->{last_event_tx_time} = Time::HiRes::time;
  6907. $! = 0; $status = $sock->close;
  6908. $status or do_log($destroying ? 5 : 1,
  6909. "Error closing socket on Amavis::IO::RW::%s: %s",
  6910. $destroying?'DESTROY':'close',
  6911. !$self->{ssl_active} ? $! : $sock->errstr.", $!" );
  6912. $status = $flush_status if $status && !$flush_status;
  6913. }
  6914. $status;
  6915. }
  6916. sub close {
  6917. my $self = $_[0];
  6918. $self->internal_close(0);
  6919. }
  6920. sub DESTROY {
  6921. my $self = $_[0]; local($@,$!,$_);
  6922. # ignore failure, make perlcritic happy
  6923. eval { $self->internal_close(1) } or 1;
  6924. }
  6925. sub rw_loop {
  6926. my($self,$needline,$flushoutput) = @_;
  6927. #
  6928. # RFC 2920: Client SMTP implementations MAY elect to operate in a nonblocking
  6929. # fashion, processing server responses immediately upon receipt, even if
  6930. # there is still data pending transmission from the client's previous TCP
  6931. # send operation. If nonblocking operation is not supported, however, client
  6932. # SMTP implementations MUST also check the TCP window size and make sure that
  6933. # each group of commands fits entirely within the window. The window size
  6934. # is usually, but not always, 4K octets. Failure to perform this check can
  6935. # lead to deadlock conditions.
  6936. #
  6937. # We choose to operate in a nonblocking mode. Responses are read as soon as
  6938. # they become available and stored for later, but not immediately processed
  6939. # as they come in. This requires some sanity limiting against rogue servers.
  6940. #
  6941. my $sock = $self->{socket};
  6942. my $fd_sock = fileno($sock);
  6943. my $timeout = $self->timeout;
  6944. my $timeout_displ = !defined $timeout ? 'undef'
  6945. : int($timeout) == $timeout ? "$timeout"
  6946. : sprintf("%.3f",$timeout);
  6947. my $eol_str = $self->{eol_str};
  6948. my $idle_cnt = 0; my $failed_write_attempts = 0;
  6949. local $SIG{PIPE} = 'IGNORE'; # don't signal on a write to a widowed pipe
  6950. for (;;) {
  6951. $idle_cnt++;
  6952. my($rout,$wout,$eout,$rin,$win,$ein); $rin=$win=$ein='';
  6953. my $want_to_write = $self->{out} ne '' && ($flushoutput || $needline);
  6954. ll(5) && do_log(5, 'rw_loop: needline=%d, flush=%s, wr=%d, timeout=%s',
  6955. $needline, $flushoutput, $want_to_write, $timeout_displ);
  6956. if (!defined($fd_sock)) {
  6957. do_log(3, 'rw_loop read: got a closed socket');
  6958. $self->{inpeof} = 1; last;
  6959. }
  6960. vec($rin,$fd_sock,1) = 1;
  6961. vec($win,$fd_sock,1) = $want_to_write ? 1 : 0;
  6962. $ein = $rin | $win;
  6963. $self->{last_event} = 'select';
  6964. $self->{last_event_time} = Time::HiRes::time;
  6965. my($nfound,$timeleft) =
  6966. select($rout=$rin, $wout=$win, $eout=$ein, $timeout);
  6967. defined $nfound && $nfound >= 0
  6968. or die "Select failed: ".
  6969. (!$self->{ssl_active} ? $! : $sock->errstr.", $!");
  6970. if (vec($rout,$fd_sock,1)) {
  6971. ll(5) && do_log(5, 'rw_loop: receiving');
  6972. my $inbuf = ''; $! = 0;
  6973. my $nread = sysread($sock,$inbuf,16384);
  6974. if ($nread) { # successful read
  6975. $self->{last_event} = 'read-ok';
  6976. $self->{inpeof} = 0;
  6977. ll(5) && do_log(5,'rw_loop read %d chars< %s', length($inbuf),$inbuf);
  6978. $self->{inp} .= $inbuf; $idle_cnt = 0;
  6979. length($self->{inp}) < $self->{inp_sane_size}
  6980. or die "rw_loop: Aborting on a runaway server, inp_len=" .
  6981. length($self->{inp});
  6982. } elsif (defined $nread) { # defined but zero, sysread returns 0 at eof
  6983. $self->{last_event} = 'read-eof';
  6984. $self->{inpeof} = 1; do_log(3, 'rw_loop read: got eof');
  6985. } elsif ($! == EAGAIN || $! == EINTR) {
  6986. $self->{last_event} = 'read-intr'.(0+$!);
  6987. $idle_cnt = 0;
  6988. do_log(2, 'rw_loop read interrupted: %s',
  6989. !$self->{ssl_active} ? $! : $sock->errstr.", $!");
  6990. Time::HiRes::sleep(0.1); # slow down, just in case
  6991. # retry
  6992. } else {
  6993. $self->{last_event} = 'read-fail';
  6994. $self->{inpeof} = 1;
  6995. die "Error reading from socket: ".
  6996. (!$self->{ssl_active} ? $! : $sock->errstr.", $!");
  6997. }
  6998. $self->{last_event_time} = Time::HiRes::time;
  6999. }
  7000. if (vec($wout,$fd_sock,1)) {
  7001. my $out_l = length($self->{out});
  7002. ll(5) && do_log(5,'rw_loop: sending %d chars', $out_l);
  7003. my $nwrite = syswrite($sock, $self->{out});
  7004. if (!defined($nwrite)) {
  7005. if ($! == EAGAIN || $! == EINTR) {
  7006. $self->{last_event} = 'write-intr'.(0+$!);
  7007. $idle_cnt = 0; $failed_write_attempts++;
  7008. do_log(2, 'rw_loop writing %d bytes interrupted: %s', $out_l,
  7009. !$self->{ssl_active} ? $! : $sock->errstr.", $!");
  7010. Time::HiRes::sleep(0.1); # slow down, just in case
  7011. } else {
  7012. $self->{last_event} = 'write-fail';
  7013. die sprintf('Error writing %d bytes to socket: %s', $out_l,
  7014. !$self->{ssl_active} ? $! : $sock->errstr.", $!");
  7015. }
  7016. } else { # successful write
  7017. $self->{last_event} = 'write-ok';
  7018. my $ll = $nwrite != $out_l ? 4 : 5;
  7019. if (ll($ll)) {
  7020. my $msg = $nwrite==$out_l ? sprintf("%d", $nwrite)
  7021. : sprintf("%d (of %d)", $nwrite,$out_l);
  7022. my $nlog = min(200,$nwrite);
  7023. do_log($ll, 'rw_loop sent %s> %s%s',
  7024. $msg, substr($self->{out},0,$nlog), $nlog<$nwrite?' [...]':'');
  7025. };
  7026. $idle_cnt = 0;
  7027. if ($nwrite <= 0) { $failed_write_attempts++ }
  7028. elsif ($nwrite < $out_l) { substr($self->{out},0,$nwrite) = '' }
  7029. else { $self->{out} = '' }
  7030. }
  7031. $self->{last_event_time} = $self->{last_event_tx_time} =
  7032. Time::HiRes::time;
  7033. }
  7034. if ( ( !$needline || !defined($eol_str) || $eol_str eq '' ||
  7035. index($self->{inp},$eol_str) >= 0 ) &&
  7036. ( !$flushoutput || $self->{out} eq '' ) ) {
  7037. last;
  7038. }
  7039. if ($self->{inpeof}) {
  7040. if ($self->{out} ne '') {
  7041. do_log(2, 'rw_loop: EOF on input, output buffer not yet empty');
  7042. }
  7043. last;
  7044. }
  7045. if ($idle_cnt > 0) { # probably exceeded timeout in select
  7046. do_log(-1, 'rw_loop: leaving rw loop, no progress, '.
  7047. 'last event (%s) %.3f s ago', $self->{last_event},
  7048. Time::HiRes::time - $self->{last_event_time});
  7049. last;
  7050. }
  7051. $failed_write_attempts < 100 or die "rw_loop: Aborting stalled sending";
  7052. }
  7053. }
  7054. sub socketname
  7055. { my $self=shift; !@_ ? $self->{socketname} : ($self->{socketname}=shift) }
  7056. sub protocol
  7057. { my $self=shift; !@_ ? $self->{protocol} : ($self->{protocol}=shift) }
  7058. sub timeout
  7059. { my $self=shift; !@_ ? $self->{timeout} : ($self->{timeout}=shift) }
  7060. sub ssl_active
  7061. { my $self=shift; !@_ ? $self->{ssl_active} : ($self->{ssl_active}=shift) }
  7062. sub eof
  7063. { my $self=shift; $self->{inpeof} && $self->{inp} eq '' ? 1 : 0 }
  7064. sub last_io_event_timestamp
  7065. { my($self,$keyword) = @_; $self->{last_event_time} }
  7066. sub last_io_event_tx_timestamp
  7067. { my($self,$keyword) = @_; $self->{last_event_tx_time} }
  7068. sub flush
  7069. { my $self=shift; $self->rw_loop(0,1) if $self->{out} ne ''; 1 }
  7070. sub out_buff_large
  7071. { my $self=shift; length $self->{out} > 40000 }
  7072. sub print {
  7073. my $self = shift;
  7074. $self->{out} .= $_ for @_;
  7075. # $self->out_buff_large ? $self->flush : 1;
  7076. length $self->{out} > 40000 ? $self->flush : 1; # inlined out_buff_large()
  7077. }
  7078. sub at_line_boundary {
  7079. my $self = $_[0];
  7080. my $eol_str = $self->{eol_str};
  7081. my $eol_str_l = !defined($eol_str) ? 0 : length($eol_str);
  7082. !$eol_str_l ? 1
  7083. : substr($self->{out}, -$eol_str_l, $eol_str_l) eq $eol_str ? 1 : 0;
  7084. }
  7085. # returns true if there is any full line (or last incomplete line)
  7086. # in the buffer waiting to be read, 0 otherwise, undef on eof or error
  7087. #
  7088. sub response_line_available {
  7089. my($self) = @_;
  7090. my $eol_str = $self->{eol_str};
  7091. if (!defined $eol_str || $eol_str eq '') {
  7092. return length($self->{inp});
  7093. } elsif (index($self->{inp},$eol_str) >= 0) {
  7094. return 1;
  7095. } elsif ($self->{inpeof} && $self->{inp} eq '') {
  7096. return; # undef on end-of-file
  7097. } elsif ($self->{inpeof}) { # partial last line
  7098. return length($self->{inp});
  7099. }
  7100. }
  7101. # get one full text line, or last partial line, or undef on eof/error/timeout
  7102. #
  7103. sub get_response_line {
  7104. my($self) = @_;
  7105. my $ind; my $attempts = 0;
  7106. my $eol_str = $self->{eol_str};
  7107. my $eol_str_l = !defined($eol_str) ? 0 : length($eol_str);
  7108. for (;;) {
  7109. if (!$eol_str_l) {
  7110. my $str = $self->{inp}; $self->{inp} = ''; return $str;
  7111. } elsif (($ind=index($self->{inp},$eol_str)) >= 0) {
  7112. return substr($self->{inp},0,$ind+$eol_str_l,'');
  7113. } elsif ($self->{inpeof} && $self->{inp} eq '') {
  7114. $! = 0; return; # undef on end-of-file
  7115. } elsif ($self->{inpeof}) { # return partial last line
  7116. my $str = $self->{inp}; $self->{inp} = ''; return $str;
  7117. } elsif ($attempts > 0) {
  7118. $! = EIO; return; # timeout or error
  7119. }
  7120. # try reading some more input, one attempt only
  7121. $self->rw_loop(1,0); $attempts++;
  7122. }
  7123. }
  7124. # read whatever is available, up to LENGTH bytes
  7125. #
  7126. sub read { # SCALAR,LENGTH,OFFSET
  7127. my $self = shift; my $len = $_[1]; my $offset = $_[2];
  7128. defined $len or die "Amavis::IO::RW::read: length argument undefined";
  7129. $len >= 0 or die "Amavis::IO::RW::read: length argument negative";
  7130. $self->rw_loop(0,0);
  7131. my $nbytes = length($self->{inp});
  7132. $nbytes = $len if $len < $nbytes;
  7133. if (!defined($offset) || $offset == 0) {
  7134. $_[0] = substr($self->{inp}, 0, $len, '');
  7135. } else {
  7136. substr($_[0],$offset) = substr($self->{inp}, 0, $len, '');
  7137. }
  7138. $nbytes; # eof: 0; error: undef
  7139. }
  7140. use vars qw($ssl_cache);
  7141. sub ssl_upgrade {
  7142. my($self,%params) = @_;
  7143. $self->flush;
  7144. IO::Socket::SSL->VERSION(1.05); # required minimal version
  7145. $ssl_cache = IO::Socket::SSL::Session_Cache->new(2) if !defined $ssl_cache;
  7146. my $sock = $self->{socket};
  7147. IO::Socket::SSL->start_SSL($sock, SSL_session_cache => $ssl_cache,
  7148. SSL_error_trap =>
  7149. sub { my($sock,$msg)=@_; do_log(-2,"Error on socket: %s",$msg) },
  7150. %params,
  7151. ) or die "Error upgrading socket to SSL: ".IO::Socket::SSL::errstr();
  7152. $self->{last_event} = 'ssl-upgrade';
  7153. $self->{last_event_time} = $self->{last_event_tx_time} = Time::HiRes::time;
  7154. $self->{ssl_active} = 1;
  7155. ll(3) && do_log(3,"TLS cipher: %s", $sock->get_cipher);
  7156. ll(5) && do_log(5,"TLS certif: %s", $sock->dump_peer_certificate);
  7157. 1;
  7158. }
  7159. 1;
  7160. #
  7161. package Amavis::In::Connection;
  7162. # Keeps relevant information about how we received the message:
  7163. # client connection information, SMTP envelope and SMTP parameters
  7164. use strict;
  7165. use re 'taint';
  7166. BEGIN {
  7167. require Exporter;
  7168. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  7169. $VERSION = '2.316';
  7170. @ISA = qw(Exporter);
  7171. }
  7172. sub new
  7173. { my($class) = @_; bless {}, $class }
  7174. sub client_ip # client IP address (immediate SMTP client, i.e. our MTA)
  7175. { my $self=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) }
  7176. sub socket_ip # IP address of our interface that received connection
  7177. { my $self=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) }
  7178. sub socket_port # TCP port of our interface that received connection
  7179. { my $self=shift; !@_ ? $self->{socket_port}: ($self->{socket_port}=shift) }
  7180. sub socket_proto # TCP/UNIX
  7181. { my $self=shift; !@_ ? $self->{socket_proto}:($self->{socket_proto}=shift)}
  7182. sub socket_path # socket path, UNIX sockets only
  7183. { my $self=shift; !@_ ? $self->{socket_path}: ($self->{socket_path}=shift)}
  7184. # RFC 3848
  7185. sub appl_proto # SMTP/ESMTP(A|S|SA)/LMTP(A|S|SA) / AM.PDP/AM.CL/QMQP/QMQPqq
  7186. { my $self=shift; !@_ ? $self->{appl_proto} : ($self->{appl_proto}=shift) }
  7187. sub smtp_helo # (E)SMTP HELO/EHLO parameter
  7188. { my $self=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) }
  7189. 1;
  7190. #
  7191. package Amavis::In::Message::PerRecip;
  7192. use strict;
  7193. use re 'taint';
  7194. BEGIN {
  7195. require Exporter;
  7196. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  7197. $VERSION = '2.316';
  7198. @ISA = qw(Exporter);
  7199. import Amavis::Conf qw(:platform);
  7200. import Amavis::Util qw(setting_by_given_contents_category_all
  7201. setting_by_given_contents_category cmp_ccat);
  7202. }
  7203. sub new # NOTE: this class is a list for historical reasons, not a hash
  7204. { my($class) = @_; bless [(undef) x 41], $class }
  7205. # subs to set or access individual elements of a n-tuple by name
  7206. sub recip_addr # unquoted recipient envelope e-mail address
  7207. { my $self=shift; !@_ ? $$self[0] : ($$self[0]=shift) }
  7208. sub recip_addr_smtp # SMTP-encoded recipient envelope e-mail address in <>
  7209. { my $self=shift; !@_ ? $$self[1] : ($$self[1]=shift) }
  7210. sub recip_addr_modified # recip. addr. with possible addr. extension inserted
  7211. { my $self=shift; !@_ ? $$self[2] : ($$self[2]=shift) }
  7212. sub recip_is_local # recip_addr matches @local_domains_maps
  7213. { my $self=shift; !@_ ? $$self[3] : ($$self[3]=shift) }
  7214. sub recip_maddr_id # maddr.id field from SQL corresponding to recip_addr_smtp
  7215. { my $self=shift; !@_ ? $$self[4] : ($$self[4]=shift) }
  7216. sub recip_maddr_id_orig # maddr.id field from SQL corresponding to dsn_orcpt
  7217. { my $self=shift; !@_ ? $$self[5] : ($$self[5]=shift) }
  7218. sub recip_penpals_age # penpals age in seconds if logging to SQL is enabled
  7219. { my $self=shift; !@_ ? $$self[6] : ($$self[6]=shift) }
  7220. sub recip_penpals_score # penpals score (info, also added to spam_level)
  7221. { my $self=shift; !@_ ? $$self[7] : ($$self[7]=shift) }
  7222. sub dsn_notify # ESMTP RCPT command NOTIFY option (DSN-RFC 3461, listref)
  7223. { my $self=shift; !@_ ? $$self[8] : ($$self[8]=shift) }
  7224. sub dsn_orcpt # ESMTP RCPT command ORCPT option (DSN-RFC 3461, encoded)
  7225. { my $self=shift; !@_ ? $$self[9] : ($$self[9]=shift) }
  7226. sub dsn_suppress_reason # if defined disable sending DSN and supply a reason
  7227. { my $self=shift; !@_ ? $$self[10] : ($$self[10]=shift) }
  7228. sub recip_destiny # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
  7229. { my $self=shift; !@_ ? $$self[11] : ($$self[11]=shift) }
  7230. sub recip_done # false: not done, true: done (1: faked, 2: truly sent)
  7231. { my $self=shift; !@_ ? $$self[12] : ($$self[12]=shift) }
  7232. sub recip_smtp_response # RFC 5321 response (3-digit + enhanced resp + text)
  7233. { my $self=shift; !@_ ? $$self[13] : ($$self[13]=shift) }
  7234. sub recip_remote_mta_smtp_response # smtp response as issued by remote MTA
  7235. { my $self=shift; !@_ ? $$self[14] : ($$self[14]=shift) }
  7236. sub recip_remote_mta # remote MTA that issued the smtp response
  7237. { my $self=shift; !@_ ? $$self[15] : ($$self[15]=shift) }
  7238. sub recip_tagged # message was tagged by address extension or Subject or X-Spam
  7239. { my $self=shift; !@_ ? $$self[16] : ($$self[16]=shift) }
  7240. sub recip_mbxname # mailbox name or file when known (local:, bsmtp: or sql:)
  7241. { my $self=shift; !@_ ? $$self[17] : ($$self[17]=shift) }
  7242. sub recip_whitelisted_sender # recip considers this sender whitelisted
  7243. { my $self=shift; !@_ ? $$self[18] : ($$self[18]=shift) }
  7244. sub recip_blacklisted_sender # recip considers this sender blacklisted
  7245. { my $self=shift; !@_ ? $$self[19] : ($$self[19]=shift) }
  7246. sub bypass_virus_checks # boolean: virus checks to be bypassed for this recip
  7247. { my $self=shift; !@_ ? $$self[20] : ($$self[20]=shift) }
  7248. sub bypass_banned_checks # bool: ban checks are to be bypassed for this recip
  7249. { my $self=shift; !@_ ? $$self[21] : ($$self[21]=shift) }
  7250. sub bypass_spam_checks # boolean: spam checks are to be bypassed for this recip
  7251. { my $self=shift; !@_ ? $$self[22] : ($$self[22]=shift) }
  7252. sub banned_parts # banned part descriptions (ref to a list of banned parts)
  7253. { my $self=shift; !@_ ? $$self[23] : ($$self[23]=shift) }
  7254. sub banned_parts_as_attr # banned part descriptions - newer syntax (listref)
  7255. { my $self=shift; !@_ ? $$self[24] : ($$self[24]=shift) }
  7256. sub banning_rule_key # matching banned rules (lookup table keys) (ref to list)
  7257. { my $self=shift; !@_ ? $$self[25] : ($$self[25]=shift) }
  7258. sub banning_rule_comment #comments (or whole expr) from banning_rule_key regexp
  7259. { my $self=shift; !@_ ? $$self[26] : ($$self[26]=shift) }
  7260. sub banning_reason_short # just one banned part leaf name with a rule comment
  7261. { my $self=shift; !@_ ? $$self[27] : ($$self[27]=shift) }
  7262. sub banning_rule_rhs # a right-hand side of matching rules (a ref to a list)
  7263. { my $self=shift; !@_ ? $$self[28] : ($$self[28]=shift) }
  7264. sub mail_body_mangle # mail body is being modified (and how) (e.g. defanged)
  7265. { my $self=shift; !@_ ? $$self[29] : ($$self[29]=shift) }
  7266. sub contents_category # sorted listref of "major,minor" strings(category types)
  7267. { my $self=shift; !@_ ? $$self[30] : ($$self[30]=shift) }
  7268. sub blocking_ccat # category type most responsible for blocking msg, or undef
  7269. { my $self=shift; !@_ ? $$self[31] : ($$self[31]=shift) }
  7270. sub user_id # listref of recipient IDs from a lookup, e.g. SQL field users.id
  7271. { my $self=shift; !@_ ? $$self[32] : ($$self[32]=shift) }
  7272. sub user_policy_id # recipient's policy ID, e.g. SQL field users.policy_id
  7273. { my $self=shift; !@_ ? $$self[33] : ($$self[33]=shift) }
  7274. sub courier_control_file # path to control file containing this recipient
  7275. { my $self=shift; !@_ ? $$self[34] : ($$self[34]=shift) }
  7276. sub courier_recip_index # index of recipient within control file
  7277. { my $self=shift; !@_ ? $$self[35] : ($$self[35]=shift) }
  7278. sub delivery_method # delivery method, or empty for implicit delivery (milter)
  7279. { my $self=shift; !@_ ? $$self[36] : ($$self[36]=shift) }
  7280. sub spam_level # spam score as returned by spam scanners, ham near 0, spam 5
  7281. { my $self=shift; !@_ ? $$self[37] : ($$self[37]=shift) }
  7282. sub spam_tests # a listref of r/o stringrefs, each: t1=score1,t2=score2,..
  7283. { my $self=shift; !@_ ? $$self[38] : ($$self[38]=shift) }
  7284. # per-recipient spam info - when undefined consult a per-message counterpart
  7285. sub spam_report # SA terse report of tests hit (for header section reports)
  7286. { my $self=shift; !@_ ? $$self[39] : ($$self[39]=shift) }
  7287. sub spam_summary # SA summary of tests hit for standard body reports
  7288. { my $self=shift; !@_ ? $$self[40] : ($$self[40]=shift) }
  7289. sub recip_final_addr { # return recip_addr_modified if set, else recip_addr
  7290. my $self = shift;
  7291. my $newaddr = $self->recip_addr_modified;
  7292. defined $newaddr ? $newaddr : $self->recip_addr;
  7293. }
  7294. # The contents_category list is a sorted list of strings, each of the form
  7295. # "major" or "major,minor", where major and minor are numbers, representing
  7296. # major and minor category type. Sort order is descending by numeric values,
  7297. # major first, and subordered by a minor value. When an entry "major,minor"
  7298. # is added, an entry "major" is added automatically (minor implied to be 0).
  7299. # A string "major" means the same as "major,0". See CC_* constants for major
  7300. # category types. Minor category types semantics is specific to each major
  7301. # category, higher number represent more important finding than a lower number.
  7302. # add new findings to the contents_category list
  7303. #
  7304. sub add_contents_category {
  7305. my($self, $major,$minor) = @_;
  7306. my $aref = $self->contents_category || [];
  7307. # major category is always inserted, but "$major,$minor" only if minor>0
  7308. if (defined $minor && $minor > 0) { # straight insertion of "$major,$minor"
  7309. my $el = sprintf("%d,%d",$major,$minor); my $j=0;
  7310. for (@$aref) { if (cmp_ccat($_,$el) <= 0) { last } else { $j++ } };
  7311. if ($j > $#{$aref}) { push(@$aref,$el) } # append
  7312. elsif (cmp_ccat($aref->[$j],$el) != 0) { splice(@$aref,$j,0,$el) }
  7313. }
  7314. # straight insertion of "$major" into an ordered array (descending order)
  7315. my $el = sprintf("%d",$major); my $j=0;
  7316. for (@$aref) { if (cmp_ccat($_,$el) <= 0) { last } else { $j++ } };
  7317. if ($j > $#{$aref}) { push(@$aref,$el) } # append
  7318. elsif (cmp_ccat($aref->[$j],$el) != 0)
  7319. { splice(@$aref,$j,0,$el) } # insert at index $j
  7320. $self->contents_category($aref);
  7321. }
  7322. # is the "$major,$minor" category in the list?
  7323. #
  7324. sub is_in_contents_category {
  7325. my($self, $major,$minor) = @_;
  7326. my $el = sprintf('%d,%d', $major,$minor);
  7327. my $aref = $self->contents_category;
  7328. !defined($aref) ? undef : scalar(grep(cmp_ccat($_,$el) == 0, @$aref));
  7329. }
  7330. # get a setting corresponding to the most important contents category;
  7331. # i.e. the highest entry from the category list for which a corresponding entry
  7332. # in the associative array of settings exists determines returned setting;
  7333. #
  7334. sub setting_by_main_contents_category {
  7335. my($self, @settings_href_list) = @_;
  7336. return undef if !@settings_href_list;
  7337. my $aref = $self->contents_category;
  7338. setting_by_given_contents_category($aref, @settings_href_list);
  7339. }
  7340. # get a list of settings corresponding to all relevant contents categories,
  7341. # sorted from the most important to the least important entry; entries which
  7342. # have no corresponding setting are not included in the list
  7343. #
  7344. sub setting_by_main_contents_category_all {
  7345. my($self, @settings_href_list) = @_;
  7346. return undef if !@settings_href_list;
  7347. my $aref = $self->contents_category;
  7348. setting_by_given_contents_category_all($aref, @settings_href_list);
  7349. }
  7350. sub setting_by_blocking_contents_category {
  7351. my($self, @settings_href_list) = @_;
  7352. my $blocking_ccat = $self->blocking_ccat;
  7353. !defined($blocking_ccat) ? undef
  7354. : setting_by_given_contents_category($blocking_ccat, @settings_href_list);
  7355. }
  7356. sub setting_by_contents_category {
  7357. my($self, @settings_href_list) = @_;
  7358. my $blocking_ccat = $self->blocking_ccat;
  7359. !defined($blocking_ccat)
  7360. ? $self->setting_by_main_contents_category(@settings_href_list)
  7361. : setting_by_given_contents_category($blocking_ccat, @settings_href_list);
  7362. }
  7363. 1;
  7364. #
  7365. package Amavis::In::Message;
  7366. # this class keeps information about the message being processed
  7367. use strict;
  7368. use re 'taint';
  7369. BEGIN {
  7370. require Exporter;
  7371. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  7372. $VERSION = '2.316';
  7373. @ISA = qw(Exporter);
  7374. import Amavis::Conf qw(:platform);
  7375. import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp quote_rfc2821_local
  7376. qquote_rfc2821_local);
  7377. import Amavis::Util qw(orcpt_encode ll do_log);
  7378. import Amavis::In::Message::PerRecip;
  7379. }
  7380. sub new
  7381. { my($class) = @_; my $self = bless({},$class); $self->skip_bytes(0); $self }
  7382. sub conn_obj # ref to a connection object Amavis::In::Connection
  7383. { my $self=shift; !@_ ? $self->{conn} : ($self->{conn}=shift) }
  7384. sub rx_time # Unix time (s since epoch) of message reception by amavisd
  7385. { my $self=shift; !@_ ? $self->{rx_time} : ($self->{rx_time}=shift) }
  7386. sub partition_tag # SQL partition tag (e.g. an ISO week number 1..53, or 0)
  7387. { my $self=shift; !@_ ? $self->{partition} : ($self->{partition}=shift) }
  7388. sub client_proto # orig. client protocol, obtained from XFORWARD or milter
  7389. { my $self=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) }
  7390. sub client_addr # original client IP addr, obtained from XFORWARD or milter
  7391. { my $self=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) }
  7392. sub client_name # orig. client DNS name, obtained from XFORWARD or milter
  7393. { my $self=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) }
  7394. sub client_port # orig client src port num, obtained from XFORWARD or milter
  7395. { my $self=shift; !@_ ? $self->{cli_port} : ($self->{cli_port}=shift) }
  7396. sub client_source # LOCAL/REMOTE/undef, local_header_rewrite_clients/XFORWARD
  7397. { my $self=shift; !@_ ? $self->{cli_source} : ($self->{cli_source}=shift) }
  7398. sub client_helo # orig. client EHLO name, obtained from XFORWARD or milter
  7399. { my $self=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) }
  7400. sub client_os_fingerprint # SMTP client's OS fingerprint, obtained from p0f
  7401. { my $self=shift; !@_ ? $self->{cli_p0f} : ($self->{cli_p0f}=shift) }
  7402. sub originating # originating from our users, copied from c('originating')
  7403. { my $self=shift; !@_ ? $self->{originating}: ($self->{originating}=shift) }
  7404. sub queue_id # MTA queue ID of message if known (Courier, milter/AM.PDP, XFORW)
  7405. { my $self=shift; !@_ ? $self->{queue_id} : ($self->{queue_id}=shift) }
  7406. sub log_id # task id as shown in the log, also known as am_id
  7407. { my $self=shift; !@_ ? $self->{log_id} : ($self->{log_id}=shift) }
  7408. sub mail_id # long-term unique id of the message on this system
  7409. { my $self=shift; !@_ ? $self->{mail_id} : ($self->{mail_id}=shift) }
  7410. sub secret_id # secret string to grant access to a message with mail_id
  7411. { my $self=shift; !@_ ? $self->{secret_id} : ($self->{secret_id}=shift) }
  7412. sub attachment_password # scrambles a potentially dangerous released mail
  7413. { my $self=shift; !@_ ? $self->{release_pwd}: ($self->{release_pwd}=shift) }
  7414. sub msg_size # ESMTP SIZE value, later corrected to actual size,RFC 1870
  7415. { my $self=shift; !@_ ? $self->{msg_size} : ($self->{msg_size}=shift) }
  7416. sub auth_user # ESMTP AUTH username
  7417. { my $self=shift; !@_ ? $self->{auth_user} : ($self->{auth_user}=shift) }
  7418. sub auth_pass # ESMTP AUTH password
  7419. { my $self=shift; !@_ ? $self->{auth_pass} : ($self->{auth_pass}=shift) }
  7420. sub auth_submitter # ESMTP MAIL command AUTH option value (addr-spec or "<>")
  7421. { my $self=shift; !@_ ? $self->{auth_subm} : ($self->{auth_subm}=shift) }
  7422. sub tls_cipher # defined if TLS was on, e.g. contains cipher alg.,RFC 3207
  7423. { my $self=shift; !@_ ? $self->{auth_tlscif}: ($self->{auth_tlscif}=shift) }
  7424. sub dsn_ret # ESMTP MAIL command RET option (DSN-RFC 3461)
  7425. { my $self=shift; !@_ ? $self->{dsn_ret} : ($self->{dsn_ret}=shift) }
  7426. sub dsn_envid # ESMTP MAIL command ENVID option (DSN-RFC 3461) xtext enc.
  7427. { my $self=shift; !@_ ? $self->{dsn_envid} : ($self->{dsn_envid}=shift) }
  7428. sub dsn_passed_on # obligation to send notification on SUCCESS was relayed
  7429. { my $self=shift; !@_ ? $self->{dsn_pass_on}: ($self->{dsn_pass_on}=shift) }
  7430. sub requested_by # Resent-From addr who requested release from a quarantine
  7431. { my $self=shift; !@_ ? $self->{requested_by}:($self->{requested_by}=shift)}
  7432. sub body_type # ESMTP BODY param (RFC 1652: 7BIT, 8BITMIME) or BINARYMIME
  7433. { my $self=shift; !@_ ? $self->{body_type} : ($self->{body_type}=shift) }
  7434. sub header_8bit # true if header contains characters with code above 255
  7435. { my $self=shift; !@_ ? $self->{header_8bit}: ($self->{header_8bit}=shift) }
  7436. sub body_8bit # true if body contains chars with code above 255
  7437. { my $self=shift; !@_ ? $self->{body_8bit}: ($self->{body_8bit}=shift) }
  7438. sub sender # envelope sender, internal form, e.g.: j doe@example.com
  7439. { my $self=shift; !@_ ? $self->{sender} : ($self->{sender}=shift) }
  7440. sub sender_smtp # env sender, SMTP form in <>, e.g.: <"j doe"@example.com>
  7441. { my $self=shift; !@_ ? $self->{sender_smtp}: ($self->{sender_smtp}=shift) }
  7442. sub sender_credible # envelope sender is believed to be valid
  7443. { my $self=shift; !@_ ? $self->{sender_cred}: ($self->{sender_cred}=shift) }
  7444. sub sender_source # unmangled sender addr. or info from the trace (log/notif)
  7445. { my $self=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) }
  7446. sub sender_maddr_id # maddr.id field from SQL if logging to SQL is enabled
  7447. { my $self=shift; !@_ ? $self->{maddr_id} : ($self->{maddr_id}=shift) }
  7448. sub mime_entity # MIME::Parser entity holding the parsed message
  7449. { my $self=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)}
  7450. sub parts_root # Amavis::Unpackers::Part root object
  7451. { my $self=shift; !@_ ? $self->{parts_root} : ($self->{parts_root}=shift)}
  7452. sub skip_bytes # file offset where mail starts, useful for quar. release
  7453. { my $self=shift; !@_ ? $self->{file_ofs} : ($self->{file_ofs}=shift) }
  7454. sub mail_text # RFC 5322 msg: open file handle, or MIME::Entity object
  7455. { my $self=shift; !@_ ? $self->{mail_text} : ($self->{mail_text}=shift) }
  7456. sub mail_text_str # RFC 5322 msg: small messages as a stringref, else undef
  7457. { my $self=shift; !@_ ? $self->{mailtextstr}: ($self->{mailtextstr}=shift) }
  7458. sub mail_text_fn # orig. mail filename or undef, e.g. mail_tempdir/email.txt
  7459. { my $self=shift; !@_ ? $self->{mailtextfn} : ($self->{mailtextfn}=shift) }
  7460. sub mail_tempdir # work directory, under $TEMPBASE or supplied by client
  7461. { my $self=shift; !@_ ? $self->{mailtempdir}: ($self->{mailtempdir}=shift)}
  7462. sub mail_tempdir_obj # Amavis::TempDir obj when non-persistent (quar.release)
  7463. { my $self=shift; !@_ ? $self->{tempdirobj}: ($self->{tempdirobj}=shift)}
  7464. sub header_edits # Amavis::Out::EditHeader object or undef
  7465. { my $self=shift; !@_ ? $self->{hdr_edits} : ($self->{hdr_edits}=shift) }
  7466. sub rfc2822_from #author addresses list (rfc allows one or more), parsed 'From'
  7467. { my $self=shift; !@_ ? $self->{hdr_from} : ($self->{hdr_from}=shift) }
  7468. sub rfc2822_sender # sender address (rfc allows none or one), parsed 'Sender'
  7469. { my $self=shift; !@_ ? $self->{hdr_sender} : ($self->{hdr_sender}=shift) }
  7470. sub rfc2822_resent_from # resending author addresses list, parsed 'Resent-From'
  7471. { my $self=shift; !@_ ? $self->{hdr_rfrom} : ($self->{hdr_rfrom}=shift) }
  7472. sub rfc2822_resent_sender # resending sender addresses, parsed 'Resent-Sender'
  7473. { my $self=shift; !@_ ? $self->{hdr_rsender}: ($self->{hdr_rsender}=shift) }
  7474. sub rfc2822_to # parsed 'To' header field: a list of recipients
  7475. { my $self=shift; !@_ ? $self->{hdr_to} : ($self->{hdr_to}=shift) }
  7476. sub rfc2822_cc # parsed 'Cc' header field: a list of Cc recipients
  7477. { my $self=shift; !@_ ? $self->{hdr_cc} : ($self->{hdr_cc}=shift) }
  7478. sub orig_header_fields # header field indices by h.f. name, hashref of arrays
  7479. { my $self=shift; !@_ ? $self->{orig_hdr_f} : ($self->{orig_hdr_f}=shift) }
  7480. sub orig_header # orig.h.sect, arrayref of h.fields, with folding & trailing LF
  7481. { my $self=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) }
  7482. sub orig_header_size # size of original header, incl. a separator line,RFC 1870
  7483. { my $self=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) }
  7484. sub orig_body_size # size of original body (in bytes), RFC 1870
  7485. { my $self=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) }
  7486. sub body_start_pos # byte offset into a msg where mail body starts (if known)
  7487. { my $self=shift; !@_ ? $self->{body_pos}: ($self->{body_pos}=shift) }
  7488. sub body_digest # digest of a message body (e.g. MD5, SHA1, SHA256), hex
  7489. { my $self=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) }
  7490. sub is_mlist # mail is from a mailing list (boolean/string)
  7491. { my $self=shift; !@_ ? $self->{is_mlist} : ($self->{is_mlist}=shift) }
  7492. sub is_auto # mail is an auto-response (boolean/string)
  7493. { my $self=shift; !@_ ? $self->{is_auto} : ($self->{is_auto}=shift) }
  7494. sub is_bulk # mail from a m.list or bulk or auto-response (bool/string)
  7495. { my $self=shift; !@_ ? $self->{is_bulk} : ($self->{is_bulk}=shift) }
  7496. sub dkim_signatures_all # a ref to a list of DKIM signature objects, or undef
  7497. { my $self=shift; !@_ ? $self->{dkim_sall} : ($self->{dkim_sall}=shift) }
  7498. sub dkim_signatures_valid # a ref to a list of valid DKIM signature objects
  7499. { my $self=shift; !@_ ? $self->{dkim_sval} : ($self->{dkim_sval}=shift) }
  7500. sub dkim_author_sig # author domain signature present and valid (bool/domain)
  7501. { my $self=shift; !@_ ? $self->{dkim_auth_s}: ($self->{dkim_auth_s}=shift) }
  7502. sub dkim_thirdparty_sig # third-party signature present and valid (bool/domain)
  7503. { my $self=shift; !@_ ? $self->{dkim_3rdp_s}: ($self->{dkim_3rdp_s}=shift) }
  7504. sub dkim_sender_sig # a sender signature is present and is valid (bool/domain)
  7505. { my $self=shift; !@_ ? $self->{dkim_sndr_s}: ($self->{dkim_sndr_s}=shift) }
  7506. sub dkim_envsender_sig # boolean: envelope sender signature present and valid
  7507. { my $self=shift; !@_ ? $self->{dkim_envs_s}: ($self->{dkim_envs_s}=shift) }
  7508. sub dkim_signatures_new # ref to a list of DKIM signature objects, our signing
  7509. { my $self=shift; !@_ ? $self->{dkim_snew} : ($self->{dkim_snew}=shift) }
  7510. sub dkim_signwith_sd # ref to a pair [selector,domain] to force signing with
  7511. { my $self=shift; !@_ ? $self->{dkim_signsd}: ($self->{dkim_signsd}=shift) }
  7512. sub quarantined_to # list of quar mailbox names or addresses if quarantined
  7513. { my $self=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) }
  7514. sub quar_type # list of quar types: F/Z/B/Q/M (file/zipfile/bsmtp/sql/mailbox)
  7515. { my $self=shift; !@_ ? $self->{quar_type} : ($self->{quar_type}=shift) }
  7516. sub dsn_sent # delivery status notification was sent(1) or suppressed(2)
  7517. { my $self=shift; !@_ ? $self->{dsn_sent} : ($self->{dsn_sent}=shift) }
  7518. sub client_delete # don't delete the tempdir, it is a client's responsibility
  7519. { my $self=shift; !@_ ? $self->{client_del} :($self->{client_del}=shift)}
  7520. sub contents_category # sorted arrayref CC_VIRUS/CC_BANNED/CC_SPAM../CC_CLEAN
  7521. { my $self=shift; !@_ ? $self->{category} : ($self->{category}=shift) }
  7522. sub blocking_ccat # category type most responsible for blocking msg, or undef
  7523. { my $self=shift; !@_ ? $self->{bl_ccat} : ($self->{bl_ccat}=shift) }
  7524. sub checks_performed # a hashref of checks done on a msg (for statistics/log)
  7525. { my $self=shift; !@_ ? $self->{checks_perf}: ($self->{checks_perf}=shift) }
  7526. sub actions_performed # listref, summarized actions & SMTP status, for logging
  7527. { my $self=shift; !@_ ? $self->{act_perf} : ($self->{act_perf}=shift) }
  7528. sub virusnames # a ref to a list of virus names detected, or undef
  7529. { my $self=shift; !@_ ? $self->{virusnames} : ($self->{virusnames}=shift) }
  7530. sub spam_report # SA terse report of tests hit (for header section reports)
  7531. { my $self=shift; !@_ ? $self->{spam_report} :($self->{spam_report}=shift)}
  7532. sub spam_summary # SA summary of tests hit for standard body reports
  7533. { my $self=shift; !@_ ? $self->{spam_summary}:($self->{spam_summary}=shift)}
  7534. # new style of providing additional information from checkers
  7535. sub supplementary_info { # holds a hash of tag/value pairs, such as SA get_tag
  7536. my $self=shift; my $key=shift;
  7537. !@_ ? $self->{info_tag}{$key} : ($self->{info_tag}{$key}=shift);
  7538. }
  7539. { no warnings 'once';
  7540. # the following methods apply on a per-message level as well, summarizing
  7541. # per-recipient information as far as possible
  7542. *add_contents_category =
  7543. \&Amavis::In::Message::PerRecip::add_contents_category;
  7544. *is_in_contents_category =
  7545. \&Amavis::In::Message::PerRecip::is_in_contents_category;
  7546. *setting_by_main_contents_category =
  7547. \&Amavis::In::Message::PerRecip::setting_by_main_contents_category;
  7548. *setting_by_main_contents_category_all =
  7549. \&Amavis::In::Message::PerRecip::setting_by_main_contents_category_all;
  7550. *setting_by_blocking_contents_category =
  7551. \&Amavis::In::Message::PerRecip::setting_by_blocking_contents_category;
  7552. *setting_by_contents_category =
  7553. \&Amavis::In::Message::PerRecip::setting_by_contents_category;
  7554. }
  7555. # The order of entries in a per-recipient list is the original order
  7556. # in which recipient addresses (e.g. obtained via 'MAIL TO:') were received.
  7557. # Only the entries that were accepted (via SMTP response code 2xx)
  7558. # are placed in the list. The ORDER MUST BE PRESERVED and no recipients
  7559. # may be added or removed from the list (without precaution)! This is vital
  7560. # to be able to produce correct per-recipient responses to an LMTP client!
  7561. #
  7562. sub per_recip_data { # get or set a listref of envelope recipient objects
  7563. my $self = shift;
  7564. # store a copy of the a given listref of recip objects
  7565. if (@_) { $self->{recips} = [@{$_[0]}] }
  7566. # caller may modify data if he knows what he is doing
  7567. $self->{recips}; # return a list of recipient objects
  7568. }
  7569. sub recips { # get or set a listref of envelope recipients
  7570. my $self = shift;
  7571. if (@_) { # store a copy of a given listref of recipient addresses
  7572. my($recips_list_ref, $set_dsn_orcpt_too) = @_;
  7573. $self->per_recip_data([ map {
  7574. my $per_recip_obj = Amavis::In::Message::PerRecip->new;
  7575. $per_recip_obj->recip_addr($_);
  7576. $per_recip_obj->recip_addr_smtp(qquote_rfc2821_local($_));
  7577. $per_recip_obj->dsn_orcpt(orcpt_encode($per_recip_obj->recip_addr_smtp))
  7578. if $set_dsn_orcpt_too;
  7579. $per_recip_obj->recip_destiny(D_PASS); # default is Pass
  7580. $per_recip_obj } @{$recips_list_ref} ]);
  7581. }
  7582. return if !defined wantarray; # don't bother
  7583. # return listref of recipient addresses
  7584. [ map($_->recip_addr, @{$self->per_recip_data}) ];
  7585. }
  7586. # for each header field maintain a list of signature indices which covered it;
  7587. # returns a list of signature indices for a given header field position
  7588. #
  7589. sub header_field_signed_by {
  7590. my($self,$header_field_index) = @_; shift; shift;
  7591. my $h = $self->{hdr_sig_ind}; my $hf;
  7592. if (@_) {
  7593. $self->{hdr_sig_ind} = $h = [] if !$h;
  7594. $hf = $h->[$header_field_index];
  7595. $h->[$header_field_index] = $hf = [] if !$hf;
  7596. push(@$hf, @_); # store signature index(es) at a given header position
  7597. }
  7598. $hf = $h->[$header_field_index] if $h && !$hf;
  7599. $hf ? @{$hf} : ();
  7600. }
  7601. # return a j-th header field with a given field name, along with its index
  7602. # in the array of all header fields; if a field name is undef then all
  7603. # header fields are considered; search proceeds top-down if j >= 0,
  7604. # or bottom up for negative values (-1=last, -2=next-to-last, ...)
  7605. #
  7606. sub get_header_field2 {
  7607. my($self, $field_name, $j) = @_;
  7608. my($field_ind, $field, $all_fields, $hfield_indices);
  7609. $hfield_indices = # arrayref of h.field indices for a given h.field name
  7610. $self->orig_header_fields->{lc $field_name} if defined $field_name;
  7611. $all_fields = $self->orig_header;
  7612. if (defined $field_name) {
  7613. if (!defined $hfield_indices) {
  7614. # no header field with such name
  7615. } elsif (ref $hfield_indices) {
  7616. # $hfield_indices is an arrayref
  7617. $j = 0 if !defined $j;
  7618. $field_ind = $hfield_indices->[$j];
  7619. } else {
  7620. # optimized: $hfield_indices is a scalar - the only element
  7621. $field_ind = $hfield_indices if !defined($j) || $j == 0 || $j == -1;
  7622. }
  7623. } elsif (!ref $all_fields) {
  7624. # no header section
  7625. } elsif ($j >= 0) { # top-down, 0,1,2,...
  7626. $field_ind = $j if $j <= $#$all_fields;
  7627. } else { # bottom-up, -1,-2,-3,...
  7628. $j += @$all_fields; # turn into an absolute index
  7629. $field_ind = $j if $j >= 0;
  7630. }
  7631. return $field_ind if !wantarray;
  7632. ($field_ind, !defined $field_ind ? undef : $all_fields->[$field_ind]);
  7633. }
  7634. # compatibility wrapper for pre-2.8.0 custom code
  7635. #
  7636. sub get_header_field {
  7637. my($self, $field_name, $j) = @_;
  7638. my($field_ind, $field) = $self->get_header_field2($field_name,$j);
  7639. if (defined($field_ind) && wantarray) {
  7640. local $1;
  7641. $field_name = lc($1) if $field =~ /^([^:]*?)[ \t]*:/s;
  7642. }
  7643. !wantarray ? $field_ind : ($field_ind, $field_name, $field);
  7644. }
  7645. sub get_header_field_body {
  7646. my($self, $field_name, $j) = @_;
  7647. my $k; my($field_ind, $f) = $self->get_header_field2($field_name,$j);
  7648. defined $f && ($k=index($f,':')) >= 0 ? substr($f,$k+1) : $f;
  7649. }
  7650. 1;
  7651. #
  7652. package Amavis::Out::EditHeader;
  7653. # Accumulates instructions on what header fields need to be added
  7654. # to a header section, which deleted, or how to change existing ones.
  7655. # A call to write_header() then performs these edits on the fly.
  7656. use strict;
  7657. use re 'taint';
  7658. BEGIN {
  7659. require Exporter;
  7660. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  7661. $VERSION = '2.316';
  7662. @ISA = qw(Exporter);
  7663. @EXPORT_OK = qw(&hdr);
  7664. import Amavis::Conf qw(:platform c cr ca);
  7665. import Amavis::Timing qw(section_time);
  7666. import Amavis::rfc2821_2822_Tools qw(wrap_string);
  7667. import Amavis::Util qw(ll do_log min max q_encode
  7668. safe_encode safe_encode_ascii safe_encode_utf8);
  7669. }
  7670. use MIME::Words;
  7671. use Errno qw(EBADF);
  7672. sub new {
  7673. my($class) = @_;
  7674. bless { prepend=>[], append=>[], addrcvd=>[], edit=>{} }, $class;
  7675. }
  7676. sub prepend_header($$$;$) {
  7677. my($self, $field_name, $field_body, $structured) = @_;
  7678. unshift(@{$self->{prepend}}, hdr($field_name,$field_body,$structured));
  7679. }
  7680. sub append_header($$$;$) {
  7681. my($self, $field_name, $field_body, $structured) = @_;
  7682. push(@{$self->{append}}, hdr($field_name,$field_body,$structured));
  7683. }
  7684. sub append_header_above_received($$$;$) {
  7685. my($self, $field_name, $field_body, $structured) = @_;
  7686. push(@{$self->{addrcvd}}, hdr($field_name,$field_body,$structured));
  7687. }
  7688. # now a synonym for append_header_above_received() (old semantics: prepend
  7689. # or append, depending on setting of $append_header_fields_to_bottom)
  7690. #
  7691. sub add_header($$$;$) {
  7692. my($self, $field_name, $field_body, $structured) = @_;
  7693. push(@{$self->{addrcvd}}, hdr($field_name,$field_body,$structured));
  7694. }
  7695. # delete all header fields with a $field_name
  7696. #
  7697. sub delete_header($$) {
  7698. my($self, $field_name) = @_;
  7699. $self->{edit}{lc($field_name)} = [undef];
  7700. }
  7701. # all header fields with $field_name will be edited by a supplied subroutine
  7702. #
  7703. sub edit_header($$$;$) {
  7704. my($self, $field_name, $field_edit_sub, $structured) = @_;
  7705. # $field_edit_sub will be called with 2 args: a field name and a field body;
  7706. # It should return a pair consisting of a replacement field body (no field
  7707. # name and no colon, with or without a trailing NL), and a boolean 'verbatim'
  7708. # (false in its absence). An undefined replacement field body indicates a
  7709. # deletion of the entire header field. A value true in the second returned
  7710. # element indicates that a verbatim replacement is desired (i.e. no other
  7711. # changes are allowed on a replacement body such as folding or encoding).
  7712. !defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE'
  7713. or die "edit_header: arg#3 must be undef or a subroutine ref";
  7714. $field_name = lc($field_name);
  7715. if (!exists($self->{edit}{$field_name})) {
  7716. $self->{edit}{$field_name} = [$field_edit_sub];
  7717. } else {
  7718. do_log(5, "INFO: multiple header edits: %s", $field_name);
  7719. push(@{$self->{edit}{$field_name}}, $field_edit_sub);
  7720. }
  7721. }
  7722. # copy all header edits from another header-edits object into this one
  7723. #
  7724. sub inherit_header_edits($$) {
  7725. my($self, $other_edits) = @_;
  7726. if (defined $other_edits) {
  7727. for (qw(prepend addrcvd append)) {
  7728. unshift(@{$self->{$_}}, @{$other_edits->{$_}}) if $other_edits->{$_};
  7729. }
  7730. my $o_edit = $other_edits->{edit};
  7731. if ($o_edit) {
  7732. for my $fn (keys %$o_edit) {
  7733. if (!exists($self->{edit}{$fn})) {
  7734. $self->{edit}{$fn} = [ @{$o_edit->{$fn}} ]; # copy list
  7735. } else {
  7736. unshift(@{$self->{edit}{$fn}}, @{$o_edit->{$fn}});
  7737. }
  7738. }
  7739. }
  7740. }
  7741. }
  7742. # Conditioning of a header field to be added.
  7743. # Insert space after colon if not present, RFC 2047 -encode if field body
  7744. # contains non-ASCII characters, fold long lines if needed, prepend space
  7745. # before each NL if missing, append NL if missing. Header lines with only
  7746. # spaces are not allowed. (RFC 5322: Each line of characters MUST be no more
  7747. # than 998 characters, and SHOULD be no more than 78 characters, excluding
  7748. # the CRLF). $structured==0 indicates an unstructured header field,
  7749. # folding may be inserted at any existing whitespace character position;
  7750. # $structured==1 indicates that folding is only allowed at positions
  7751. # indicated by \n in the provided header body, original \n will be removed.
  7752. # With $structured==2 folding is preserved, wrapping step is skipped.
  7753. #
  7754. sub hdr($$$;$) {
  7755. my($field_name, $field_body, $structured, $wrap_char) = @_;
  7756. $wrap_char = "\t" if !defined $wrap_char;
  7757. local($1);
  7758. if ($field_name =~ /^ (?: Subject\z | Comments\z |
  7759. X- (?! Envelope- (?:From|To)\z ) )/six &&
  7760. $field_body !~ /^[\t\n\040-\176]*\z/ # not all printable (or TAB or LF)
  7761. ) { # encode according to RFC 2047
  7762. # actually RFC 2047 also allows encoded-words in rfc822 extension
  7763. # message header fields (now: optional header fields), within comments
  7764. # in structured header fields, or within 'phrase' (e.g. in From, To, Cc);
  7765. # we are being sloppy here!
  7766. $field_body =~ s/\n(?=[ \t])//gs; # unfold
  7767. chomp($field_body);
  7768. my $field_body_octets;
  7769. my $chset = c('hdr_encoding'); my $qb = c('hdr_encoding_qb');
  7770. $field_body_octets = safe_encode($chset, $field_body);
  7771. # do_log(5, "hdr - UTF-8 body: %s", $field_body);
  7772. # do_log(5, "hdr - body octets: %s", $field_body_octets);
  7773. my $encoder_func = uc($qb) eq 'Q' ? \&q_encode
  7774. : \&MIME::Words::encode_mimeword;
  7775. $field_body = join("\n", map { /^[\001-\011\013\014\016-\177]*\z/ ? $_
  7776. : &$encoder_func($_,$qb,$chset) }
  7777. split(/\n/, $field_body_octets, -1));
  7778. } else { # supposed to be in plain ASCII, let's make sure it is
  7779. $field_body = safe_encode_ascii($field_body);
  7780. }
  7781. $field_name = safe_encode_ascii($field_name);
  7782. my $str = $field_name . ':';
  7783. $str .= ' ' if $field_body =~ /^[^ \t]/; # looks nicer
  7784. $str .= $field_body;
  7785. if ($structured == 2) { # already folded, keep it that way, sanitize
  7786. 1 while $str =~ s/^([ \t]*)\n/$1/; # prefixed by whitespace lines?
  7787. $str =~ s/\n(?=[ \t]*(\n|\z))//g; # whitespace lines within or at end
  7788. $str =~ s/\n(?![ \t])/\n /g; # insert a space at line folds if missing
  7789. } else {
  7790. $wrap_char = "\t" if !defined $wrap_char;
  7791. $str = wrap_string($str, 78, '', $wrap_char, $structured
  7792. ) if $structured==1 || length($str) > 78;
  7793. }
  7794. if (length($str) > 998) {
  7795. my(@lines) = split(/\n/,$str); my $trunc = 0;
  7796. for (@lines)
  7797. { if (length($_) > 998) { $_ = substr($_,0,998-3).'...'; $trunc = 1 } }
  7798. if ($trunc) {
  7799. do_log(0, "INFO: truncating long header field (len=%d): %s[...]",
  7800. length($str), substr($str,0,100) );
  7801. $str = join("\n",@lines);
  7802. }
  7803. }
  7804. $str =~ s{\n*\z}{\n}s; # ensure a single final NL
  7805. ll(5) && do_log(5, 'header: %s', $str);
  7806. $str;
  7807. }
  7808. # Copy mail header section to the supplied method while adding, removing,
  7809. # or changing certain header fields as required, and append an empty line
  7810. # (header/body separator). Returns a number of original 'Received:'
  7811. # header fields to make a simple loop detection possible (as required
  7812. # by RFC 5321 (ex RFC 2821) section 6.3).
  7813. # Leaves input file positioned at the beginning of a body.
  7814. #
  7815. sub write_header($$$$) {
  7816. my($self, $msginfo, $out_fh, $noninitial_submission) = @_;
  7817. my $received_cnt = 0;
  7818. my($fix_whitespace_lines, $fix_long_header_lines, $fix_bare_cr) = (0,0,0);
  7819. if ($noninitial_submission && c('allow_fixing_improper_header')) {
  7820. $fix_bare_cr = 1;
  7821. $fix_long_header_lines = 1 if c('allow_fixing_long_header_lines');
  7822. $fix_whitespace_lines = 1 if c('allow_fixing_improper_header_folding');
  7823. }
  7824. my(@header); my $pos = 0; my $header_in_array = 0;
  7825. my $msg = $msginfo->mail_text;
  7826. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  7827. $msg = $msg_str_ref if ref $msg_str_ref;
  7828. if (!defined $msg) {
  7829. # empty mail
  7830. $header_in_array = 1;
  7831. } elsif (ref $msg eq 'SCALAR') {
  7832. $header_in_array = 1;
  7833. $pos = min($msginfo->skip_bytes, length($$msg));
  7834. if ($pos >= length($$msg)) { # empty message
  7835. $pos = length($$msg);
  7836. } elsif (substr($$msg,$pos,1) eq "\n") { # empty header section
  7837. $pos++;
  7838. } else {
  7839. my $ind = index($$msg, "\n\n", $pos); # find header/body separator
  7840. if ($ind < 0) { # no body
  7841. @header = split(/^/m, substr($$msg, $pos));
  7842. $pos = length($$msg);
  7843. } else { # normal, nonempty header section and nonempty body
  7844. @header = split(/^/m, substr($$msg, $pos, $ind+1-$pos));
  7845. $pos = $ind+2;
  7846. }
  7847. }
  7848. # $pos now points to the first byte of a body
  7849. } elsif ($msg->isa('MIME::Entity')) {
  7850. $header_in_array = 1;
  7851. $fix_whitespace_lines = 1; # fix MIME::Entity artifacts
  7852. @header = @{$msg->header};
  7853. } else { # a file handle assumed
  7854. $pos = $msginfo->skip_bytes;
  7855. $msg->seek($pos,0) or die "Can't rewind mail file: $!";
  7856. }
  7857. ll(5) && do_log(5, 'write_header: %s, %s', $header_in_array, $out_fh);
  7858. # preallocate some storage
  7859. my $str = ''; vec($str,8192,8) = 0; $str = '';
  7860. $str .= $_ for @{$self->{prepend}};
  7861. $str .= $_ for @{$self->{addrcvd}};
  7862. my($ill_white_cnt, $ill_long_cnt, $ill_bare_cr) = (0,0,0);
  7863. local($1,$2); my $curr_head; my $next_head; my $eof = 0;
  7864. for (;;) {
  7865. if ($eof) {
  7866. $next_head = "\n"; # fake a missing header/body separator line
  7867. } elsif ($header_in_array) {
  7868. for (;;) { # get next nonempty line or eof
  7869. if (!@header) { $eof = 1; $next_head = "\n"; last }
  7870. $next_head = shift @header;
  7871. # ensure NL at end, faster than m/\n\z/
  7872. $next_head .= "\n" if substr($next_head,-1,1) ne "\n";
  7873. last if !$fix_whitespace_lines || $next_head !~ /^[ \t]*\n\z/s;
  7874. $ill_white_cnt++;
  7875. }
  7876. } else {
  7877. $! = 0; $next_head = $msg->getline;
  7878. if (defined $next_head) {
  7879. $pos += length($next_head);
  7880. } else {
  7881. $eof = 1; $next_head = "\n";
  7882. $! == 0 or # returning EBADF at EOF is a perl bug
  7883. $! == EBADF ? do_log(0,"Error reading mail header section: $!")
  7884. : die "Error reading mail header section: $!";
  7885. }
  7886. }
  7887. if ($next_head =~ /^[ \t]/) {
  7888. $curr_head .= $next_head; # folded
  7889. } else { # new header field
  7890. if (!defined($curr_head)) {
  7891. # no previous complete header field (we are at the first hdr field)
  7892. } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) { # parse
  7893. # invalid header field, but we'll write it anyway
  7894. } else { # count, edit, or delete
  7895. # obsolete RFC 822 syntax allowed whitespace before colon
  7896. my($field_name, $field_body) = ($1, $2);
  7897. my $field_name_lc = lc($field_name);
  7898. $received_cnt++ if $field_name_lc eq 'received';
  7899. if (exists($self->{edit}{$field_name_lc})) {
  7900. chomp($field_body);
  7901. ### $field_body =~ s/\n(?=[ \t])//gs; # unfold
  7902. my $edit = $self->{edit}{$field_name_lc}; # listref of edits
  7903. for my $e (@$edit) { # possibly multiple (iterative) edits
  7904. my($new_fbody,$verbatim);
  7905. ($new_fbody,$verbatim) =
  7906. &$e($field_name,$field_body) if defined $e;
  7907. if (!defined($new_fbody)) {
  7908. ll(5) && do_log(5, 'deleted: %s:%s', $field_name, $field_body);
  7909. $curr_head = undef; last;
  7910. }
  7911. $curr_head = $verbatim ? ($field_name . ':' . $new_fbody)
  7912. : hdr($field_name, $new_fbody, 0);
  7913. chomp($curr_head); $curr_head .= "\n";
  7914. $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s;
  7915. $field_body = $2; chomp($field_body); # carry to next iteration
  7916. }
  7917. }
  7918. }
  7919. if (defined $curr_head) {
  7920. if ($fix_bare_cr) { # sanitize header sect. by removing CR characters
  7921. $curr_head =~ tr/\r//d and $ill_bare_cr++;
  7922. }
  7923. if ($fix_whitespace_lines) { # unfold illegal all-whitespace lines
  7924. $curr_head =~ s/\n(?=[ \t]*\n)//g and $ill_white_cnt++;
  7925. }
  7926. if ($fix_long_header_lines) { # truncate long header lines to 998 ch
  7927. $curr_head =~ s{^(.{995}).{4,}$}{$1...}mg and $ill_long_cnt++;
  7928. }
  7929. # use buffering to reduce number of calls to datasend()
  7930. if (length($str) > 16384) {
  7931. $out_fh->print($str) or die "sending mail header: $!";
  7932. $str = '';
  7933. }
  7934. $str .= $curr_head;
  7935. }
  7936. last if $next_head eq "\n"; # header/body separator
  7937. last if substr($next_head,0,2) eq '--'; # mime sep. (missing h/b sep.)
  7938. $curr_head = $next_head;
  7939. }
  7940. }
  7941. do_log(0, "INFO: unfolded %d illegal all-whitespace ".
  7942. "continuation lines", $ill_white_cnt) if $ill_white_cnt;
  7943. do_log(0, "INFO: truncated %d header line(s) longer than 998 characters",
  7944. $ill_long_cnt) if $ill_long_cnt;
  7945. do_log(0, "INFO: removed bare CR from %d header line(s)",
  7946. $ill_bare_cr) if $ill_bare_cr;
  7947. $str .= $_ for @{$self->{append}};
  7948. $str .= "\n"; # end of header section - a separator line
  7949. $out_fh->print($str) or die "sending mail header final: $!";
  7950. section_time('write-header');
  7951. ($received_cnt, $pos);
  7952. }
  7953. 1;
  7954. #
  7955. package Amavis::Out;
  7956. use strict;
  7957. use re 'taint';
  7958. BEGIN {
  7959. require Exporter;
  7960. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  7961. $VERSION = '2.316';
  7962. @ISA = qw(Exporter);
  7963. @EXPORT = qw(&mail_dispatch);
  7964. import Amavis::Conf qw(:platform :confvars c cr ca);
  7965. import Amavis::Util qw(ll do_log);
  7966. }
  7967. sub mail_dispatch($$$;$) {
  7968. my($msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_;
  7969. my $tmp_hdr_edits;
  7970. my $saved_hdr_edits = $msginfo->header_edits;
  7971. if (!c('enable_dkim_signing')) {
  7972. # no signing
  7973. } elsif ($initial_submission && $initial_submission eq 'Quar') {
  7974. # do not attempt to sign messages on their way to a quarantine
  7975. } else {
  7976. # generate and add DKIM signatures
  7977. my(@signatures) = Amavis::DKIM::dkim_make_signatures($msginfo,0);
  7978. if (@signatures) {
  7979. $msginfo->dkim_signatures_new(\@signatures);
  7980. if (!defined($tmp_hdr_edits)) {
  7981. $tmp_hdr_edits = Amavis::Out::EditHeader->new;
  7982. $tmp_hdr_edits->inherit_header_edits($saved_hdr_edits);
  7983. }
  7984. for my $signature (@signatures) {
  7985. my $s = $signature->as_string;
  7986. local($1); $s =~ s{\015\012}{\n}gs; $s =~ s{\n+\z}{}gs;
  7987. $s =~ s/^((?:DKIM|DomainKey)-Signature)://si;
  7988. $tmp_hdr_edits->prepend_header($1, $s, 2);
  7989. }
  7990. if (c('enable_dkim_verification') &&
  7991. grep($_->recip_is_local, @{$msginfo->per_recip_data})) {
  7992. # it is too late to split a message now, add the A-R header field
  7993. # if at least one recipient is local
  7994. my $allowed_hdrs = cr('allowed_added_header_fields');
  7995. if ($allowed_hdrs && $allowed_hdrs->{lc('Authentication-Results')}) {
  7996. for my $h (Amavis::DKIM::generate_authentication_results(
  7997. $msginfo, 0, \@signatures)) {
  7998. $tmp_hdr_edits->prepend_header('Authentication-Results', $h, 1);
  7999. }
  8000. }
  8001. }
  8002. }
  8003. $msginfo->header_edits($tmp_hdr_edits) if defined $tmp_hdr_edits;
  8004. }
  8005. my $any_deliveries = 0;
  8006. my $per_recip_data = $msginfo->per_recip_data;
  8007. my $num_recips_notdone =
  8008. scalar(grep(!$_->recip_done && (!$filter || &$filter($_)),
  8009. @$per_recip_data));
  8010. while ($num_recips_notdone > 0) {
  8011. # a delivery method may be a scalar of a form protocol:socket_specs, or
  8012. # a listref of such elements; if a list is provided, it is expected that
  8013. # each entry will be using the same protocol name, otherwise behaviour
  8014. # is unspecified - so just obtain the protocol name from the first entry
  8015. #
  8016. my(%protocols,$any_tempfail);
  8017. for my $r (@$per_recip_data) {
  8018. if (!$dsn_per_recip_capable) {
  8019. my $recip_smtp_response = $r->recip_smtp_response; # any 4xx code ?
  8020. if (defined($recip_smtp_response) && $recip_smtp_response =~ /^4/) {
  8021. $any_tempfail = $recip_smtp_response . ' (' . $r->recip_addr . ')';
  8022. }
  8023. }
  8024. if (!$r->recip_done && (!$filter || &$filter($r))) {
  8025. my $proto_sockname = $r->delivery_method;
  8026. defined $proto_sockname
  8027. or die "mail_dispatch: undefined delivery_method";
  8028. !ref $proto_sockname || ref $proto_sockname eq 'ARRAY'
  8029. or die "mail_dispatch: not a scalar or array ref: $proto_sockname";
  8030. for (ref $proto_sockname ? @$proto_sockname : $proto_sockname) {
  8031. local($1);
  8032. if (/^([a-z][a-z0-9.+-]*):/si) { $protocols{lc($1)} = 1 }
  8033. else { die "mail_dispatch: no recognized protocol name: $_" }
  8034. }
  8035. }
  8036. }
  8037. my(@unknown) =
  8038. grep(!/^(?:smtp|lmtp|pipe|bsmtp|sql|local)\z/i, keys %protocols);
  8039. !@unknown or die "mail_dispatch: unknown protocol: ".join(', ',@unknown);
  8040. if (!$dsn_per_recip_capable && defined $any_tempfail) {
  8041. do_log(0, "temporary failures, giving up further deliveries: %s",
  8042. $any_tempfail);
  8043. my $smtp_resp =
  8044. "451 4.5.0 Giving up due to previous temporary failures, id=" .
  8045. $msginfo->log_id;
  8046. # flag the remaining undelivered recipients as temporary failures
  8047. for my $r (@$per_recip_data) {
  8048. next if $r->recip_done;
  8049. $r->recip_smtp_response($smtp_resp); $r->recip_done(1);
  8050. }
  8051. last;
  8052. }
  8053. # do one protocol per iteration only, so that we can bail out
  8054. # as soon as some 4xx temporary failure is detected, avoiding
  8055. # further deliveries which would lead to duplicate deliveries
  8056. #
  8057. if ($protocols{'smtp'} || $protocols{'lmtp'}) {
  8058. Amavis::Out::SMTP::mail_via_smtp(@_);
  8059. $any_deliveries = 1; # approximation, will do for the time being
  8060. } elsif ($protocols{'local'}) {
  8061. Amavis::Out::Local::mail_to_local_mailbox(@_);
  8062. $any_deliveries = 1; # approximation, will do for the time being
  8063. } elsif ($protocols{'pipe'}) {
  8064. Amavis::Out::Pipe::mail_via_pipe(@_);
  8065. $any_deliveries = 1; # approximation, will do for the time being
  8066. } elsif ($protocols{'bsmtp'}) {
  8067. Amavis::Out::BSMTP::mail_via_bsmtp(@_);
  8068. $any_deliveries = 1; # approximation, will do for the time being
  8069. } elsif ($protocols{'sql'}) {
  8070. $Amavis::extra_code_sql_quar && $Amavis::sql_storage
  8071. or die "SQL quarantine code not enabled (1)";
  8072. Amavis::Out::SQL::Quarantine::mail_via_sql(
  8073. $Amavis::sql_dataset_conn_storage, @_);
  8074. $any_deliveries = 1; # approximation, will do for the time being
  8075. }
  8076. # are we done yet?
  8077. my $num_recips_notdone_after =
  8078. scalar(grep(!$_->recip_done && (!$filter || &$filter($_)),
  8079. @$per_recip_data));
  8080. if ($num_recips_notdone_after >= $num_recips_notdone) {
  8081. do_log(-2, "TROUBLE: Number of recipients (%d) not reduced, ".
  8082. "abandoning effort, proto: %s",
  8083. $num_recips_notdone_after, join(', ', keys %protocols) );
  8084. last;
  8085. }
  8086. if ($num_recips_notdone_after > 0) {
  8087. do_log(3, "Sent to %s recipients, %s still to go",
  8088. $num_recips_notdone - $num_recips_notdone_after,
  8089. $num_recips_notdone_after);
  8090. }
  8091. $num_recips_notdone = $num_recips_notdone_after;
  8092. }
  8093. # restore header edits if modified
  8094. $msginfo->header_edits($saved_hdr_edits) if defined $tmp_hdr_edits;
  8095. $any_deliveries; # (estimate) were any successful deliveries actually done?
  8096. }
  8097. 1;
  8098. #
  8099. package Amavis::UnmangleSender;
  8100. use strict;
  8101. use re 'taint';
  8102. BEGIN {
  8103. require Exporter;
  8104. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8105. $VERSION = '2.316';
  8106. @ISA = qw(Exporter);
  8107. @EXPORT_OK = qw(&parse_ip_address_from_received &first_received_from);
  8108. import Amavis::Conf qw(:platform c cr ca);
  8109. import Amavis::Util qw(ll do_log unique_list);
  8110. import Amavis::rfc2821_2822_Tools qw(
  8111. split_address parse_received fish_out_ip_from_received);
  8112. import Amavis::Lookup qw(lookup lookup2);
  8113. import Amavis::Lookup::IP qw(lookup_ip_acl);
  8114. }
  8115. use subs @EXPORT_OK;
  8116. # Obtain and parse the first entry (oldest) in the 'Received:' header field
  8117. # path trace - to be used as the value of a macro %t in customized messages
  8118. #
  8119. sub first_received_from($) {
  8120. my($msginfo) = @_;
  8121. my $first_received;
  8122. my $fields_ref =
  8123. parse_received($msginfo->get_header_field_body('received')); # last
  8124. if (exists $fields_ref->{'from'}) {
  8125. $first_received = join(' ', unique_list(grep(defined($_),
  8126. @$fields_ref{qw(from from-tcp from-com)})));
  8127. do_log(5, "first_received_from: %s", $first_received);
  8128. }
  8129. $first_received;
  8130. }
  8131. # Try to extract sender's IP address from the Received trace.
  8132. # When $search_top_down is true: search top-down, use first valid IP address;
  8133. # otherwise, search bottom-up, use the first *public* IP address from the trace
  8134. #
  8135. use vars qw(@nonhostlocalnetworks_maps @publicnetworks_maps);
  8136. sub parse_ip_address_from_received($;$) {
  8137. my($msginfo,$search_top_down) = @_;
  8138. @publicnetworks_maps = (
  8139. Amavis::Lookup::Label->new('publicnetworks'),
  8140. Amavis::Lookup::IP->new(qw(
  8141. !0.0.0.0/8 !127.0.0.0/8 !169.254.0.0/16 !:: !::1 !FE80::/10
  8142. !172.16.0.0/12 !192.168.0.0/16 !10.0.0.0/8 !FEC0::/10
  8143. !192.88.99.0/24 !240.0.0.0/4 !224.0.0.0/4 !FF00::/8
  8144. ::FFFF:0:0/96 ::/0)) ) if !@publicnetworks_maps;
  8145. # RFC 5735 (ex RFC 3330), RFC 3513
  8146. my $received_from_ip;
  8147. my(@search_list) = $search_top_down ? (0,1) # the topmost two Received flds
  8148. : (-1,-2,-3,-4,-5,-6); # bottom-up, first six chronologically
  8149. for my $j (@search_list) { # walk through a list of Received field indices
  8150. my $r = $msginfo->get_header_field_body('received',$j);
  8151. last if !defined $r;
  8152. $received_from_ip = fish_out_ip_from_received($r);
  8153. if ($received_from_ip ne '') {
  8154. last if $search_top_down; # any valid address would do
  8155. my($is_public,$fullkey,$err) =
  8156. lookup_ip_acl($received_from_ip,@publicnetworks_maps);
  8157. last if (!defined($err) || $err eq '') && $is_public;
  8158. }
  8159. }
  8160. do_log(5, "parse_ip_address_from_received: %s", $received_from_ip);
  8161. $received_from_ip;
  8162. }
  8163. 1;
  8164. #
  8165. package Amavis::Unpackers::NewFilename;
  8166. use strict;
  8167. use re 'taint';
  8168. BEGIN {
  8169. require Exporter;
  8170. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8171. $VERSION = '2.316';
  8172. @ISA = qw(Exporter);
  8173. @EXPORT_OK = qw(&consumed_bytes);
  8174. import Amavis::Conf qw(c cr ca
  8175. $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
  8176. $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR);
  8177. import Amavis::Util qw(ll do_log min max minmax);
  8178. }
  8179. use vars qw($avail_quota); # available bytes quota for unpacked mail
  8180. use vars qw($rem_quota); # remaining bytes quota for unpacked mail
  8181. sub new($;$$) { # create a file name generator object
  8182. my($class, $maxfiles,$mail_size) = @_;
  8183. # calculate and initialize quota
  8184. $avail_quota = $rem_quota = # quota in bytes
  8185. max($MIN_EXPANSION_QUOTA, $mail_size * $MIN_EXPANSION_FACTOR,
  8186. min($MAX_EXPANSION_QUOTA, $mail_size * $MAX_EXPANSION_FACTOR));
  8187. do_log(4,"Original mail size: %d; quota set to: %d bytes",
  8188. $mail_size,$avail_quota);
  8189. # create object
  8190. bless {
  8191. num_of_issued_names => 0, first_issued_ind => 1, last_issued_ind => 0,
  8192. maxfiles => $maxfiles, # undef disables limit
  8193. objlist => [],
  8194. }, $class;
  8195. }
  8196. sub parts_list_reset($) { # clear a list of recently issued names
  8197. my $self = shift;
  8198. $self->{num_of_issued_names} = 0;
  8199. $self->{first_issued_ind} = $self->{last_issued_ind} + 1;
  8200. $self->{objlist} = [];
  8201. }
  8202. sub parts_list($) { # returns a ref to a list of recently issued names
  8203. my $self = shift;
  8204. $self->{objlist};
  8205. }
  8206. sub parts_list_add($$) { # add a parts object to the list of parts
  8207. my($self, $part) = @_;
  8208. push(@{$self->{objlist}}, $part);
  8209. }
  8210. sub generate_new_num($$) { # make-up a new number for a file and return it
  8211. my($self, $ignore_limit) = @_;
  8212. if (!$ignore_limit && defined($self->{maxfiles}) &&
  8213. $self->{num_of_issued_names} >= $self->{maxfiles}) {
  8214. # do not change the text in die without adjusting decompose_part()
  8215. die "Maximum number of files ($self->{maxfiles}) exceeded";
  8216. }
  8217. $self->{num_of_issued_names}++; $self->{last_issued_ind}++;
  8218. $self->{last_issued_ind};
  8219. }
  8220. sub consumed_bytes($$;$$) {
  8221. my($bytes, $bywhom, $tentatively, $exquota) = @_;
  8222. if (ll(4)) {
  8223. my $perc = !$avail_quota ? '' : sprintf(", (%.0f%%)",
  8224. 100 * ($avail_quota - ($rem_quota - $bytes)) / $avail_quota);
  8225. do_log(4,"Charging %d bytes to remaining quota %d (out of %d%s) - by %s",
  8226. $bytes, $rem_quota, $avail_quota, $perc, $bywhom);
  8227. }
  8228. if ($bytes > $rem_quota && $rem_quota >= 0) {
  8229. # Do not modify the following signal text, it gets matched elsewhere!
  8230. my $msg = "Exceeded storage quota $avail_quota bytes by $bywhom; ".
  8231. "last chunk $bytes bytes";
  8232. do_log(-1, "%s", $msg);
  8233. die "$msg\n" if !$exquota; # die, unless allowed to exceed quota
  8234. }
  8235. $rem_quota -= $bytes unless $tentatively;
  8236. $rem_quota; # return remaining quota
  8237. }
  8238. 1;
  8239. #
  8240. package Amavis::Unpackers::Part;
  8241. use strict;
  8242. use re 'taint';
  8243. BEGIN {
  8244. require Exporter;
  8245. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8246. $VERSION = '2.316';
  8247. @ISA = qw(Exporter);
  8248. import Amavis::Util qw(ll do_log);
  8249. }
  8250. use vars qw($file_generator_object);
  8251. sub init($) { $file_generator_object = shift }
  8252. sub new($;$$$) { # create a part descriptor object
  8253. my($class, $dir_name,$parent,$ignore_limit) = @_;
  8254. my $self = bless {}, $class;
  8255. if (!defined($dir_name) && !defined($parent)) {
  8256. # just make an empty object, presumably used as a new root
  8257. } else {
  8258. $self->number($file_generator_object->generate_new_num($ignore_limit));
  8259. $self->dir_name($dir_name) if defined $dir_name;
  8260. if (defined $parent) {
  8261. $self->parent($parent);
  8262. my $ch_ref = $parent->children;
  8263. push(@$ch_ref,$self); $parent->children($ch_ref);
  8264. }
  8265. $file_generator_object->parts_list_add($self); # save it
  8266. ll(4) && do_log(4, "Issued a new %s: %s",
  8267. defined $dir_name ? "file name" : "pseudo part", $self->base_name);
  8268. }
  8269. $self;
  8270. }
  8271. sub number
  8272. { my $self=shift; !@_ ? $self->{number} : ($self->{number}=shift) };
  8273. sub dir_name
  8274. { my $self=shift; !@_ ? $self->{dir_name} : ($self->{dir_name}=shift) };
  8275. sub parent
  8276. { my $self=shift; !@_ ? $self->{parent} : ($self->{parent}=shift) };
  8277. sub children
  8278. { my $self=shift; !@_ ? $self->{children}||[] : ($self->{children}=shift) };
  8279. sub mime_placement # part location within a MIME tree, e.g. "1/1/3"
  8280. { my $self=shift; !@_ ? $self->{place} : ($self->{place}=shift) };
  8281. sub type_short # string or a ref to a list of strings, case sensitive
  8282. { my $self=shift; !@_ ? $self->{ty_short} : ($self->{ty_short}=shift) };
  8283. sub type_long
  8284. { my $self=shift; !@_ ? $self->{ty_long} : ($self->{ty_long}=shift) };
  8285. sub type_declared
  8286. { my $self=shift; !@_ ? $self->{ty_decl} : ($self->{ty_decl}=shift) };
  8287. sub name_declared # string or a ref to a list of strings
  8288. { my $self=shift; !@_ ? $self->{nm_decl} : ($self->{nm_decl}=shift) };
  8289. sub report_type # a string, e.g. 'delivery-status', RFC 3462
  8290. { my $self=shift; !@_ ? $self->{rep_typ} : ($self->{rep_typ}=shift) };
  8291. sub size
  8292. { my $self=shift; !@_ ? $self->{size} : ($self->{size}=shift) };
  8293. sub exists
  8294. { my $self=shift; !@_ ? $self->{exists} : ($self->{exists}=shift) };
  8295. sub attributes # listref of characters representing attributes
  8296. { my $self=shift; !@_ ? $self->{attr} : ($self->{attr}=shift) };
  8297. sub attributes_add { # U=undecodable, C=crypted, D=directory,S=special,L=link
  8298. my $self = shift; my $a = $self->{attr} || [];
  8299. for my $arg (@_) { push(@$a,$arg) if $arg ne '' && !grep($_ eq $arg, @$a) }
  8300. $self->{attr} = $a;
  8301. };
  8302. sub base_name { my $self = shift; sprintf("p%03d",$self->number) }
  8303. sub full_name {
  8304. my $self = shift; my $d = $self->dir_name;
  8305. !defined($d) ? undef : $d.'/'.$self->base_name;
  8306. }
  8307. # returns a ref to a list of part ancestors, starting with the root object,
  8308. # and including the part object itself
  8309. #
  8310. sub path {
  8311. my $self = shift;
  8312. my(@path);
  8313. for (my $p=$self; defined($p); $p=$p->parent) { unshift(@path,$p) }
  8314. \@path;
  8315. };
  8316. 1;
  8317. #
  8318. package Amavis::Unpackers::OurFiler;
  8319. use strict;
  8320. use re 'taint';
  8321. BEGIN {
  8322. require Exporter;
  8323. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8324. $VERSION = '2.316';
  8325. @ISA = qw(Exporter MIME::Parser::Filer); # subclass of MIME::Parser::Filer
  8326. }
  8327. # This package will be used by mime_decode().
  8328. #
  8329. # We don't want no heavy MIME::Parser machinery for file name extension
  8330. # guessing, decoding charsets in filenames (and listening to complaints
  8331. # about it), checking for evil filenames, checking for filename contention, ...
  8332. # (which cannot be turned off completely by ignore_filename(1) !!!)
  8333. # Just enforce our file name! And while at it, collect generated filenames.
  8334. #
  8335. sub new($$$) {
  8336. my($class, $dir, $parent_obj) = @_;
  8337. $dir =~ s{/+\z}{}; # chop off trailing slashes from directory name
  8338. bless {parent => $parent_obj, directory => $dir}, $class;
  8339. }
  8340. # provide a generated file name
  8341. #
  8342. sub output_path($@) {
  8343. my($self, $head) = @_;
  8344. my $newpart_obj =
  8345. Amavis::Unpackers::Part->new($self->{directory}, $self->{parent}, 1);
  8346. get_amavisd_part($head, $newpart_obj); # store object into head
  8347. $newpart_obj->full_name;
  8348. }
  8349. sub get_amavisd_part($;$) {
  8350. my $head = shift;
  8351. !@_ ? $head->{amavisd_parts_obj} : ($head->{amavisd_parts_obj} = shift);
  8352. }
  8353. 1;
  8354. #
  8355. package Amavis::Unpackers::Validity;
  8356. use strict;
  8357. use re 'taint';
  8358. BEGIN {
  8359. require Exporter;
  8360. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8361. $VERSION = '2.316';
  8362. @ISA = qw(Exporter);
  8363. @EXPORT_OK = qw(&check_header_validity &check_for_banned_names);
  8364. import Amavis::Util qw(ll do_log min max minmax untaint untaint_inplace
  8365. sanitize_str);
  8366. import Amavis::Conf qw(:platform %banned_rules c cr ca);
  8367. import Amavis::Lookup qw(lookup lookup2);
  8368. }
  8369. use subs @EXPORT_OK;
  8370. sub check_header_validity($) {
  8371. my($msginfo) = @_;
  8372. my(%field_head_counts, @bad);
  8373. my $minor_badh_category = 0;
  8374. my $allowed_tests = cr('allowed_header_tests');
  8375. my($t_syntax,$t_empty,$t_long,$t_control,$t_8bit,$t_missing,$t_multiple) =
  8376. !$allowed_tests ? () : @$allowed_tests{'syntax','empty','long','control',
  8377. '8bit','missing','multiple'};
  8378. # minor category: 2: 8-bit char, 3: NUL/CR control, 4: empty line, 5: long,
  8379. # 6: syntax, 7: missing, 8: multiple
  8380. local($1,$2,$3);
  8381. for my $curr_head (@{$msginfo->orig_header}) {#array of hdr fields, not lines
  8382. my($field_name,$msg1,$msg2,$pre,$mid,$post);
  8383. # obsolete RFC 822 syntax allowed whitespace before colon
  8384. $field_name = $1 if $curr_head =~ /^([!-9;-\176]+)[ \t]*:/s;
  8385. $field_head_counts{lc($field_name)}++ if defined $field_name;
  8386. if (!defined($field_name) || substr($field_name,0,2) eq '--') {
  8387. if ($t_syntax) {
  8388. $msg1 = "Invalid header field syntax";
  8389. $pre = ''; $mid = ''; $post = $curr_head;
  8390. $minor_badh_category = max(6, $minor_badh_category);
  8391. }
  8392. } elsif ($t_empty && $curr_head =~ /^([ \t]+)(?=\n|\z)/gms) {
  8393. $mid = $1;
  8394. $msg1 ="Improper folded header field made up entirely of whitespace";
  8395. # note: using //g and pos to avoid deep recursion in regexp
  8396. $minor_badh_category = max(4, $minor_badh_category);
  8397. } elsif ($t_long && $curr_head =~ /^([^\n]{999,})(?=\n|\z)/gms) {
  8398. $mid = $1; $msg1 = "Header line longer than 998 characters";
  8399. $minor_badh_category = max(5, $minor_badh_category);
  8400. } elsif ($t_control && $curr_head =~ /([\000\015])/gs) {
  8401. $mid = $1; $msg1 = "Improper use of control character";
  8402. $minor_badh_category = max(3, $minor_badh_category);
  8403. } elsif ($t_8bit && $curr_head =~ /([\200-\377])/gs) {
  8404. $mid = $1; $msg1 = "Non-encoded 8-bit data";
  8405. $minor_badh_category = max(2, $minor_badh_category);
  8406. } elsif ($t_8bit && $curr_head =~ /([^\000-\377])/gs) {
  8407. $mid = $1; $msg1 = "Non-encoded Unicode character"; # should not happen
  8408. $minor_badh_category = max(2, $minor_badh_category);
  8409. }
  8410. if (defined $msg1) {
  8411. $pre = substr($curr_head,0,pos($curr_head)-length($mid)) if !defined $pre;
  8412. $post = substr($curr_head,pos($curr_head)) if !defined $post;
  8413. chomp($post);
  8414. if (length($mid) > 20) { $mid = substr($mid, 0,15) . '[...]' }
  8415. if (length($post) > 20) { $post = substr($post,0,15) . '[...]' }
  8416. if (length($pre)-length($field_name)-2 > 50-length($post)) {
  8417. $pre = $field_name . ': ...'
  8418. . substr($pre, length($pre) - (45-length($post)));
  8419. }
  8420. $msg1 .= sprintf(" (char %02X hex)", ord($mid)) if length($mid)==1;
  8421. $msg2 = sanitize_str($pre . $mid . $post);
  8422. push(@bad, "$msg1: $msg2");
  8423. last if @bad >= 100; # some sanity limit
  8424. }
  8425. }
  8426. # RFC 5322 (ex RFC 2822), RFC 2045, RFC 2183
  8427. for (qw(Date From Sender Reply-To To Cc Bcc Subject Message-ID References
  8428. In-Reply-To MIME-Version Content-Type Content-Transfer-Encoding
  8429. Content-ID Content-Description Content-Disposition Auto-Submitted)) {
  8430. my $n = $field_head_counts{lc($_)};
  8431. if (!$n && $t_missing && /^(?:Date|From)\z/i) {
  8432. push(@bad, "Missing required header field: \"$_\"");
  8433. $minor_badh_category = max(7, $minor_badh_category);
  8434. } elsif ($n > 1 && $t_multiple) {
  8435. if ($n == 2) {
  8436. push(@bad, "Duplicate header field: \"$_\"");
  8437. } else {
  8438. push(@bad, sprintf('Header field occurs more than once: "%s" '.
  8439. 'occurs %d times', $_, $n));
  8440. }
  8441. $minor_badh_category = max(8, $minor_badh_category);
  8442. }
  8443. }
  8444. if (!@bad)
  8445. { do_log(5,"check_header: %d, OK", $minor_badh_category) }
  8446. elsif (ll(2))
  8447. { do_log(2,"check_header: %d, %s", $minor_badh_category, $_) for @bad }
  8448. (\@bad, $minor_badh_category);
  8449. }
  8450. sub check_for_banned_names($) {
  8451. my($msginfo) = @_;
  8452. do_log(3, "Checking for banned types and filenames");
  8453. my $bfnmr = ca('banned_filename_maps'); # two-level map: recip, partname
  8454. my(@recip_tables); # a list of records describing banned tables for recips
  8455. my $any_table_in_recip_tables = 0; my $any_not_bypassed = 0;
  8456. for my $r (@{$msginfo->per_recip_data}) {
  8457. my $recip = $r->recip_addr;
  8458. my(@tables,@tables_m); # list of banned lookup tables for this recipient
  8459. if (!$r->bypass_banned_checks) { # not bypassed
  8460. $any_not_bypassed = 1;
  8461. my($t_ref,$m_ref) = lookup2(1,$recip,$bfnmr);
  8462. if (defined $t_ref) {
  8463. for my $ti (0..$#$t_ref) { # collect all relevant tables for each recip
  8464. my $t = $t_ref->[$ti];
  8465. # an entry may be a ref to a list of lookup tables, or a comma- or
  8466. # whitespace-separated list of table names (suitable for SQL),
  8467. # which are mapped to actual lookup tables through %banned_rules
  8468. if (!defined($t)) {
  8469. # ignore
  8470. } elsif (ref($t) eq 'ARRAY') { # a list of actual lookup tables
  8471. push(@tables, @$t);
  8472. push(@tables_m, ($m_ref->[$ti]) x @$t);
  8473. } else { # a list of rules _names_, to be mapped via %banned_rules
  8474. my(@names);
  8475. my(@rawnames) = grep(!/^[, ]*\z/,
  8476. ($t =~ /\G (?: " (?: \\. | [^"\\] ){0,999} "
  8477. | [^, ] )+ | [, ]+/gsx));
  8478. # in principle quoted strings could be used
  8479. # to construct lookup tables on-the-fly (not implemented)
  8480. for my $n (@rawnames) { # collect only valid names
  8481. if (!exists($banned_rules{$n})) {
  8482. do_log(2,"INFO: unknown banned table name %s, recip=%s",
  8483. $n,$recip);
  8484. } elsif (!defined($banned_rules{$n})) { # ignore undef
  8485. } else { push(@names,$n) }
  8486. }
  8487. ll(3) && do_log(3,"collect banned table[%d]: %s, tables: %s",
  8488. $ti,$recip, join(', ',map($_.'=>'.$banned_rules{$_}, @names)));
  8489. if (@names) { # any known and valid table names?
  8490. push(@tables, map($banned_rules{$_}, @names));
  8491. push(@tables_m, ($m_ref->[$ti]) x @names);
  8492. }
  8493. }
  8494. }
  8495. }
  8496. }
  8497. push(@recip_tables, { r => $r, recip => $recip,
  8498. tables => \@tables, tables_m => \@tables_m } );
  8499. $any_table_in_recip_tables=1 if @tables;
  8500. }
  8501. my $bnpre = cr('banned_namepath_re');
  8502. $bnpre = $$bnpre if ref($bnpre) eq 'REF'; # allow one level of indirection
  8503. if (!$any_not_bypassed) {
  8504. do_log(3,"skipping banned check: all recipients bypass banned checks");
  8505. } elsif (!$any_table_in_recip_tables && !ref($bnpre)) {
  8506. do_log(3,"skipping banned check: no applicable lookup tables");
  8507. } else {
  8508. do_log(4,"starting banned checks - traversing message structure tree");
  8509. my $parts_root = $msginfo->parts_root;
  8510. my $part;
  8511. for (my(@unvisited)=($parts_root);
  8512. @unvisited and $part=shift(@unvisited);
  8513. push(@unvisited,@{$part->children}))
  8514. { # traverse decomposed parts tree breadth-first
  8515. my(@path) = @{$part->path};
  8516. next if @path <= 1;
  8517. shift(@path); # ignore place-holder root node
  8518. next if @{$part->children}; # ignore non-leaf nodes
  8519. my(@descr_trad); # a part path: list of predecessors of a message part
  8520. my(@descr); # same, but in form suitable for check on banned_namepath_re
  8521. for my $p (@path) {
  8522. my(@k,$n);
  8523. $n = $p->base_name;
  8524. if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"P=$n") }
  8525. $n = $p->mime_placement;
  8526. if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"L=$n") }
  8527. $n = $p->type_declared;
  8528. $n = [$n] if !ref($n);
  8529. for (@$n) {if ($_ ne ''){my $m=$_; $m=~s/[\t\n]/ /g; push(@k,"M=$m")}}
  8530. $n = $p->type_short;
  8531. $n = [$n] if !ref($n);
  8532. for (@$n) {if (defined($_) && $_ ne '')
  8533. {my $m=$_; $m=~s/[\t\n]/ /g; push(@k,"T=$m")} }
  8534. $n = $p->name_declared;
  8535. $n = [$n] if !ref($n);
  8536. for (@$n) {if (defined($_) && $_ ne '')
  8537. {my $m=$_; $m=~s/[\t\n]/ /g; push(@k,"N=$m")} }
  8538. $n = $p->attributes;
  8539. $n = [$n] if !ref($n);
  8540. for (@$n) {if (defined($_) && $_ ne '')
  8541. {my $m=$_; $m=~s/[\t\n]/ /g; push(@k,"A=$m")} }
  8542. push(@descr, join("\t",@k));
  8543. push(@descr_trad, [map { local($1,$2);
  8544. /^([a-zA-Z0-9])=(.*)\z/s; my($key_what,$key_val) = ($1,$2);
  8545. $key_what eq 'M' || $key_what eq 'N' ? $key_val
  8546. : $key_what eq 'T' ? ('.'.$key_val) # prepend a dot (compatibility)
  8547. : $key_what eq 'A' && $key_val eq 'U' ? 'UNDECIPHERABLE' : ()} @k]);
  8548. }
  8549. # we have obtained a description of a part as a list of its predecessors
  8550. # in a message structure including the part itself at the end of the list
  8551. my $key_val_str = join(' | ',@descr); $key_val_str =~ s/\t/,/g;
  8552. my $key_val_trad_str = join(' | ', map(join(',',@$_), @descr_trad));
  8553. # simplified result to be presented in an SMTP response and DSN
  8554. my $simple_part_name = join(',', @{$descr_trad[-1]}); # just leaf node
  8555. # evaluate current mail component path against each recipients' tables
  8556. ll(4) && do_log(4, "check_for_banned (%s) %s",
  8557. join(',', map($_->base_name, @path)), $key_val_trad_str);
  8558. for my $e (@recip_tables) {
  8559. @$e{qw(found result matchk part_descr_attr part_descr_trad part_name)}
  8560. = (0, undef, undef, undef, undef, undef);
  8561. }
  8562. my($result, $matchingkey, $t_ref_old);
  8563. for my $e (@recip_tables) { # for each recipient and his tables
  8564. my($found,$recip,$t_ref) = @$e{qw(found recip tables)};
  8565. if ($t_ref && @$t_ref) {
  8566. my $same_as_prev = $t_ref_old && @$t_ref_old==@$t_ref &&
  8567. !grep($t_ref_old->[$_] ne $t_ref->[$_], (0..$#$t_ref)) ? 1 : 0;
  8568. if ($same_as_prev) {
  8569. do_log(4,
  8570. "skip banned check for %s, same tables as previous, result => %s",
  8571. $recip,$result);
  8572. } else {
  8573. do_log(5,"doing banned check for %s on %s",
  8574. $recip,$key_val_trad_str);
  8575. ($result,$matchingkey) =
  8576. lookup2(0, [map(@$_,@descr_trad)], # check all attribs in one go
  8577. [map(ref($_) eq 'ARRAY' ? @$_ : $_, @$t_ref)],
  8578. Label=>"check_bann:$recip");
  8579. $t_ref_old = $t_ref;
  8580. }
  8581. if (defined $result) {
  8582. @$e{qw(found result matchk
  8583. part_descr_attr part_descr_trad part_name)} =
  8584. (1, $result, $matchingkey,
  8585. $key_val_str, $key_val_trad_str, $simple_part_name);
  8586. }
  8587. }
  8588. }
  8589. if (ref $bnpre && grep(!$_->{result}, @recip_tables)) { # any non-true?
  8590. # try new style: banned_namepath_re; it is global, not per-recipient
  8591. my $descr_str = join("\n",@descr);
  8592. if ($] < 5.012003) {
  8593. # avoid a [perl #62048] bug in lookup_re():
  8594. # Unwarranted "Malformed UTF-8 character" on tainted variable
  8595. untaint_inplace($descr_str);
  8596. }
  8597. my($result,$matchingkey) = lookup2(0, $descr_str, [$bnpre],
  8598. Label=>'banned_namepath_re');
  8599. if (defined $result) {
  8600. for my $e (@recip_tables) {
  8601. if (!$e->{found}) {
  8602. @$e{qw(found result matchk
  8603. part_descr_attr part_descr_trad part_name)} =
  8604. (1, $result, $matchingkey,
  8605. $key_val_str, $key_val_trad_str, $simple_part_name);
  8606. }
  8607. }
  8608. }
  8609. }
  8610. my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
  8611. e => "\e", a => "\a", t => "\t"); # for pretty-printing
  8612. my $ll = grep($_->{result}, @recip_tables) ? 1 : 3; # log level
  8613. for my $e (@recip_tables) { # log and store results
  8614. my($r, $recip, $result, $matchingkey,
  8615. $part_descr_attr, $part_descr_trad, $part_name) =
  8616. @$e{qw(r recip result matchk
  8617. part_descr_attr part_descr_trad part_name)};
  8618. if (ll($ll)) { # only bother with logging when needed
  8619. local($1);
  8620. my $mk = defined $matchingkey ? $matchingkey : ''; # pretty-print
  8621. $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx;
  8622. do_log($result?1:3, 'p.path%s %s: "%s"%s',
  8623. !$result?'':" BANNED:$result", $recip, $key_val_str,
  8624. !defined $result ? '' : ", matching_key=\"$mk\"");
  8625. }
  8626. my $a;
  8627. if ($result) { # the part being tested is banned for this recipient
  8628. $a = $r->banned_parts || [];
  8629. push(@$a,$part_descr_trad); $r->banned_parts($a);
  8630. $a = $r->banned_parts_as_attr || [];
  8631. push(@$a,$part_descr_attr); $r->banned_parts_as_attr($a);
  8632. $a = $r->banning_rule_rhs || [];
  8633. push(@$a,$result); $r->banning_rule_rhs($a);
  8634. $a = $r->banning_rule_key || [];
  8635. $matchingkey = "$matchingkey"; # make a plain string out of a qr
  8636. push(@$a,$matchingkey); $r->banning_rule_key($a);
  8637. my(@comments) = $matchingkey =~ / \( \? \# \s* (.*?) \s* \) /gsx;
  8638. $a = $r->banning_rule_comment || [];
  8639. push(@$a, @comments ? join(' ',@comments) : $matchingkey);
  8640. $r->banning_rule_comment($a);
  8641. if (!defined($r->banning_reason_short)) { # just the first
  8642. my $s = $part_name;
  8643. $s =~ s/[ \t]{6,}/ ... /g; # compact whitespace
  8644. $s = join(' ',@comments) . ':' . $s if @comments;
  8645. $r->banning_reason_short($s);
  8646. }
  8647. }
  8648. }
  8649. # last if !grep(!$_->{result}, @recip_tables); # stop if all recips true
  8650. } # endfor: message tree traversal
  8651. } # endif: doing parts checking
  8652. }
  8653. 1;
  8654. #
  8655. package Amavis::Unpackers::MIME;
  8656. use strict;
  8657. use re 'taint';
  8658. BEGIN {
  8659. require Exporter;
  8660. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8661. $VERSION = '2.316';
  8662. @ISA = qw(Exporter);
  8663. @EXPORT_OK = qw(&mime_decode);
  8664. import Amavis::Conf qw(:platform c cr ca $TEMPBASE $MAXFILES);
  8665. import Amavis::Timing qw(section_time);
  8666. import Amavis::Util qw(snmp_count untaint ll do_log safe_decode
  8667. safe_encode safe_encode_ascii safe_encode_utf8);
  8668. import Amavis::Unpackers::NewFilename qw(consumed_bytes);
  8669. }
  8670. use subs @EXPORT_OK;
  8671. use Errno qw(ENOENT EACCES);
  8672. use IO::File qw(O_CREAT O_EXCL O_WRONLY);
  8673. use MIME::Parser;
  8674. use MIME::Words;
  8675. # use Scalar::Util qw(tainted);
  8676. # save MIME preamble and epilogue (if nontrivial) as extra (pseudo)parts
  8677. #
  8678. sub mime_decode_pre_epi($$$$$) {
  8679. my($pe_name, $pe_lines, $tempdir, $parent_obj, $placement) = @_;
  8680. if (defined $pe_lines && @$pe_lines) {
  8681. do_log(5, "mime_decode_%s: %d lines", $pe_name, scalar(@$pe_lines));
  8682. if (@$pe_lines > 5 || "@$pe_lines" !~ m{^[A-Za-z0-9/\@:;,. \t\n_-]*\z}s) {
  8683. my $newpart_obj =
  8684. Amavis::Unpackers::Part->new("$tempdir/parts",$parent_obj,1);
  8685. $newpart_obj->mime_placement($placement);
  8686. $newpart_obj->name_declared($pe_name);
  8687. my $newpart = $newpart_obj->full_name;
  8688. my $outpart = IO::File->new;
  8689. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  8690. $outpart->open($newpart, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640)
  8691. or die "Can't create $pe_name file $newpart: $!";
  8692. binmode($outpart,':bytes') or die "Can't cancel :utf8 mode: $!";
  8693. my $len;
  8694. for (@$pe_lines) {
  8695. $outpart->print($_) or die "Can't write $pe_name to $newpart: $!";
  8696. $len += length($_);
  8697. }
  8698. $outpart->close or die "Error closing $pe_name $newpart: $!";
  8699. $newpart_obj->size($len);
  8700. consumed_bytes($len, "mime_decode_$pe_name", 0, 1);
  8701. }
  8702. }
  8703. }
  8704. # traverse MIME::Entity object depth-first,
  8705. # extracting preambles and epilogues as extra (pseudo)parts, and
  8706. # filling-in additional information into Amavis::Unpackers::Part objects
  8707. #
  8708. sub mime_traverse($$$$$); # prototype
  8709. sub mime_traverse($$$$$) {
  8710. my($entity, $tempdir, $parent_obj, $depth, $placement) = @_;
  8711. mime_decode_pre_epi('preamble', $entity->preamble,
  8712. $tempdir, $parent_obj, $placement);
  8713. my($mt, $et) = ($entity->mime_type, $entity->effective_type);
  8714. my $part; my $head = $entity->head; my $body = $entity->bodyhandle;
  8715. if (!defined($body)) { # a MIME container only contains parts, no bodypart
  8716. # create pseudo-part objects for MIME containers (e.g. multipart/* )
  8717. $part = Amavis::Unpackers::Part->new(undef,$parent_obj,1);
  8718. # $part->type_short('no-file');
  8719. do_log(2, "%s %s Content-Type: %s", $part->base_name, $placement, $mt);
  8720. } else { # does have a body part (i.e. not a MIME container)
  8721. my $fn = $body->path; my $size;
  8722. if (!defined($fn)) {
  8723. $size = length($body->as_string);
  8724. } else {
  8725. my $msg; my $errn = lstat($fn) ? 0 : 0+$!;
  8726. if ($errn == ENOENT) { $msg = "does not exist" }
  8727. elsif ($errn) { $msg = "is inaccessible: $!" }
  8728. elsif (!-r _) { $msg = "is not readable" }
  8729. elsif (!-f _) { $msg = "is not a regular file" }
  8730. else {
  8731. $size = -s _;
  8732. do_log(4,"mime_traverse: file %s is empty", $fn) if $size==0;
  8733. }
  8734. do_log(-1,"WARN: mime_traverse: file %s %s", $fn,$msg) if defined $msg;
  8735. }
  8736. consumed_bytes($size, 'mime_decode', 0, 1);
  8737. # retrieve Amavis::Unpackers::Part object (if any), stashed into head obj
  8738. $part = Amavis::Unpackers::OurFiler::get_amavisd_part($head);
  8739. if (defined $part) {
  8740. $part->size($size);
  8741. if (defined($size) && $size==0)
  8742. { $part->type_short('empty'); $part->type_long('empty') }
  8743. ll(2) && do_log(2, "%s %s Content-Type: %s, size: %d B, name: %s",
  8744. $part->base_name, $placement, $mt, $size,
  8745. $entity->head->recommended_filename);
  8746. my $old_parent_obj = $part->parent;
  8747. if ($parent_obj ne $old_parent_obj) { # reparent if necessary
  8748. ll(5) && do_log(5,"reparenting %s from %s to %s", $part->base_name,
  8749. $old_parent_obj->base_name, $parent_obj->base_name);
  8750. my $ch_ref = $old_parent_obj->children;
  8751. $old_parent_obj->children([grep($_ ne $part, @$ch_ref)]);
  8752. $ch_ref = $parent_obj->children;
  8753. push(@$ch_ref,$part); $parent_obj->children($ch_ref);
  8754. $part->parent($parent_obj);
  8755. }
  8756. }
  8757. }
  8758. if (defined $part) {
  8759. $part->mime_placement($placement);
  8760. $part->type_declared($mt eq $et ? $mt : [$mt, $et]);
  8761. $part->attributes_add('U','C') if $mt =~ m{/encrypted}i ||
  8762. $et =~ m{/encrypted}i;
  8763. my %rn_seen;
  8764. my @rn; # recommended file names, both raw and RFC 2047 / RFC 2231 decoded
  8765. for my $attr_name ('content-disposition.filename', 'content-type.name') {
  8766. my $val_raw = $head->mime_attr($attr_name);
  8767. next if !defined $val_raw || $val_raw eq '';
  8768. my $val_dec = ''; # decoded, represented as native Perl characters
  8769. eval {
  8770. my(@chunks) = MIME::Words::decode_mimewords($val_raw);
  8771. for my $pair (@chunks) {
  8772. my($data,$encoding) = @$pair;
  8773. $encoding = 'ISO-8859-1' if !defined $encoding || $encoding eq '';
  8774. $encoding =~ s/\*[^*]*\z//; # strip RFC 2231 language suffix
  8775. $val_dec .= safe_decode($encoding,$data);
  8776. }
  8777. 1;
  8778. } or do {
  8779. do_log(3, "mime_traverse: decoding MIME words failed: %s", $@);
  8780. };
  8781. if ($val_dec ne '' && !$rn_seen{$val_dec}) {
  8782. push(@rn,$val_dec); $rn_seen{$val_dec} = 1;
  8783. }
  8784. if (!$rn_seen{$val_raw}) {
  8785. push(@rn,$val_raw); $rn_seen{$val_raw} = 1;
  8786. }
  8787. }
  8788. $part->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
  8789. my $val = $head->mime_attr('content-type.report-type');
  8790. if (defined $val && $val ne '') {
  8791. # $val = safe_encode_utf8($val);
  8792. $part->report_type($val);
  8793. }
  8794. }
  8795. mime_decode_pre_epi('epilogue', $entity->epilogue,
  8796. $tempdir, $parent_obj, $placement);
  8797. my $item_num = 0;
  8798. for my $e ($entity->parts) { # recursive descent
  8799. $item_num++;
  8800. mime_traverse($e,$tempdir,$part,$depth+1,"$placement/$item_num");
  8801. }
  8802. }
  8803. # Break up mime parts, return a MIME::Entity object
  8804. #
  8805. sub mime_decode($$$) {
  8806. my($msg, $tempdir, $parent_obj) = @_;
  8807. # $msg may be an open file handle, or a file name, or a string ref
  8808. my $parser = MIME::Parser->new;
  8809. # File::Temp->new defaults to /tmp or a current directory, ignoring TMPDIR
  8810. $parser->tmp_dir($TEMPBASE) if $parser->UNIVERSAL::can('tmp_dir');
  8811. $parser->filer(
  8812. Amavis::Unpackers::OurFiler->new("$tempdir/parts", $parent_obj) );
  8813. $parser->ignore_errors(1); # also is the default
  8814. # if bounce killer is enabled, extract_nested_messages must be off,
  8815. # otherwise we lose headers of attached message/rfc822 messages
  8816. $parser->extract_nested_messages(0);
  8817. # $parser->extract_nested_messages("NEST"); # parse embedded message/rfc822
  8818. # "NEST" complains with "part did not end with expected boundary" when
  8819. # the outer message is message/partial and the inner message is chopped
  8820. $parser->extract_uuencode(1); # to enable or not to enable ???
  8821. $parser->max_parts($MAXFILES) if defined $MAXFILES && $MAXFILES > 0 &&
  8822. $parser->UNIVERSAL::can('max_parts');
  8823. snmp_count('OpsDecByMimeParser');
  8824. my $entity;
  8825. { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.* bug, $1 can get tainted !
  8826. if (!defined $msg) {
  8827. $entity = $parser->parse_data('');
  8828. } elsif (!ref $msg) { # assume $msg is a file name
  8829. do_log(4, "Extracting mime components from file %s", $msg);
  8830. $entity = $parser->parse_open("$tempdir/parts/$msg");
  8831. } elsif (ref $msg eq 'SCALAR') {
  8832. do_log(4, "Extracting mime components from a string");
  8833. # parse_data() should be avoided with IO::File 1.09 or older:
  8834. # it uses a mode '>:' to force a three-argument open(), but a mode
  8835. # with a colon is only recognized starting with IO::File 1.10,
  8836. # which comes with perl 5.8.1
  8837. IO::File->VERSION(1.10); # required minimal version
  8838. $entity = $parser->parse_data($msg); # takes a ref to a string
  8839. } elsif (ref $msg) { # assume an open file handle
  8840. do_log(4, "Extracting mime components from a file");
  8841. $msg->seek(0,0) or die "Can't rewind mail file: $!";
  8842. $entity = $parser->parse($msg);
  8843. }
  8844. }
  8845. my $mime_err;
  8846. my(@mime_errors) = $parser->results->errors; # a list!
  8847. if (@mime_errors) {
  8848. # $mime_err = $mime_errors[0]; # only show the first error
  8849. $mime_err = join('; ',@mime_errors); # show all errors
  8850. }
  8851. if (defined $mime_err) {
  8852. $mime_err=~s/\s+\z//; $mime_err=~s/[ \t\r]*\n+/; /g; $mime_err=~s/\s+/ /g;
  8853. $mime_err = substr($mime_err,0,250) . '[...]' if length($mime_err) > 250;
  8854. do_log(1, "WARN: MIME::Parser %s", $mime_err) if $mime_err ne '';
  8855. } elsif (!defined($entity)) {
  8856. $mime_err = "Unable to parse, perhaps message contains too many parts";
  8857. do_log(1, "WARN: MIME::Parser %s", $mime_err);
  8858. $entity = '';
  8859. }
  8860. mime_traverse($entity, $tempdir, $parent_obj, 0, '1') if $entity;
  8861. section_time('mime_decode');
  8862. ($entity, $mime_err);
  8863. }
  8864. 1;
  8865. #
  8866. package Amavis::MIME::Body::OnOpenFh;
  8867. # A body class that keeps data on an open file handle, read-only,
  8868. # while allowing to prepend a couple of lines when reading from it.
  8869. # $skip_bytes bytes at the beginning of a given open file are ignored.
  8870. use strict;
  8871. use re 'taint';
  8872. BEGIN {
  8873. require Exporter;
  8874. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8875. $VERSION = '2.316';
  8876. @ISA = qw(Exporter MIME::Body); # subclass of MIME::Body
  8877. import Amavis::Util qw(ll do_log);
  8878. }
  8879. sub init {
  8880. my($self, $fh,$prefix_lines,$skip_bytes) = @_;
  8881. $self->{MB_Am_fh} = $fh;
  8882. $self->{MB_Am_prefix} = defined $prefix_lines ? join('',@$prefix_lines) : '';
  8883. $self->{MB_Am_prefix_l} = length($self->{MB_Am_prefix});
  8884. $self->{MB_Am_skip_bytes} = !defined $skip_bytes ? 0 : $skip_bytes;
  8885. $self->is_encoded(1);
  8886. $self;
  8887. }
  8888. sub open {
  8889. my($self,$mode) = @_;
  8890. $self->close; # ignoring status
  8891. $mode eq 'r' or die "Only offers read-only access, mode: $mode";
  8892. my $fh = $self->{MB_Am_fh}; my $skip = $self->{MB_Am_skip_bytes};
  8893. $fh->seek($skip,0) or die "Can't rewind mail file: $!";
  8894. $self->{MB_Am_pos} = 0;
  8895. bless { parent => $self }; #** One-argument "bless" warning
  8896. }
  8897. sub close { 1 }
  8898. sub read { # SCALAR,LENGTH,OFFSET
  8899. my $self = shift; my $len = $_[1]; my $offset = $_[2];
  8900. my $parent = $self->{parent}; my $pos = $parent->{MB_Am_pos};
  8901. my $str1 = ''; my $str2 = ''; my $nbytes = 0;
  8902. if ($len > 0 && $pos < $parent->{MB_Am_prefix_l}) {
  8903. $str1 = substr($parent->{MB_Am_prefix}, $pos, $len);
  8904. $nbytes += length($str1); $len -= $nbytes;
  8905. }
  8906. my $msg;
  8907. if ($len > 0) {
  8908. my $nb = $parent->{MB_Am_fh}->read($str2,$len);
  8909. if (!defined $nb) {
  8910. $msg = "Error reading: $!";
  8911. } elsif ($nb < 1) {
  8912. # read returns 0 at eof
  8913. } else {
  8914. $nbytes += $nb; $len -= $nb;
  8915. }
  8916. }
  8917. if (defined $msg) {
  8918. undef $nbytes; # $! already set by a failed read
  8919. } else {
  8920. ($offset ? substr($_[0],$offset) : $_[0]) = $str1.$str2;
  8921. $pos += $nbytes; $parent->{MB_Am_pos} = $pos;
  8922. }
  8923. $nbytes; # eof: 0; error: undef
  8924. }
  8925. 1;
  8926. #
  8927. package Amavis::Notify;
  8928. use strict;
  8929. use re 'taint';
  8930. BEGIN {
  8931. require Exporter;
  8932. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  8933. $VERSION = '2.316';
  8934. @ISA = qw(Exporter);
  8935. @EXPORT_OK = qw(&delivery_status_notification &delivery_short_report
  8936. &build_mime_entity &defanged_mime_entity
  8937. &msg_from_quarantine &expand_variables);
  8938. import Amavis::Util qw(ll do_log sanitize_str min max minmax
  8939. safe_encode safe_encode_ascii safe_encode_utf8
  8940. untaint untaint_inplace make_password
  8941. orcpt_decode xtext_decode ccat_split ccat_maj);
  8942. import Amavis::Timing qw(section_time);
  8943. import Amavis::Conf qw(:platform :confvars c cr ca);
  8944. import Amavis::ProcControl qw(exit_status_str proc_status_ok
  8945. run_command collect_results);
  8946. import Amavis::Out::EditHeader qw(hdr);
  8947. import Amavis::Lookup qw(lookup lookup2);
  8948. import Amavis::Expand qw(expand);
  8949. import Amavis::rfc2821_2822_Tools;
  8950. }
  8951. use subs @EXPORT_OK;
  8952. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  8953. use MIME::Entity;
  8954. use Time::HiRes ();
  8955. # use Encode; # Perl 5.8 UTF-8 support
  8956. # replace substring ${myhostname} with a value of a corresponding variable
  8957. sub expand_variables($) {
  8958. my($str) = @_; local($1,$2);
  8959. $str =~ s{ \$ (?: \{ ([^\}]+) \} |
  8960. ([a-zA-Z](?:[a-zA-Z0-9_]*[a-zA-Z0-9])?\b) ) }
  8961. { { 'myhostname' => c('myhostname') }->{lc($1.$2)} }egx;
  8962. $str;
  8963. }
  8964. # wrap a mail message into a ZIP archive
  8965. #
  8966. sub wrap_message_into_archive($$) {
  8967. my($msginfo,$prefix_lines_ref) = @_;
  8968. # a file with a copy of a mail msg as retrieved from a quarantine:
  8969. my $attachment_email_name = c('attachment_email_name'); # 'msg-%m.eml'
  8970. # an archive file (will contain a retrieved message) to be attached:
  8971. my $attachment_outer_name = c('attachment_outer_name'); # 'msg-%m.zip'
  8972. my($email_fh, $arch_size);
  8973. my $mail_id = $msginfo->mail_id;
  8974. if (!defined $mail_id || $mail_id eq '') {
  8975. $mail_id = '';
  8976. } else {
  8977. $mail_id =~ /^[A-Za-z0-9_-]*\z/ or die "unsafe mail_id: $mail_id";
  8978. untaint_inplace($mail_id);
  8979. }
  8980. for ($attachment_email_name, $attachment_outer_name) {
  8981. local $1;
  8982. s{%(.)}{ $1 eq 'b' ? $msginfo->body_digest
  8983. : $1 eq 'P' ? $msginfo->partition_tag
  8984. : $1 eq 'm' ? $mail_id
  8985. : $1 eq 'n' ? $msginfo->log_id
  8986. : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1) #,'-')
  8987. : $1 eq '%' ? '%' : '%'.$1 }egs;
  8988. $_ = $msginfo->mail_tempdir . '/' . $_;
  8989. }
  8990. my $eval_stat;
  8991. eval {
  8992. # copy a retrieved message to a file
  8993. $email_fh = IO::File->new;
  8994. $email_fh->open($attachment_email_name, O_CREAT|O_EXCL|O_RDWR, 0640)
  8995. or die "Can't create file $attachment_email_name: $!";
  8996. binmode($email_fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  8997. for (@$prefix_lines_ref) {
  8998. $email_fh->print($_)
  8999. or die "Error writing to $attachment_email_name: $!";
  9000. }
  9001. my $msg = $msginfo->mail_text;
  9002. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  9003. $msg = $msg_str_ref if ref $msg_str_ref;
  9004. # copy quarantined mail starting at skip_bytes to $attachment_email_name
  9005. my $file_position = $msginfo->skip_bytes;
  9006. if (!defined $msg) {
  9007. # empty mail
  9008. } elsif (ref $msg eq 'SCALAR') {
  9009. # do it in chunks, saves memory, cache friendly
  9010. while ($file_position < length($$msg)) {
  9011. $email_fh->print(substr($$msg,$file_position,16384))
  9012. or die "Error writing to $attachment_email_name: $!";
  9013. $file_position += 16384; # may overshoot, no problem
  9014. }
  9015. } elsif ($msg->isa('MIME::Entity')) {
  9016. die "wrapping a MIME::Entity object is not implemented";
  9017. } else {
  9018. $msg->seek($file_position,0) or die "Can't rewind mail file: $!";
  9019. my($nbytes,$buff);
  9020. while (($nbytes = $msg->read($buff,16384)) > 0) {
  9021. $email_fh->print($buff)
  9022. or die "Error writing to $attachment_email_name: $!";
  9023. }
  9024. defined $nbytes or die "Error reading mail file: $!";
  9025. undef $buff; # release storage
  9026. }
  9027. $email_fh->close or die "Can't close file $attachment_email_name: $!";
  9028. undef $email_fh;
  9029. # create a password-protected archive containing the just prepared file;
  9030. # no need to shell-protect arguments, as this does not invoke a shell
  9031. my $password = $msginfo->attachment_password;
  9032. my(@command) = ( qw(zip -q -j -l),
  9033. $password eq '' ? () : ('-P', $password),
  9034. $attachment_outer_name, $attachment_email_name );
  9035. # supplying a password on a command line is lame as it shows in ps(1),
  9036. # but an option -e would require a pseudo terminal, which is really
  9037. # an overweight cannon unnecessary here: the password is used as a
  9038. # scrambler only, protecting against accidental opening of a file,
  9039. # so there is no security issue here
  9040. $password = 'X' x length($password); # can't hurt to hide it
  9041. my($proc_fh,$pid) = run_command(undef,undef,@command);
  9042. my($r,$status) = collect_results($proc_fh,$pid,'zip',16384,[0]);
  9043. undef $proc_fh; undef $pid;
  9044. do_log(2,'archiver said: %s',$$r) if ref $r && $$r ne '';
  9045. $status == 0 or die "Error creating an archive: $status, $$r";
  9046. my $errn = lstat($attachment_outer_name) ? 0 : 0+$!;
  9047. if ($errn) { die "Archive $attachment_outer_name is inaccessible: $!" }
  9048. else { $arch_size = 0 + (-s _) }
  9049. 1;
  9050. } or do {
  9051. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9052. };
  9053. if ($eval_stat ne '' || !$arch_size) { # handle failure
  9054. my $msg = $eval_stat ne '' ? $eval_stat
  9055. : sprintf("archive size %d", $arch_size);
  9056. do_log(-1,'Preparing an archive from a quarantined message failed: %s',
  9057. $msg);
  9058. if (defined $email_fh && $email_fh->fileno) {
  9059. $email_fh->close
  9060. or do_log(-1,"Can't close %s: %s", $attachment_email_name, $!);
  9061. }
  9062. undef $email_fh;
  9063. if (-e $attachment_email_name) {
  9064. unlink($attachment_email_name)
  9065. or do_log(-1,"Can't remove %s: %s", $attachment_email_name, $!);
  9066. }
  9067. if (-e $attachment_outer_name) {
  9068. unlink($attachment_outer_name)
  9069. or do_log(-1,"Can't remove %s: %s", $attachment_outer_name, $!);
  9070. }
  9071. die "Preparing an archive from a quarantined message failed: $msg\n";
  9072. }
  9073. $attachment_outer_name;
  9074. }
  9075. # Create a MIME::Entity object. If $mail_as_string_ref points to a string
  9076. # (multiline mail header with a plain text body) it is added as the first
  9077. # MIME part. Optionally attach a message header section from original mail,
  9078. # or attach a complete original message.
  9079. #
  9080. sub build_mime_entity($$$$$$$) {
  9081. my($mail_as_string_ref, $msginfo, $mime_type, $msg_format, $flat,
  9082. $attach_orig_headers, $attach_orig_message) = @_;
  9083. $msg_format = '' if !defined $msg_format;
  9084. if (!defined $mime_type || $mime_type !~ m{^multipart(/|\z)}i) {
  9085. my $multipart_cnt = 0;
  9086. $multipart_cnt++ if $mail_as_string_ref;
  9087. $multipart_cnt++ if defined $msginfo &&
  9088. ($attach_orig_headers || $attach_orig_message);
  9089. $mime_type = 'multipart/mixed' if $multipart_cnt > 1;
  9090. }
  9091. my($entity,$m_hdr,$m_body);
  9092. if (!$mail_as_string_ref) {
  9093. # no plain text part
  9094. } elsif ($$mail_as_string_ref eq '') {
  9095. $m_hdr = $m_body = '';
  9096. } elsif (substr($$mail_as_string_ref, 0,1) eq "\n") { # empty header section?
  9097. $m_hdr = ''; $m_body = substr($$mail_as_string_ref,1);
  9098. } else {
  9099. # calling index and substr is much faster than an equiv. split into $1,$2
  9100. # by a regular expression: /^( (?!\n) .*? (?:\n|\z))? (?: \n (.*) )? \z/sx
  9101. my $ind = index($$mail_as_string_ref,"\n\n"); # find header/body separator
  9102. if ($ind < 0) { # no body
  9103. $m_hdr = $$mail_as_string_ref; $m_body = '';
  9104. } else { # normal mail, nonempty header section and nonempty body
  9105. $m_hdr = substr($$mail_as_string_ref, 0, $ind+1);
  9106. $m_body = substr($$mail_as_string_ref, $ind+2);
  9107. }
  9108. }
  9109. $m_body = safe_encode(c('bdy_encoding'), $m_body) if defined $m_body;
  9110. # make sure _our_ source line number is reported in case of failure
  9111. my $multipart_cnt = 0;
  9112. eval {
  9113. $entity = MIME::Entity->build(
  9114. Type => defined $mime_type ? $mime_type : 'multipart/mixed',
  9115. Encoding => '7bit', 'X-Mailer' => undef);
  9116. 1;
  9117. } or do {
  9118. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9119. die $eval_stat;
  9120. };
  9121. if (defined $m_hdr) { # insert header fields into MIME::Head entity
  9122. # Mail::Header::modify allows all-or-nothing control over automatic header
  9123. # fields folding by Mail::Header, which is too bad - we would prefer
  9124. # to have full control on folding of header fields that are explicitly
  9125. # inserted here, and let Mail::Header handle the rest. Sorry, can't be
  9126. # done, so let's just disable folding by Mail::Header (which does a poor
  9127. # job when presented with few break opportunities), and wrap our header
  9128. # fields ourselves, hoping the remaining automatically generated header
  9129. # fields won't be too long.
  9130. local($1,$2);
  9131. my $head = $entity->head; $head->modify(0);
  9132. $m_hdr =~ s/\r?\n(?=[ \t])//gs; # unfold header fields in a template
  9133. for my $hdr_line (split(/\r?\n/, $m_hdr)) {
  9134. if ($hdr_line =~ /^([^:]*?)[ \t]*:[ \t]*(.*)\z/s) {
  9135. my($fhead,$fbody) = ($1,$2);
  9136. my $str = hdr($fhead,$fbody,0,' '); # encode, wrap, ...
  9137. # re-split the result
  9138. ($fhead,$fbody) = ($1,$2) if $str =~ /^([^:]*):[ \t]*(.*)\z/s;
  9139. chomp($fbody);
  9140. do_log(5, "build_mime_entity %s: %s", $fhead,$fbody);
  9141. eval { # make sure _our_ source line number is reported on failure
  9142. $head->replace($fhead,$fbody); 1;
  9143. } or do {
  9144. $@ = "errno=$!" if $@ eq ''; chomp $@;
  9145. die $@ if $@ =~ /^timed out\b/; # resignal timeout
  9146. die sprintf("%s header field '%s: %s'",
  9147. ($@ eq '' ? "invalid" : "$@, "), $fhead,$fbody);
  9148. };
  9149. }
  9150. }
  9151. }
  9152. my(@prefix_lines);
  9153. if (defined $m_body) {
  9154. if ($flat && $attach_orig_message) {
  9155. my($pos,$j); # split $m_body into lines, retaining each \n
  9156. for ($pos=0; ($j=index($m_body,"\n",$pos)) >= 0; $pos = $j+1)
  9157. { push(@prefix_lines, substr($m_body,$pos,$j-$pos+1)) }
  9158. push(@prefix_lines, substr($m_body,$pos)) if $pos < length($m_body);
  9159. } else {
  9160. eval { # make sure _our_ source line number is reported on failure
  9161. $entity->attach(
  9162. Type => 'text/plain', Data => $m_body,
  9163. Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
  9164. ); $multipart_cnt++; 1;
  9165. } or do {
  9166. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9167. die $eval_stat;
  9168. };
  9169. }
  9170. }
  9171. # prepend a Return-Path to make available the envelope sender address
  9172. push(@prefix_lines, "\n") if @prefix_lines; # separates text from a message
  9173. push(@prefix_lines, sprintf("Return-Path: %s\n", $msginfo->sender_smtp));
  9174. if (defined $msginfo && $attach_orig_headers && !$attach_orig_message) {
  9175. # attach a header section only
  9176. do_log(4, "build_mime_entity: attaching just original header section");
  9177. eval { # make sure _our_ source line number is reported on failure
  9178. $entity->attach(
  9179. Type => $flat ? 'text/plain' : 'text/rfc822-headers', # RFC 3462
  9180. Encoding => $msginfo->header_8bit ? '8bit' : '7bit',
  9181. Data => [@prefix_lines, @{$msginfo->orig_header}],
  9182. Disposition => 'inline', Filename => 'header',
  9183. Description => 'Message header section',
  9184. ); $multipart_cnt++; 1;
  9185. } or do {
  9186. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9187. die $eval_stat;
  9188. };
  9189. } elsif (defined $msginfo && $attach_orig_message) {
  9190. # attach a complete message
  9191. my $password;
  9192. if ($msg_format eq 'attach') { # not 'arf' and not 'dsn'
  9193. $password = $msginfo->attachment_password; # already have it?
  9194. if (!defined $password) { # make one, and store it for later
  9195. $password = make_password(c('attachment_password'), $msginfo);
  9196. $msginfo->attachment_password($password);
  9197. }
  9198. }
  9199. if ($msg_format eq 'attach' && # not 'arf' and not 'dsn'
  9200. defined $password && $password ne '') {
  9201. # attach as a ZIP archive
  9202. $password = 'X' x length($password); # can't hurt to hide it
  9203. do_log(4, "build_mime_entity: attaching entire original message as zip");
  9204. my $archive_fn = wrap_message_into_archive($msginfo,\@prefix_lines);
  9205. local($1); $archive_fn =~ m{([^/]*)\z}; my $att_filename = $1;
  9206. eval { # make sure _our_ source line number is reported on failure
  9207. my $att = $entity->attach( # RFC 2046
  9208. Type => 'application/zip', Filename => $att_filename,
  9209. Path => $archive_fn, Encoding => 'base64',
  9210. Disposition => 'attachment', Description => 'Original message',
  9211. );
  9212. $multipart_cnt++; 1;
  9213. } or do {
  9214. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9215. die $eval_stat;
  9216. };
  9217. } else {
  9218. # attach as a normal message
  9219. do_log(4, "build_mime_entity: attaching entire original message, plain");
  9220. my $orig_mail_as_body;
  9221. my $msg = $msginfo->mail_text;
  9222. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  9223. $msg = $msg_str_ref if ref $msg_str_ref;
  9224. if (!defined $msg) {
  9225. # empty mail
  9226. } elsif (ref $msg eq 'SCALAR') {
  9227. # will be handled by ->attach
  9228. } elsif ($msg->isa('MIME::Entity')) {
  9229. die "attaching a MIME::Entity object is not implemented";
  9230. } else {
  9231. $orig_mail_as_body =
  9232. Amavis::MIME::Body::OnOpenFh->new($msginfo->mail_text,
  9233. \@prefix_lines, $msginfo->skip_bytes);
  9234. $orig_mail_as_body or die "Can't create Amavis::MIME::Body object: $!";
  9235. }
  9236. eval { # make sure _our_ source line number is reported on failure
  9237. my $att = $entity->attach( # RFC 2046
  9238. Type => $flat ? 'text/plain' : 'message/rfc822',
  9239. Encoding => ($msginfo->header_8bit || $msginfo->body_8bit) ?
  9240. '8bit' : '7bit',
  9241. Data => defined $orig_mail_as_body ? []
  9242. : !$msginfo->skip_bytes ? $msg
  9243. : substr($$msg, $msginfo->skip_bytes),
  9244. # Path => $msginfo->mail_text_fn,
  9245. $flat ? () : (Disposition => 'attachment', Filename => 'message',
  9246. Description => 'Original message'),
  9247. );
  9248. # direct access to tempfile handle
  9249. $att->bodyhandle($orig_mail_as_body) if defined $orig_mail_as_body;
  9250. $multipart_cnt++; 1;
  9251. } or do {
  9252. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9253. die $eval_stat;
  9254. };
  9255. }
  9256. }
  9257. $entity->make_singlepart if $multipart_cnt < 2;
  9258. $entity; # return the constructed MIME::Entity
  9259. }
  9260. # If $msg_format is 'dsn' generate a delivery status notification according
  9261. # to RFC 3462 (ex RFC 1892), RFC 3464 (ex RFC 1894) and RFC 3461 (ex RFC 1891).
  9262. # If $msg_format is 'arf' generate an abuse report according to RFC 5965
  9263. # - "An Extensible Format for Email Feedback Reports". If $msg_format is
  9264. # 'attach', generate a report message and attach the original message.
  9265. # If $msg_format is 'plain', generate a simple (flat) mail with the only
  9266. # MIME part being the original message (abuse@yahoo.com can't currently
  9267. # handle attachments in reports). Returns a message object, or undef if
  9268. # DSN is requested but not needed.
  9269. # $request_type: dsn, release, requeue, report
  9270. # $msg_format: dsn, arf, attach, plain, resend
  9271. # $feedback_type: abuse, dkim, fraud, miscategorized, not-spam,
  9272. # opt-out, virus, other
  9273. #
  9274. sub delivery_status_notification($$$;$$$$) { # ..._or_report
  9275. my($msginfo,$dsn_per_recip_capable,$builtins_ref,
  9276. $notif_recips,$request_type,$feedback_type,$msg_format) = @_;
  9277. my $notification; my $suppressed = 0;
  9278. if (!defined($msg_format)) {
  9279. $msg_format = $request_type eq 'dsn' ? 'dsn'
  9280. : $request_type eq 'report' ? c('report_format')
  9281. : c('release_format');
  9282. }
  9283. my($is_arf,$is_dsn,$is_attach,$is_plain) = (0) x 4;
  9284. if ($msg_format eq 'dsn') { $is_dsn = 1 }
  9285. elsif ($msg_format eq 'arf') { $is_arf = 1 }
  9286. elsif ($msg_format eq 'attach') { $is_attach = 1 }
  9287. else { $is_plain = 1 } # 'plain'
  9288. my $dsn_time = $msginfo->rx_time; # time of dsn creation - same as message
  9289. # use a reception time for consistency and to be resilient to clock jumps
  9290. $dsn_time = Time::HiRes::time if !$dsn_time; # now
  9291. my $rfc2822_dsn_time = rfc2822_timestamp($dsn_time);
  9292. my $sender = $msginfo->sender;
  9293. my $dsn_passed_on = $msginfo->dsn_passed_on; # NOTIFY=SUCCESS passed to MTA
  9294. my $per_recip_data = $msginfo->per_recip_data;
  9295. my $txt_recip = ''; # per-recipient part of dsn text according to RFC 3464
  9296. my $all_rejected = 0;
  9297. if (@$per_recip_data) {
  9298. $all_rejected = 1;
  9299. for my $r (@$per_recip_data) {
  9300. if ($r->recip_destiny != D_REJECT || $r->recip_smtp_response !~ /^5/)
  9301. { $all_rejected = 0; last }
  9302. }
  9303. }
  9304. my($min_spam_level, $max_spam_level) =
  9305. minmax(map($_->spam_level, @{$msginfo->per_recip_data}));
  9306. $min_spam_level = 0 if !defined $min_spam_level;
  9307. $max_spam_level = 0 if !defined $max_spam_level;
  9308. my $is_credible = $msginfo->sender_credible || '';
  9309. my $os_fingerprint = $msginfo->client_os_fingerprint;
  9310. my($cutoff_byrecip_maps, $cutoff_bysender_maps);
  9311. my($dsn_cutoff_level_bysender, $dsn_cutoff_level);
  9312. if ($is_dsn && $sender ne '') {
  9313. # for null sender it doesn't matter, as DSN will not be sent regardless
  9314. if ($is_credible) {
  9315. do_log(3, "DSN: sender is credible (%s), SA: %.3f, <%s>",
  9316. $is_credible, $max_spam_level, $sender);
  9317. $cutoff_byrecip_maps = ca('spam_crediblefrom_dsn_cutoff_level_maps');
  9318. $cutoff_bysender_maps =
  9319. ca('spam_crediblefrom_dsn_cutoff_level_bysender_maps');
  9320. } else {
  9321. do_log(5, "DSN: sender NOT credible, SA: %.3f, <%s>",
  9322. $max_spam_level, $sender);
  9323. $cutoff_byrecip_maps = ca('spam_dsn_cutoff_level_maps');
  9324. $cutoff_bysender_maps = ca('spam_dsn_cutoff_level_bysender_maps');
  9325. }
  9326. $dsn_cutoff_level_bysender = lookup2(0,$sender,$cutoff_bysender_maps);
  9327. }
  9328. my($any_succ,$any_fail,$any_delayed) = (0,0,0); local($1);
  9329. for my $r (!$is_dsn ? () : @$per_recip_data) { # prepare per-recip fields
  9330. my $recip = $r->recip_addr;
  9331. my $smtp_resp = $r->recip_smtp_response;
  9332. my $recip_done = $r->recip_done; # 2=relayed to MTA, 1=faked deliv/quarant
  9333. my $ccat_name = $r->setting_by_contents_category(\%ccat_display_names);
  9334. $ccat_name = "NonBlocking:$ccat_name" if !defined($r->blocking_ccat);
  9335. my $spam_level = $r->spam_level;
  9336. if (!$recip_done) {
  9337. my $fwd_m = $r->delivery_method;
  9338. if (!defined $fwd_m) {
  9339. do_log(-2,"TROUBLE: recipient not done, undefined delivery_method: ".
  9340. "<%s> %s", $recip,$smtp_resp);
  9341. } elsif ($fwd_m eq '') { # e.g. milter
  9342. # as far as we are concerned all is ok, delivery will be performed
  9343. # by a helper program or MTA
  9344. $smtp_resp = "250 2.5.0 Ok, continue delivery";
  9345. } else {
  9346. do_log(-2,"TROUBLE: recipient not done: <%s> %s", $recip,$smtp_resp);
  9347. }
  9348. }
  9349. my $smtp_resp_class = $smtp_resp =~ /^(\d)/ ? $1 : '0';
  9350. my $smtp_resp_code = $smtp_resp =~ /^(\d+)/ ? $1 : '0';
  9351. my $dsn_notify = $r->dsn_notify;
  9352. my($notify_on_failure,$notify_on_success,$notify_on_delay,$notify_never) =
  9353. (0,0,0,0);
  9354. if (!defined($dsn_notify)) {
  9355. $notify_on_failure = $notify_on_delay = 1;
  9356. } else {
  9357. for (@$dsn_notify) { # validity of the list has already been checked
  9358. if ($_ eq 'FAILURE') { $notify_on_failure = 1 }
  9359. elsif ($_ eq 'SUCCESS') { $notify_on_success = 1 }
  9360. elsif ($_ eq 'DELAY') { $notify_on_delay = 1 }
  9361. elsif ($_ eq 'NEVER') { $notify_never = 1 }
  9362. }
  9363. }
  9364. if ($notify_never || $sender eq '')
  9365. { $notify_on_failure = $notify_on_success = $notify_on_delay = 0 }
  9366. my $dest = $r->recip_destiny;
  9367. my $remote_or_local = $recip_done==2 ? 'from MTA' :
  9368. $recip_done==1 ? '.' : # this agent
  9369. 'status-to-be-passed-back';
  9370. # warn_sender is an old relic and does not fit well into DSN concepts;
  9371. # we'll sneak it in, pretending to cause a DELAY notification
  9372. my $warn_sender =
  9373. $notify_on_delay && $smtp_resp_class eq '2' && $recip_done==2 &&
  9374. $r->setting_by_contents_category(cr('warnsender_by_ccat'));
  9375. ll(5) && do_log(5,
  9376. "dsn: %s %s %s <%s> -> <%s>: on_succ=%d, on_dly=%d, ".
  9377. "on_fail=%d, never=%d, warn_sender=%s, DSN_passed_on=%s, ".
  9378. "destiny=%s, mta_resp: \"%s\"",
  9379. $remote_or_local, $smtp_resp_code, $ccat_name, $sender, $recip,
  9380. $notify_on_success, $notify_on_delay, $notify_on_failure,
  9381. $notify_never, $warn_sender, $dsn_passed_on, $dest, $smtp_resp);
  9382. # clearly log common cases to facilitate troubleshooting;
  9383. # first look for some standard reasons for not sending a DSN
  9384. if ($smtp_resp_class eq '4') {
  9385. do_log(4, "DSN: TMPFAIL %s %s %s, not to be reported: <%s> -> <%s>",
  9386. $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip);
  9387. } elsif ($smtp_resp_class eq '5' && $dest==D_REJECT &&
  9388. ($dsn_per_recip_capable || $all_rejected)) {
  9389. do_log(4, "DSN: FAIL %s %s %s, status propagated back: <%s> -> <%s>",
  9390. $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip);
  9391. } elsif ($smtp_resp_class eq '5' && !$notify_on_failure) {
  9392. $suppressed = 1;
  9393. do_log($recip_done==2 ? 0 : 4, # log level 0 for remotes, RFC 3461 5.2.2d
  9394. "DSN: FAIL %s %s %s, %s requested to be IGNORED: <%s> -> <%s>",
  9395. $remote_or_local,$smtp_resp_code,$ccat_name,
  9396. $notify_never?'explicitly':'implicitly', $sender, $recip);
  9397. } elsif ($smtp_resp_class eq '2' && !$notify_on_success && !$warn_sender) {
  9398. my $fmt = $dest==D_DISCARD
  9399. ? "SUCC (discarded) %s %s %s, destiny=DISCARD"
  9400. : "SUCC %s %s %s, no DSN requested";
  9401. do_log(5, "DSN: $fmt: <%s> -> <%s>",
  9402. $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip);
  9403. } elsif ($smtp_resp_class eq '2' && $notify_on_success && $dsn_passed_on &&
  9404. !$warn_sender) {
  9405. do_log(5, "DSN: SUCC %s %s %s, DSN parameters PASSED-ON: <%s> -> <%s>",
  9406. $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip);
  9407. } elsif ($notify_never || $sender eq '') { # test sender just in case
  9408. $suppressed = 1;
  9409. do_log(5, "DSN: NEVER %s %s, <%s> -> %s",
  9410. $smtp_resp_code,$ccat_name,$sender,$recip);
  9411. # next, look for some good _excuses_ for not sending a DSN
  9412. } elsif ($dest==D_DISCARD) { # requested by final_*_destiny
  9413. $suppressed = 1;
  9414. do_log(4, "DSN: FILTER %s %s %s, destiny=DISCARD: <%s> -> <%s>",
  9415. $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip);
  9416. } elsif (defined $r->dsn_suppress_reason) {
  9417. $suppressed = 1;
  9418. do_log(3, "DSN: FILTER %s %s, suppress reason: %s, <%s> -> <%s>",
  9419. $smtp_resp_code, $ccat_name, $r->dsn_suppress_reason,
  9420. $sender,$recip);
  9421. } elsif (defined $dsn_cutoff_level_bysender &&
  9422. $spam_level >= $dsn_cutoff_level_bysender) {
  9423. $suppressed = 1;
  9424. do_log(3, "DSN: FILTER %s %s, spam level %.3f exceeds cutoff %s%s, ".
  9425. "<%s> -> <%s>", $smtp_resp_code, $ccat_name,
  9426. $spam_level, $dsn_cutoff_level_bysender,
  9427. !$is_credible ? '' : ", (credible: $is_credible)",
  9428. $sender, $recip);
  9429. } elsif (defined($cutoff_byrecip_maps) &&
  9430. ( $dsn_cutoff_level=lookup2(0,$recip,$cutoff_byrecip_maps),
  9431. defined($dsn_cutoff_level) &&
  9432. ( $spam_level >= $dsn_cutoff_level ||
  9433. ( $r->recip_blacklisted_sender &&
  9434. !$r->recip_whitelisted_sender) )
  9435. ) ) {
  9436. $suppressed = 1;
  9437. do_log(3, "DSN: FILTER %s %s, spam level %.3f exceeds ".
  9438. "by-recipient cutoff %s%s, <%s> -> <%s>",
  9439. $smtp_resp_code, $ccat_name,
  9440. $spam_level, $dsn_cutoff_level,
  9441. !$is_credible ? '' : ", (credible: $is_credible)",
  9442. $sender, $recip);
  9443. } elsif (defined($msginfo->is_bulk) &&
  9444. ccat_maj($r->contents_category) > CC_CLEAN) {
  9445. $suppressed = 1;
  9446. do_log(3, "DSN: FILTER %s %s, suppressed, bulk mail (%s), <%s> -> <%s>",
  9447. $smtp_resp_code,$ccat_name,$msginfo->is_bulk,$sender,$recip);
  9448. } elsif ($os_fingerprint =~ /^Windows\b/ && # hard-coded limits!
  9449. !$msginfo->dkim_envsender_sig && # a hack
  9450. $spam_level >=
  9451. ($os_fingerprint=~/^Windows XP(?![^(]*\b2000 SP)/ ? 5 : 8)) {
  9452. $os_fingerprint =~ /^(\S+\s+\S+)/;
  9453. do_log(3, "DSN: FILTER %s %s, suppressed for mail from %s ".
  9454. "at %s, score=%s, <%s> -> <%s>", $smtp_resp_code, $ccat_name,
  9455. $1, $msginfo->client_addr, $spam_level, $sender,$recip);
  9456. } else {
  9457. # RFC 3461, section 5.2.8: "A single DSN may describe attempts to deliver
  9458. # a message to multiple recipients of that message. If a DSN is issued
  9459. # for some recipients in an SMTP transaction and not for others according
  9460. # to the rules above, the DSN SHOULD NOT contain information for
  9461. # recipients for whom DSNs would not otherwise have been issued."
  9462. $txt_recip .= "\n"; # empty line between groups of per-recipient fields
  9463. my $dsn_orcpt = $r->dsn_orcpt;
  9464. if (defined $dsn_orcpt) {
  9465. my($addr_type,$orcpt) = orcpt_decode($dsn_orcpt);
  9466. $txt_recip .= "Original-Recipient: " .
  9467. sanitize_str($addr_type.';'.$orcpt) . "\n";
  9468. }
  9469. my $remote_mta = $r->recip_remote_mta;
  9470. if (!defined($dsn_orcpt) && $remote_mta ne '' &&
  9471. $r->recip_final_addr ne $recip) {
  9472. $txt_recip .= "X-NextToLast-Final-Recipient: rfc822;" .
  9473. quote_rfc2821_local($recip) . "\n";
  9474. $txt_recip .= "Final-Recipient: rfc822;" .
  9475. quote_rfc2821_local($r->recip_final_addr) . "\n";
  9476. } else {
  9477. $txt_recip .= "Final-Recipient: rfc822;" .
  9478. quote_rfc2821_local($recip) . "\n";
  9479. }
  9480. local($1,$2,$3); my($smtp_resp_code,$smtp_resp_enhcode,$smtp_resp_msg);
  9481. if ($smtp_resp =~ /^ (\d{3}) [ \t-] [ \t]* ([245] \. \d{1,3} \. \d{1,3})?
  9482. \s* (.*) \z/xs) {
  9483. ($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3);
  9484. } else {
  9485. $smtp_resp_msg = $smtp_resp;
  9486. }
  9487. if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) {
  9488. $smtp_resp_enhcode = "$1.0.0";
  9489. }
  9490. my $action; # failed / relayed / delivered / expanded
  9491. if ($recip_done == 2) { # truly forwarded to MTA
  9492. $action = $smtp_resp_class eq '5' ? 'failed' # remote reject
  9493. : $smtp_resp_class ne '2' ? undef # shouldn't happen
  9494. : !$dsn_passed_on ? 'relayed' # relayed to non-conforming MTA
  9495. : $warn_sender ? 'delayed' # disguised as a DELAY notification
  9496. : undef; # shouldn't happen
  9497. } elsif ($recip_done == 1) { # faked delivery to bit bucket or quarantine
  9498. $action = $smtp_resp_class eq '5' ? 'failed' # local reject
  9499. : $smtp_resp_class eq '2' ? 'delivered' # discard / bit bucket
  9500. : undef; # shouldn't happen
  9501. } elsif (!defined($recip_done) || $recip_done == 0) {
  9502. $action = $smtp_resp_class eq '2' ? 'relayed' #????
  9503. : undef; # shouldn't happen
  9504. }
  9505. defined $action or die "Assert failed: $smtp_resp, $smtp_resp_class, ".
  9506. "$recip_done, $dsn_passed_on";
  9507. if ($action eq 'failed') { $any_fail=1 }
  9508. elsif ($action eq 'delayed') { $any_delayed=1 } else { $any_succ=1 }
  9509. $txt_recip .= "Action: $action\n";
  9510. $txt_recip .= "Status: $smtp_resp_enhcode\n";
  9511. my $rem_smtp_resp = $r->recip_remote_mta_smtp_response;
  9512. if ($warn_sender && $action eq 'delayed') {
  9513. $smtp_resp = '250 2.6.0 Bad message, but will be delivered anyway';
  9514. } elsif ($remote_mta ne '' && $rem_smtp_resp ne '') {
  9515. $txt_recip .= "Remote-MTA: dns; $remote_mta\n";
  9516. $smtp_resp = $rem_smtp_resp;
  9517. } elsif ($smtp_resp !~ /\n/ && length($smtp_resp) > 78-23) { # wrap magic
  9518. # take liberty to wrap our own SMTP responses
  9519. $smtp_resp = wrap_string("x" x (23-11) . $smtp_resp, 78-11,'','',0);
  9520. # length(" 554 5.0.0 ") = 11; length("Diagnostic-Code: smtp; ") = 23
  9521. # insert and then remove prefix to maintain consistent wrapped size
  9522. $smtp_resp =~ s/^x{12}//;
  9523. # wrap response code according to RFC 3461 section 9.2
  9524. $smtp_resp = join("\n", @{wrap_smtp_resp($smtp_resp)});
  9525. }
  9526. $smtp_resp =~ s/\n(?![ \t])/\n /gs;
  9527. $txt_recip .= "Diagnostic-Code: smtp; $smtp_resp\n";
  9528. $txt_recip .= "Last-Attempt-Date: $rfc2822_dsn_time\n";
  9529. my $final_log_id = $msginfo->log_id;
  9530. $final_log_id .= '/' . $msginfo->mail_id if defined $msginfo->mail_id;
  9531. $txt_recip .= sprintf("Final-Log-ID: %s\n", $final_log_id);
  9532. do_log(2, "DSN: NOTIFICATION: Action:%s, %s %s %s, spam level %.3f, ".
  9533. "<%s> -> <%s>", $action,
  9534. $recip_done==2 && $action ne 'delayed' ? 'RELAYED' : 'LOCAL',
  9535. $smtp_resp_code, $ccat_name, $spam_level, $sender, $recip);
  9536. }
  9537. } # endfor per_recip_data
  9538. if ( $is_arf || $is_plain || $is_attach ||
  9539. ($is_dsn && ($any_succ || $any_fail || $any_delayed)) ) {
  9540. my(@hdr_to) = defined $notif_recips ? qquote_rfc2821_local(@$notif_recips)
  9541. : map($_->recip_addr_smtp, @$per_recip_data);
  9542. my $hdr_from = $msginfo->setting_by_contents_category(
  9543. $is_dsn ? cr('hdrfrom_notify_sender_by_ccat') :
  9544. $request_type eq 'report' ? cr('hdrfrom_notify_report_by_ccat') :
  9545. cr('hdrfrom_notify_release_by_ccat') );
  9546. $hdr_from = expand_variables($hdr_from);
  9547. # use the provided template text
  9548. my(%mybuiltins) = %$builtins_ref; # make a local copy
  9549. # not really needed, these header fields are overridden later
  9550. $mybuiltins{'f'} = $hdr_from;
  9551. $mybuiltins{'T'} = \@hdr_to;
  9552. $mybuiltins{'d'} = $rfc2822_dsn_time;
  9553. $mybuiltins{'report_format'} = $msg_format;
  9554. $mybuiltins{'feedback_type'} = $feedback_type;
  9555. # RFC 3461 section 6.2: "If a DSN contains no notifications of
  9556. # delivery failure, the MTA SHOULD return only the header section."
  9557. my $dsn_ret = $msginfo->dsn_ret;
  9558. my $attach_full_msg =
  9559. !$is_dsn ? 1 : (defined $dsn_ret && $dsn_ret eq 'FULL' && $any_fail);
  9560. if ($attach_full_msg && $is_dsn) {
  9561. # apologize in the log, we should have supplied the full message, yet
  9562. # RFC 3461 section 6.2 gives us an excuse: "However, if the length of the
  9563. # message is greater than some implementation-specified length, the MTA
  9564. # MAY return only the headers even if the RET parameter specified FULL."
  9565. do_log(1, "DSN RET=%s requested, but we'll only attach a header section",
  9566. $dsn_ret);
  9567. $attach_full_msg = 0; # override, just attach a header section
  9568. }
  9569. my $template_ref = $msginfo->setting_by_contents_category(
  9570. $is_dsn ? cr('notify_sender_templ_by_ccat') :
  9571. $request_type eq 'report' ? cr('notify_report_templ_by_ccat') :
  9572. cr('notify_release_templ_by_ccat') );
  9573. my $report_str_ref = expand($template_ref, \%mybuiltins);
  9574. my $report_entity = build_mime_entity($report_str_ref, $msginfo,
  9575. $is_dsn ? 'multipart/report; report-type=delivery-status' :
  9576. $is_arf ? 'multipart/report; report-type=feedback-report' :
  9577. 'multipart/mixed',
  9578. $msg_format, $is_plain, 1, $attach_full_msg);
  9579. my $head = $report_entity->head;
  9580. # RFC 3464: The From field of the message header section of the DSN SHOULD
  9581. # contain the address of a human who is responsible for maintaining the
  9582. # mail system at the Reporting MTA site (e.g. Postmaster), so that a reply
  9583. # to the DSN will reach that person.
  9584. # Override header fields from the template:
  9585. eval { $head->replace('From', $hdr_from); 1 }
  9586. or do { chomp $@; die $@ };
  9587. eval { $head->replace('To', join(', ',@hdr_to)); 1 }
  9588. or do { chomp $@; die $@ };
  9589. eval { $head->replace('Date', $rfc2822_dsn_time); 1 }
  9590. or do { chomp $@; die $@ };
  9591. my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461
  9592. $dsn_envid = sanitize_str(xtext_decode($dsn_envid)) if defined $dsn_envid;
  9593. my $txt_msg = ''; # per-message part of a report
  9594. if ($is_arf) { # abuse report format - RFC 5965
  9595. # abuse, dkim, fraud, miscategorized, not-spam, opt-out, virus, other
  9596. $txt_msg .= "Version: 1\n"; # required
  9597. $txt_msg .= "Feedback-Type: $feedback_type\n"; # required
  9598. # User-Agent must comply with RFC 2616, section 14.43
  9599. my $ua_version = "$myproduct_name/$myversion_id ($myversion_date)";
  9600. $txt_msg .= "User-Agent: $ua_version\n"; # required
  9601. $txt_msg .= "Reporting-MTA: dns; " . c('myhostname') . "\n";
  9602. # optional fields:
  9603. $txt_msg .= "Arrival-Date: ". rfc2822_timestamp($msginfo->rx_time) ."\n";
  9604. my $cl_ip_addr = $msginfo->client_addr;
  9605. $cl_ip_addr = 'IPv6:'.$cl_ip_addr if $cl_ip_addr =~ /:.*:/ &&
  9606. $cl_ip_addr !~ /^IPv6:/i;
  9607. $txt_msg .= "Source-IP: $cl_ip_addr\n" if defined $cl_ip_addr;
  9608. # draft-kucherawy-marf-source-ports:
  9609. my $cl_ip_port = $msginfo->client_port;
  9610. $txt_msg .= "Source-Port: $cl_ip_port\n" if defined $cl_ip_port;
  9611. $txt_msg .= "Original-Envelope-Id: $dsn_envid\n" if defined $dsn_envid;
  9612. $txt_msg .= "Original-Mail-From: " . $msginfo->sender_smtp . "\n";
  9613. for my $r (@$per_recip_data)
  9614. { $txt_msg .= "Original-Rcpt-To: " . $r->recip_addr_smtp . "\n" }
  9615. my $sigs_ref = $msginfo->dkim_signatures_valid;
  9616. if ($sigs_ref) {
  9617. for my $sig (@$sigs_ref) {
  9618. my $type = $sig->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM';
  9619. $txt_msg .= sprintf("Reported-Domain: %s (valid %s signature by)\n",
  9620. $sig->domain, $type);
  9621. }
  9622. }
  9623. if (c('enable_dkim_verification')) {
  9624. for (Amavis::DKIM::generate_authentication_results($msginfo,0)) {
  9625. my $h = $_; $h =~ tr/\n//d; # remove potential folding points
  9626. $txt_msg .= "Authentication-Results: $h\n";
  9627. }
  9628. }
  9629. $txt_msg .= "Incidents: 1\n";
  9630. # Reported-URI
  9631. } elsif ($is_dsn) { # DSN - per-msg part of dsn text according to RFC 3464
  9632. my $conn = $msginfo->conn_obj;
  9633. my $from_mta = $conn->smtp_helo;
  9634. my $client_ip = $conn->client_ip;
  9635. $txt_msg .= "Reporting-MTA: dns; " . c('myhostname') . "\n";
  9636. $txt_msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n"
  9637. if $from_mta ne '';
  9638. $txt_msg .= "Arrival-Date: ". rfc2822_timestamp($msginfo->rx_time) ."\n";
  9639. $txt_msg .= "Original-Envelope-Id: $dsn_envid\n" if defined $dsn_envid;
  9640. }
  9641. if ($is_dsn || $is_arf) { # attach a delivery-status or a feedback-report
  9642. eval { # make sure our source line number is reported in case of failure
  9643. $report_entity->add_part(
  9644. MIME::Entity->build(Top => 0,
  9645. Type => $is_dsn ? 'message/delivery-status'
  9646. : 'message/feedback-report',
  9647. Encoding => '7bit', Disposition => 'inline',
  9648. Filename => $is_arf ? 'arf_status' : 'dsn_status',
  9649. Description => $is_arf ? "\u$feedback_type report" :
  9650. $any_fail ? 'Delivery error report' :
  9651. $any_delayed ? 'Delivery delay report' :
  9652. 'Delivery report',
  9653. Data => $txt_msg.$txt_recip),
  9654. 1); # insert as a second mime part (at offset 1)
  9655. 1;
  9656. } or do {
  9657. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9658. die $eval_stat;
  9659. };
  9660. }
  9661. my $mailfrom = $is_dsn ? '' # DSN envelope sender must be empty
  9662. : unquote_rfc2821_local( (parse_address_list($hdr_from))[0] );
  9663. $notification = Amavis::In::Message->new;
  9664. $notification->rx_time($dsn_time);
  9665. $notification->log_id($msginfo->log_id);
  9666. $notification->partition_tag($msginfo->partition_tag);
  9667. $notification->conn_obj($msginfo->conn_obj);
  9668. $notification->originating(
  9669. ($request_type eq 'dsn' || $request_type eq 'report') ? 1 : 0);
  9670. # $notification->body_type('7BIT');
  9671. $notification->mail_text($report_entity);
  9672. $notification->sender($mailfrom);
  9673. $notification->sender_smtp(qquote_rfc2821_local($mailfrom));
  9674. $notification->auth_submitter('<>');
  9675. $notification->auth_user(c('amavis_auth_user'));
  9676. $notification->auth_pass(c('amavis_auth_pass'));
  9677. if (defined $hdr_from) {
  9678. my(@rfc2822_from) = map(unquote_rfc2821_local($_),
  9679. parse_address_list($hdr_from));
  9680. $notification->rfc2822_from($rfc2822_from[0]);
  9681. }
  9682. my $bcc;
  9683. if ($request_type eq 'dsn' || $request_type eq 'report') {
  9684. $bcc = $msginfo->setting_by_contents_category(cr('dsn_bcc_by_ccat'));
  9685. }
  9686. $notification->recips([(defined $notif_recips ? @$notif_recips
  9687. : map($_->recip_addr, @$per_recip_data)),
  9688. defined $bcc && $bcc ne '' ? $bcc : () ], 1);
  9689. my $notif_m = c('notify_method');
  9690. $_->delivery_method($notif_m) for @{$notification->per_recip_data};
  9691. }
  9692. # $suppressed is true if DNS would be needed, but either the sender requested
  9693. # that DSN is not to be sent, or it is believed the bounce would not reach
  9694. # the correct sender (faked sender with viruses or spam);
  9695. # $notification is undef if DSN is not needed
  9696. ($notification,$suppressed);
  9697. }
  9698. # Return a triple of arrayrefs of quoted recipient addresses (the first lists
  9699. # recipients with successful delivery status, the second lists all the rest),
  9700. # plus a list of short per-recipient delivery reports for failed deliveries,
  9701. # that can be used in the first MIME part (the free text format) of delivery
  9702. # status notifications.
  9703. #
  9704. sub delivery_short_report($) {
  9705. my($msginfo) = @_;
  9706. my(@succ_recips, @failed_recips, @failed_recips_full);
  9707. for my $r (@{$msginfo->per_recip_data}) {
  9708. my $remote_mta = $r->recip_remote_mta;
  9709. my $smtp_resp = $r->recip_smtp_response;
  9710. my $qrecip_addr = scalar(qquote_rfc2821_local($r->recip_addr));
  9711. if ($r->recip_destiny == D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)) {
  9712. push(@succ_recips, $qrecip_addr);
  9713. } else {
  9714. push(@failed_recips, $qrecip_addr);
  9715. push(@failed_recips_full, sprintf("%s:%s\n %s", $qrecip_addr,
  9716. (!defined($remote_mta)||$remote_mta eq '' ?'' :" [$remote_mta] said:"),
  9717. $smtp_resp));
  9718. }
  9719. }
  9720. (\@succ_recips, \@failed_recips, \@failed_recips_full);
  9721. }
  9722. # Build a new MIME::Entity object based on the original mail, but hopefully
  9723. # safer to mail readers: conventional mail header fields are retained,
  9724. # original mail becomes an attachment of type 'message/rfc822'.
  9725. # Text in $first_part becomes the first MIME part of type 'text/plain',
  9726. # $first_part may be a scalar string or a ref to a list of lines
  9727. #
  9728. sub defanged_mime_entity($$) {
  9729. my($msginfo,$first_part) = @_;
  9730. my $new_entity;
  9731. $_ = safe_encode(c('bdy_encoding'), $_)
  9732. for (ref $first_part ? @$first_part : $first_part);
  9733. eval { # make sure _our_ source line number is reported in case of failure
  9734. $new_entity = MIME::Entity->build(
  9735. Type => 'multipart/mixed', 'X-Mailer' => undef);
  9736. 1;
  9737. } or do {
  9738. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9739. die $eval_stat;
  9740. };
  9741. # reinserting some of the original header fields to a new header, sanitized
  9742. my $hdr_edits = $msginfo->header_edits;
  9743. if (!$hdr_edits) {
  9744. $hdr_edits = Amavis::Out::EditHeader->new;
  9745. $msginfo->header_edits($hdr_edits);
  9746. }
  9747. my(%desired_field);
  9748. for (qw(Received From Sender To Cc Reply-To Date Message-ID
  9749. Resent-From Resent-Sender Resent-To Resent-Cc
  9750. Resent-Date Resent-Message-ID In-Reply-To References Subject
  9751. Comments Keywords Organization Organisation User-Agent X-Mailer
  9752. DKIM-Signature DomainKey-Signature))
  9753. { $desired_field{lc($_)} = 1 };
  9754. local($1,$2);
  9755. for my $curr_head (@{$msginfo->orig_header}) { # array of header fields
  9756. # obsolete RFC 822 syntax allowed whitespace before colon
  9757. my($field_name, $field_body) =
  9758. $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s
  9759. ? ($1, $2) : (undef, $curr_head);
  9760. if ($desired_field{lc($field_name)}) { # only desired header fields
  9761. # protect NUL, CR, and characters with codes above \177
  9762. $field_body =~ s{ ( [^\001-\014\016-\177] ) }
  9763. { sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o',
  9764. ord($1)) }gsxe;
  9765. # protect NL in illegal all-whitespace continuation lines
  9766. $field_body =~ s{\n([ \t]*)(?=\n)}{\\012$1}gs;
  9767. $field_body =~ s{^(.{995}).{4,}$}{$1...}mg; # truncate lines to 998
  9768. chomp($field_body); # note that field body is already folded
  9769. if (lc($field_name) eq 'subject') {
  9770. # needs to be inserted directly into new header section so that it
  9771. # can be subjected to header edits, like inserting ***UNCHECKED***
  9772. eval { $new_entity->head->add($field_name,$field_body); 1 }
  9773. or do {chomp $@; die $@};
  9774. } else {
  9775. $hdr_edits->append_header($field_name,$field_body,2);
  9776. }
  9777. }
  9778. }
  9779. eval {
  9780. $new_entity->attach(
  9781. Type => 'text/plain',
  9782. Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
  9783. Data => $first_part);
  9784. 1;
  9785. } or do {
  9786. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9787. die $eval_stat;
  9788. };
  9789. # prepend a Return-Path to make available the envelope sender address
  9790. my $rp = sprintf("Return-Path: %s\n", $msginfo->sender_smtp);
  9791. my $orig_mail_as_body;
  9792. my $msg = $msginfo->mail_text;
  9793. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  9794. $msg = $msg_str_ref if ref $msg_str_ref;
  9795. if (!defined $msg) {
  9796. # empty mail
  9797. } elsif (ref $msg eq 'SCALAR') {
  9798. # will be handled by ->attach
  9799. } elsif ($msg->isa('MIME::Entity')) {
  9800. die "attaching a MIME::Entity object is not implemented";
  9801. } else {
  9802. $orig_mail_as_body =
  9803. Amavis::MIME::Body::OnOpenFh->new($msginfo->mail_text,
  9804. [$rp], $msginfo->skip_bytes);
  9805. $orig_mail_as_body or die "Can't create Amavis::MIME::Body object: $!";
  9806. }
  9807. eval {
  9808. my $att = $new_entity->attach( # RFC 2046
  9809. Type => 'message/rfc822; x-spam-type=original',
  9810. Encoding =>($msginfo->header_8bit || $msginfo->body_8bit) ?'8bit':'7bit',
  9811. Data => defined $orig_mail_as_body ? []
  9812. : !$msginfo->skip_bytes ? $msg
  9813. : substr($$msg, $msginfo->skip_bytes),
  9814. # Path => $msginfo->mail_text_fn,
  9815. Description => 'Original message',
  9816. Filename => 'message', Disposition => 'attachment',
  9817. );
  9818. # direct access to tempfile handle
  9819. $att->bodyhandle($orig_mail_as_body) if defined $orig_mail_as_body;
  9820. 1;
  9821. } or do {
  9822. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  9823. die $eval_stat;
  9824. };
  9825. $new_entity;
  9826. }
  9827. # Fill-in a message object with information based on a quarantined mail.
  9828. # Expects $msginfo->mail_text to be a file handle (not a Mime::Entity object),
  9829. # leaves it positioned at the beginning of a mail body (not to be relied upon).
  9830. # If given a BSMTP file, expects that it contains a single message only.
  9831. #
  9832. sub msg_from_quarantine($$$) {
  9833. my($msginfo,$request_type,$feedback_type) = @_;
  9834. my $fh = $msginfo->mail_text;
  9835. my $sender_override = $msginfo->sender;
  9836. my $recips_data_override = $msginfo->per_recip_data;
  9837. my $quarantine_id = $msginfo->mail_id;
  9838. $quarantine_id = '' if !defined $quarantine_id;
  9839. my $reporting = $request_type eq 'report';
  9840. my $release_m;
  9841. if ($request_type eq 'requeue') {
  9842. $release_m = c('requeue_method');
  9843. $release_m ne '' or die "requeue_method is unspecified";
  9844. } else { # 'release' or 'report'
  9845. $release_m = c('release_method');
  9846. $release_m = c('notify_method') if !defined $release_m || $release_m eq '';
  9847. $release_m ne '' or die "release_method and notify_method are unspecified";
  9848. }
  9849. $msginfo->originating(0); # let's make it explicit; disables DKIM signing
  9850. $msginfo->auth_submitter('<>');
  9851. $msginfo->auth_user(c('amavis_auth_user'));
  9852. $msginfo->auth_pass(c('amavis_auth_pass'));
  9853. $fh->seek($msginfo->skip_bytes, 0) or die "Can't rewind mail file: $!";
  9854. my $bsmtp = 0; # message stored in an RFC 2442 format?
  9855. my($qid,$sender,@recips_all,@recips_blocked);
  9856. my $have_recips_blocked = 0; my $curr_head;
  9857. my $ln; my $eof = 0; my $position = 0;
  9858. my $offset_bytes = 0; # file position just past the prefixed header fields
  9859. # extract envelope information from the quarantine file
  9860. do_log(4, "msg_from_quarantine: releasing %s", $quarantine_id);
  9861. for (;;) {
  9862. if ($eof) { $ln = "\n" }
  9863. else {
  9864. $! = 0; $ln = $fh->getline;
  9865. if (!defined($ln)) {
  9866. $eof = 1; $ln = "\n"; # fake a missing header/body separator line
  9867. $! == 0 or die "Error reading file ".$msginfo->mail_text_fn.": $!";
  9868. }
  9869. }
  9870. if ($ln =~ /^[ \t]/) { $curr_head .= $ln }
  9871. else {
  9872. my $next_head = $ln; local($1,$2);
  9873. local($_) = $curr_head; chomp; s/\n(?=[ \t])//gs; # unfold
  9874. if (!defined($curr_head)) { # first time
  9875. } elsif (/^(?:EHLO|HELO)(?: |$)/i) { $bsmtp = 1;
  9876. } elsif (/^MAIL FROM:[ \t]*(<.*>)/i) {
  9877. $bsmtp = 1; $sender = $1; $sender = unquote_rfc2821_local($sender);
  9878. } elsif ( $bsmtp && /^RCPT TO:[ \t]*(<.*>)/i) {
  9879. push(@recips_all, unquote_rfc2821_local($1));
  9880. } elsif ( $bsmtp && /^(?:DATA|NOOP)$/i) {
  9881. } elsif ( $bsmtp && /^RSET$/i) {
  9882. $sender = undef; @recips_all = (); @recips_blocked = (); $qid = undef;
  9883. } elsif ( $bsmtp && /^QUIT$/i) { last;
  9884. } elsif (!$bsmtp && /^Return-Path:/si) {
  9885. } elsif (!$bsmtp && /^Delivered-To:/si) {
  9886. } elsif (!$bsmtp && /^X-Envelope-From:[ \t]*(.*)$/si) {
  9887. if (!defined $sender) {
  9888. my(@addr_list) = parse_address_list($1);
  9889. @addr_list >= 1 or die "Address missing in X-Envelope-From";
  9890. @addr_list <= 1 or die "More than one address in X-Envelope-From";
  9891. $sender = unquote_rfc2821_local($addr_list[0]);
  9892. }
  9893. } elsif (!$bsmtp && /^X-Envelope-To:[ \t]*(.*)$/si) {
  9894. my(@addr_list) = parse_address_list($1);
  9895. push(@recips_all, map(unquote_rfc2821_local($_), @addr_list));
  9896. } elsif (!$bsmtp && /^X-Envelope-To-Blocked:[ \t]*(.*)$/si) {
  9897. my(@addr_list) = parse_address_list($1);
  9898. push(@recips_blocked, map(unquote_rfc2821_local($_), @addr_list));
  9899. $have_recips_blocked = 1;
  9900. } elsif (/^X-Quarantine-ID:[ \t]*(.*)$/si) {
  9901. $qid = $1; $qid = $1 if $qid =~ /^<(.*)>\z/s;
  9902. } elsif (!$reporting && /^X-Amavis-(?:Hold|Alert|Modified|PenPals|
  9903. PolicyBank|OS-Fingerprint):/xsi) {
  9904. # skip
  9905. } elsif (!$reporting && /^(?:X-Spam|X-CRM114)-.+:/si) {
  9906. # skip header fields inserted by us
  9907. } else {
  9908. last; # end of known header fields, to be marked as 'skip_bytes'
  9909. }
  9910. last if $next_head eq "\n"; # end-of-header-section reached
  9911. $offset_bytes = $position; # move past last processed header field
  9912. $curr_head = $next_head;
  9913. }
  9914. $position += length($ln);
  9915. }
  9916. @recips_blocked = @recips_all if !$have_recips_blocked; # pre-2.6.0 compatib
  9917. my(@except);
  9918. if (@recips_blocked < @recips_all) {
  9919. for my $rec (@recips_all)
  9920. { push(@except,$rec) if !grep($rec eq $_, @recips_blocked) }
  9921. }
  9922. my $sender_smtp = qquote_rfc2821_local($sender);
  9923. do_log(0,"Quarantined message %s (%s): %s %s -> %s%s",
  9924. $request_type, $feedback_type, $quarantine_id, $sender_smtp,
  9925. join(',', qquote_rfc2821_local(@recips_blocked)),
  9926. !@except ? '' : (", (excluded: ".
  9927. join(',', qquote_rfc2821_local(@except)) . " )" ));
  9928. my(@m);
  9929. if (!defined($qid)) { push(@m, 'missing X-Quarantine-ID') }
  9930. elsif ($qid ne $quarantine_id) {
  9931. push(@m, sprintf("stored quar. ID '%s' does not match requested ID '%s'",
  9932. $qid,$quarantine_id));
  9933. }
  9934. push(@m, 'missing '.($bsmtp?'MAIL FROM':'X-Envelope-From or Return-Path'))
  9935. if !defined $sender;
  9936. push(@m, 'missing '.($bsmtp?'RCPT TO' :'X-Envelope-To')) if !@recips_all;
  9937. do_log(0, "Quarantine %s %s: %s",
  9938. $request_type, $quarantine_id, join("; ",@m)) if @m;
  9939. if ($qid ne $quarantine_id)
  9940. { die "Stored quarantine ID '$qid' does not match ".
  9941. "requested ID '$quarantine_id'" }
  9942. if ($bsmtp)
  9943. { die "Releasing messages in BSMTP format not yet supported ".
  9944. "(dot de-stuffing not implemented)" }
  9945. $msginfo->sender($sender); $msginfo->sender_smtp($sender_smtp);
  9946. $msginfo->recips(\@recips_all);
  9947. $_->delivery_method($release_m) for @{$msginfo->per_recip_data};
  9948. # mark a file location past prefixed header fields where orig message starts
  9949. $msginfo->skip_bytes($offset_bytes);
  9950. my $msg_format = $request_type eq 'dsn' ? 'dsn'
  9951. : $request_type eq 'report' ? c('report_format')
  9952. : c('release_format');
  9953. my $hdr_edits = Amavis::Out::EditHeader->new;
  9954. $msginfo->header_edits($hdr_edits);
  9955. if ($msg_format eq 'resend') {
  9956. if (!defined($recips_data_override)) {
  9957. $msginfo->recips(\@recips_blocked); # override 'all' by 'blocked' recips
  9958. } else { # recipients specified in the request override stored info
  9959. ll(5) && do_log(5, 'overriding recips %s by %s',
  9960. join(',', qquote_rfc2821_local(@recips_blocked)),
  9961. join(',', map($_->recip_addr_smtp, @$recips_data_override)));
  9962. $msginfo->per_recip_data($recips_data_override);
  9963. }
  9964. $_->delivery_method($release_m) for @{$msginfo->per_recip_data};
  9965. } else {
  9966. # collect more information from a quarantined message, making it available
  9967. # to a report generator and to macros during template expansion
  9968. Amavis::get_body_digest($msginfo, $Amavis::Conf::mail_digest_algorithm);
  9969. Amavis::collect_some_info($msginfo);
  9970. if (defined($recips_data_override) && ll(5)) {
  9971. do_log(5, 'overriding recips %s by %s',
  9972. join(',', qquote_rfc2821_local(@recips_blocked)),
  9973. join(',', map($_->recip_addr_smtp, @$recips_data_override)));
  9974. }
  9975. my($notification,$suppressed) = delivery_status_notification(
  9976. $msginfo, 0, \%Amavis::builtins,
  9977. !defined($recips_data_override) ? \@recips_blocked
  9978. : [ map($_->recip_addr, @$recips_data_override) ],
  9979. $request_type, $feedback_type, undef);
  9980. # pushes original quarantined message into an attachment of a notification
  9981. $msginfo = $notification;
  9982. }
  9983. if (defined $sender_override) {
  9984. # sender specified in the request, overrides stored info
  9985. do_log(5, "overriding sender %s by %s", $sender, $sender_override);
  9986. $msginfo->sender($sender_override);
  9987. $msginfo->sender_smtp(qquote_rfc2821_local($sender_override));
  9988. }
  9989. if ($msg_format eq 'resend') { # keep quarantined message at a top MIME level
  9990. # Resent-* header fields must precede corresponding Received header field
  9991. # "Resent-From:" and "Resent-Date:" are required fields!
  9992. my $hdrfrom_recip = $msginfo->setting_by_contents_category(
  9993. cr('hdrfrom_notify_recip_by_ccat'));
  9994. $hdrfrom_recip = expand_variables($hdrfrom_recip);
  9995. if ($msginfo->requested_by eq '') {
  9996. $hdr_edits->add_header('Resent-From', $hdrfrom_recip);
  9997. } else {
  9998. $hdr_edits->add_header('Resent-From',
  9999. qquote_rfc2821_local($msginfo->requested_by));
  10000. $hdr_edits->add_header('Resent-Sender',
  10001. $hdrfrom_recip) if $hdrfrom_recip ne '';
  10002. }
  10003. my $prd = $msginfo->per_recip_data;
  10004. $hdr_edits->add_header('Resent-To',
  10005. $prd && @$prd==1 ? $prd->[0]->recip_addr_smtp
  10006. : 'undisclosed-recipients:;');
  10007. $hdr_edits->add_header('Resent-Date', # time of the release
  10008. rfc2822_timestamp($msginfo->rx_time));
  10009. $hdr_edits->add_header('Resent-Message-ID',
  10010. sprintf('<QRR%s@%s>', $msginfo->mail_id||'', c('myhostname')) );
  10011. }
  10012. $hdr_edits->add_header('Received', make_received_header_field($msginfo,1),1);
  10013. my $bcc = $msginfo->setting_by_contents_category(cr('always_bcc_by_ccat'));
  10014. if (defined $bcc && $bcc ne '' && $request_type ne 'report') {
  10015. my $recip_obj = Amavis::In::Message::PerRecip->new;
  10016. # leave recip_addr and recip_addr_smtp undefined!
  10017. $recip_obj->recip_addr_modified($bcc);
  10018. $recip_obj->recip_destiny(D_PASS);
  10019. $recip_obj->dsn_notify(['NEVER']);
  10020. $recip_obj->add_contents_category(CC_CLEAN,0);
  10021. $msginfo->per_recip_data([@{$msginfo->per_recip_data}, $recip_obj]);
  10022. do_log(2,"adding recipient - always_bcc: %s", $bcc);
  10023. }
  10024. $msginfo;
  10025. }
  10026. 1;
  10027. #
  10028. package Amavis::Custom;
  10029. # MAIL PROCESSING SEQUENCE:
  10030. # child process initialization
  10031. # loop for each mail:
  10032. # - receive mail, parse and make available some basic information
  10033. # * custom hook: new() - may inspect info, may load policy banks
  10034. # - mail checking and collecting results
  10035. # * custom hook: checks() - may inspect or modify checking results
  10036. # - deciding mail fate (lookup on *_lovers, thresholds, ...)
  10037. # - quarantining
  10038. # - sending notifications (to admin and recips)
  10039. # * custom hook: before_send() - may send other notif, quarantine, modify mail
  10040. # - forwarding (unless blocked)
  10041. # * custom hook: after_send() - may suppress DSN, send reports, quarantine
  10042. # - sending delivery status notification (if needed)
  10043. # - issue main log entry, manage statistics (timing, counters, nanny)
  10044. # * custom hook: mail_done() - may inspect results
  10045. # endloop after $max_requests or earlier
  10046. use strict;
  10047. use re 'taint';
  10048. sub new { my($class,$conn,$msginfo) = @_; undef }
  10049. sub checks { my($self,$conn,$msginfo) = @_; undef }
  10050. sub before_send { my($self,$conn,$msginfo) = @_; undef }
  10051. sub after_send { my($self,$conn,$msginfo) = @_; undef }
  10052. sub mail_done { my($self,$conn,$msginfo) = @_; undef }
  10053. 1;
  10054. #
  10055. package Amavis;
  10056. require 5.005; # need qr operator and \z in regexps
  10057. use strict;
  10058. use re 'taint';
  10059. BEGIN {
  10060. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  10061. $VERSION = '2.316';
  10062. import Amavis::Conf qw(:platform :sa :confvars c cr ca);
  10063. import Amavis::Util qw(untaint untaint_inplace
  10064. min max minmax unique_list unique_ref
  10065. ll do_log do_log_safe update_current_log_level
  10066. dump_captured_log log_capture_enabled
  10067. sanitize_str debug_oneshot am_id
  10068. safe_encode safe_encode_ascii safe_encode_utf8
  10069. safe_decode proto_decode
  10070. add_entropy stir_random generate_mail_id make_password
  10071. prolong_timer get_deadline waiting_for_client
  10072. switch_to_my_time switch_to_client_time
  10073. snmp_counters_init snmp_count dynamic_destination
  10074. ccat_split ccat_maj cmp_ccat cmp_ccat_maj
  10075. setting_by_given_contents_category_all
  10076. setting_by_given_contents_category orcpt_encode);
  10077. import Amavis::ProcControl qw(exit_status_str proc_status_ok
  10078. cloexec run_command collect_results);
  10079. import Amavis::Log qw(open_log close_log collect_log_stats);
  10080. import Amavis::Timing qw(section_time get_time_so_far);
  10081. import Amavis::rfc2821_2822_Tools;
  10082. import Amavis::Lookup qw(lookup lookup2);
  10083. import Amavis::Lookup::IP qw(lookup_ip_acl normalize_ip_addr);
  10084. import Amavis::Out;
  10085. import Amavis::Out::EditHeader;
  10086. import Amavis::UnmangleSender qw(parse_ip_address_from_received
  10087. first_received_from);
  10088. import Amavis::Unpackers::Validity qw(
  10089. check_header_validity check_for_banned_names);
  10090. import Amavis::Unpackers::MIME qw(mime_decode);
  10091. import Amavis::Expand qw(expand tokenize);
  10092. import Amavis::Notify qw(delivery_status_notification delivery_short_report
  10093. build_mime_entity defanged_mime_entity expand_variables);
  10094. import Amavis::In::Connection;
  10095. import Amavis::In::Message;
  10096. }
  10097. use Errno qw(ENOENT EACCES EAGAIN ESRCH EBADF);
  10098. use POSIX qw(locale_h);
  10099. use IO::Handle;
  10100. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  10101. use Time::HiRes ();
  10102. # body digest, either MD5 or SHA-1 (or perhaps SHA-256)
  10103. #use Digest::SHA;
  10104. use Digest::MD5;
  10105. use Net::Server 0.87; # need Net::Server::PreForkSimple::done
  10106. use MIME::Base64;
  10107. use vars qw(
  10108. $extra_code_zmq $extra_code_db
  10109. $extra_code_sql_base $extra_code_sql_log $extra_code_sql_quar
  10110. $extra_code_sql_lookup $extra_code_ldap
  10111. $extra_code_in_ampdp $extra_code_in_smtp $extra_code_in_courier
  10112. $extra_code_out_smtp $extra_code_out_pipe
  10113. $extra_code_out_bsmtp $extra_code_out_local $extra_code_p0f
  10114. $extra_code_antivirus $extra_code_antispam
  10115. $extra_code_antispam_extprog
  10116. $extra_code_antispam_spamc $extra_code_antispam_sa
  10117. $extra_code_unpackers $extra_code_dkim $extra_code_tools);
  10118. use vars qw(%modules_basic %got_signals);
  10119. use vars qw($user_id_sql $user_policy_id_sql $wb_listed_sql);
  10120. use vars qw($implicit_maps_inserted $maps_have_been_labeled);
  10121. use vars qw($db_env $snmp_db $zmq_obj @zmq_sockets);
  10122. use vars qw(%builtins); # macros in customizable notification messages
  10123. use vars qw($last_task_completed_at);
  10124. use vars qw($child_invocation_count $child_task_count);
  10125. use vars qw($child_init_hook_was_called);
  10126. # $child_invocation_count # counts child re-use from 1 to max_requests
  10127. # $child_task_count # counts check_mail_begin_task (and check_mail) calls;
  10128. # this often runs in sync with $child_invocation_count,
  10129. # but with SMTP or LMTP input there may be more than one
  10130. # message passed during a single SMTP session
  10131. use vars qw(@config_files); # configuration files provided by -c or defaulted
  10132. use vars qw($MSGINFO);
  10133. use vars qw($av_output @virusname @detecting_scanners
  10134. $banned_filename_any $banned_filename_all @bad_headers);
  10135. # Amavis::In::AMPDP, Amavis::In::SMTP and In::Courier objects
  10136. use vars qw($ampdp_in_obj $smtp_in_obj $courier_in_obj);
  10137. use vars qw($sql_dataset_conn_lookups); # Amavis::Out::SQL::Connection object
  10138. use vars qw($sql_dataset_conn_storage); # Amavis::Out::SQL::Connection object
  10139. use vars qw($sql_storage); # Amavis::Out::SQL::Log object
  10140. use vars qw($sql_lookups $sql_wblist); # Amavis::Lookup::SQL objects
  10141. use vars qw($ldap_connection); # Amavis::LDAP::Connection object
  10142. use vars qw($ldap_lookups); # Amavis::Lookup::LDAP object
  10143. use vars qw($warm_restart); # 1: warm (reload), 0: cold start (restart)
  10144. sub new {
  10145. my $class = shift;
  10146. # make Amavis a subclass of Net::Server::whatever
  10147. @ISA = !$daemonize && $max_servers==1 ? 'Net::Server' # facilitates debugging
  10148. : defined $min_servers ? 'Net::Server::PreFork'
  10149. : 'Net::Server::PreForkSimple';
  10150. # $class->SUPER::new(@_); # available since Net::Server 0.91
  10151. bless { server => $_[0] }, $class; # works with all versions
  10152. }
  10153. sub get_rusage() {
  10154. my $usage;
  10155. if (Unix::Getrusage->UNIVERSAL::can("getrusage")) {
  10156. $usage = Unix::Getrusage::getrusage();
  10157. # ru_minflt no. of page faults serviced without I/O activity
  10158. # ru_majflt no. of page faults that required I/O activity
  10159. # ru_nswap no. of times a process was swapped out
  10160. # ru_inblock no. of times a file system had to perform input
  10161. # ru_oublock no. of times a file system had to perform output
  10162. # ru_msgsnd no. of IPC messages sent
  10163. # ru_msgrcv no. of IPC messages received
  10164. # ru_nsignals no. of signals delivered
  10165. # ru_nvcsw no. of voluntary context switches
  10166. # ru_nivcsw no. of involuntary context switches
  10167. # ru_maxrss [kB] maximum resident set size utilized
  10168. # ru_ixrss [kBtics] integral of mem used by the shared text segment
  10169. # ru_idrss [kBtics] integral of unshared mem in the data segment
  10170. # ru_isrss [kBtics] integral of unshared mem in the stack segment
  10171. # ru_utime [s] time spent executing in user mode
  10172. # ru_stime [s] time spent in the system on behalf of the process
  10173. }
  10174. $usage;
  10175. }
  10176. # report process resource usage, data from a system service getrusage(2)
  10177. #
  10178. sub report_rusage() {
  10179. my $usage = get_rusage();
  10180. if ($usage) {
  10181. my(@order) = qw(minflt majflt nswap inblock oublock msgsnd msgrcv nsignals
  10182. nvcsw nivcsw maxrss ixrss idrss isrss utime stime);
  10183. my(@result) = map($_.'='.$usage->{'ru_'.$_}, @order); # known
  10184. delete $usage->{'ru_'.$_} for @order;
  10185. push(@result, map($_.'='.$usage->{$_}, keys %$usage)); # any other?
  10186. do_log(2,"RUSAGE: %s", join(', ',@result));
  10187. }
  10188. }
  10189. sub macro_rusage {
  10190. my($msginfo,$recip_index,$name,$arg) = @_;
  10191. my $usage = get_rusage();
  10192. !$usage || !defined($usage->{$arg}) ? '' : $usage->{$arg};
  10193. }
  10194. # implements macros: T, and SA lookalikes: TESTS, TESTSSCORES
  10195. #
  10196. sub macro_tests {
  10197. my($msginfo,$recip_index,$name,$sep) = @_;
  10198. my(@s); my $per_recip_data = $msginfo->per_recip_data;
  10199. if (defined $recip_index) { # return info on one particular recipient
  10200. my $r;
  10201. $r = $per_recip_data->[$recip_index] if $recip_index >= 0;
  10202. if (defined $r) {
  10203. my $spam_tests = $r->spam_tests;
  10204. @s = split(/,/, join(',',map($$_,@$spam_tests))) if defined $spam_tests;
  10205. }
  10206. } else {
  10207. my(%all_spam_tests);
  10208. for my $r (@$per_recip_data) {
  10209. my $spam_tests = $r->spam_tests;
  10210. if (defined $spam_tests) {
  10211. $all_spam_tests{$_} = 1 for split(/,/,join(',',map($$_,@$spam_tests)));
  10212. }
  10213. }
  10214. @s = sort keys %all_spam_tests;
  10215. }
  10216. if (@s > 50) { $#s = 50-1; push(@s,"...") } # sanity limit
  10217. @s = map { my($tn,$ts) = split(/=/,$_,2); $tn } @s if $name eq 'TESTS';
  10218. if ($name eq 'T' || !defined($sep)) { \@s } else { join($sep,@s) }
  10219. };
  10220. # implements macros: c, and SA lookalikes: SCORE(pad), STARS(*)
  10221. #
  10222. sub macro_score {
  10223. my($msginfo,$recip_index,$name,$arg) = @_;
  10224. my $per_recip_data = $msginfo->per_recip_data;
  10225. my($result, $sl_min, $sl_max, $w); $w = '';
  10226. if ($name eq 'SCORE' && defined($arg) && $arg=~/^(0+| +)\z/)
  10227. { $w = length($arg)+4; $w = $arg=~/^0/ ? "0$w" : "$w" } # SA style padding
  10228. my $fmt = "%$w.3f"; my $fmts = "%+$w.3f"; # padding, sign
  10229. if (defined $recip_index) { # return info on one particular recipient
  10230. my $r;
  10231. $r = $per_recip_data->[$recip_index] if $recip_index >= 0;
  10232. $sl_min = $sl_max = $r->spam_level if defined $r;
  10233. } else {
  10234. ($sl_min,$sl_max) = minmax(map($_->spam_level, @$per_recip_data));
  10235. }
  10236. if ($name eq 'STARS') {
  10237. my $slc = $arg ne '' ? $arg : c('sa_spam_level_char');
  10238. $result = $slc eq '' || !defined $sl_min ? '' : $slc x min(50,$sl_min);
  10239. } elsif (!defined $sl_min) {
  10240. $result = '-';
  10241. # } elsif ($name eq 'SCORE' || abs($sl_min-$sl_max) < 0.1) {
  10242. } elsif (abs($sl_min-$sl_max) < 0.1) {
  10243. # users expect a single value, or not worth reporting a small difference
  10244. $result = sprintf($fmt,$sl_min); $result =~ s/\.?0*\z//; # trim fraction
  10245. } else { # format SA score as min..max
  10246. $sl_min = sprintf($fmt,$sl_min); $sl_min =~ s/\.?0*\z//;
  10247. $sl_max = sprintf($fmt,$sl_max); $sl_max =~ s/\.?0*\z//;
  10248. $result = $sl_min . '..' . $sl_max;
  10249. }
  10250. $result;
  10251. };
  10252. # implements macro header_field, providing a named header field from a message
  10253. #
  10254. sub macro_header_field {
  10255. my($msginfo,$name,$header_field_name,$limit,$hf_index) = @_;
  10256. undef $hf_index if $hf_index !~ /^[+-]?\d+\z/; # defaults to last
  10257. local($_) = $msginfo->get_header_field_body($header_field_name,$hf_index);
  10258. if (defined $_) { # unfold, trim, protect CR, LF, \000 and \200
  10259. chomp; s/\n(?=[ \t])//gs; s/^[ \t]+//; s/[ \t\n]+\z//;
  10260. if ($header_field_name =~
  10261. /^(?:Message-ID|Resent-Message-ID|In-Reply-To|References)\z/i) {
  10262. $_ = join(' ',parse_message_id($_)) if $_ ne ''; # strip CFWS
  10263. }
  10264. s{([\r\n\000\200])}{sprintf("\\%03o",ord($1))}eg;
  10265. };
  10266. !defined($limit) || $limit =~ /^\s+\z/ ||
  10267. $limit < 6 || length($_) <= $limit ? $_ : substr($_,0,$limit-5) . '[...]';
  10268. };
  10269. sub dkim_test {
  10270. my($name,$which) = @_;
  10271. my $w = lc($which);
  10272. my $sigs_ref = $MSGINFO->dkim_signatures_valid;
  10273. $sigs_ref = [] if !$sigs_ref;
  10274. $w eq 'any' || $w eq '' ? (!@$sigs_ref ? undef : scalar(@$sigs_ref))
  10275. : $w eq 'author' ? $MSGINFO->dkim_author_sig
  10276. : $w eq 'sender' ? $MSGINFO->dkim_sender_sig
  10277. : $w eq 'thirdparty'? $MSGINFO->dkim_thirdparty_sig
  10278. : $w eq 'envsender' ? $MSGINFO->dkim_envsender_sig
  10279. : $w eq 'identity' ? join(',', map($_->identity, @$sigs_ref))
  10280. : $w eq 'selector' ? join(',', map($_->selector, @$sigs_ref))
  10281. : $w eq 'domain' ? join(',', map($_->domain, @$sigs_ref))
  10282. : $w eq 'sig_sd' ? join(',', unique_list(map($_->selector.':'.$_->domain,
  10283. @$sigs_ref)))
  10284. : $w eq 'newsig_sd' ? join(',', unique_list(map($_->selector.':'.$_->domain,
  10285. @{$MSGINFO->dkim_signatures_new||[]})))
  10286. : dkim_acceptable_signing_domain($MSGINFO,$which);
  10287. }
  10288. sub dkim_acceptable_signing_domain($@) {
  10289. my($msginfo,@acceptable_sdid) = @_;
  10290. my $matches = 0;
  10291. my $sigs_ref = $msginfo->dkim_signatures_valid;
  10292. if ($sigs_ref && @$sigs_ref) {
  10293. for my $sig (@$sigs_ref) {
  10294. my $sdid = lc($sig->domain);
  10295. for (@acceptable_sdid) {
  10296. my $ad = !defined $_ ? '' : lc($_);
  10297. local($1);
  10298. $ad = $1 if $ad =~ /\@([^\@]*)\z/; # compatibility with pre-2.6.5
  10299. if ($ad eq '') { # checking for author domain signature
  10300. $matches = 1 if $msginfo->dkim_author_sig;
  10301. } elsif ($ad =~ /^\.(.*)\z/s) { # domain itself or its subdomain
  10302. my $d = $1;
  10303. if ($sdid eq $d || $sdid =~ /\.\Q$d\E\z/s) { $matches = 1; last }
  10304. } else {
  10305. if ($sdid eq $ad) { $matches = 1; last }
  10306. }
  10307. }
  10308. last if $matches;
  10309. }
  10310. }
  10311. $matches;
  10312. };
  10313. # initialize the %builtins, which is an associative array of built-in macros
  10314. # to be used in notification message expansion and log templates
  10315. #
  10316. sub init_builtin_macros() {
  10317. # A key (macro name) used to be a single character, but can now be a longer
  10318. # string, typically a name containing letters, numbers and '_' or '-'.
  10319. # Upper case letters may (as a mnemonic) suggest the value is an array,
  10320. # lower case may suggest the value is a scalar string - but this is only
  10321. # a convention and not enforced. All-uppercase multicharacter names are
  10322. # intended as SpamAssassin-lookalike macros, although there is nothing
  10323. # special about them and can be called like other macros.
  10324. #
  10325. # A value may be a reference to a subroutine which will be called later at
  10326. # a time of macro expansion. This way we can provide a method for obtaining
  10327. # information which is not yet available at the time of initialization, such
  10328. # as AV scanner results, or provide a lazy evaluation for more expensive
  10329. # calculations. Subroutine will be called in scalar context, its first
  10330. # argument is a macro name (a string), remaining arguments (strings, if any)
  10331. # are arguments of a macro call as specified in the call. The subroutine may
  10332. # return a scalar string (or undef), or an array reference.
  10333. #
  10334. # for SpamAssassin-lookalike macros semantics see Mail::SpamAssassin::Conf
  10335. %builtins = (
  10336. '.' => undef,
  10337. p => sub {c('policy_bank_path')},
  10338. # mail reception timestamp (e.g. start of an SMTP transaction):
  10339. DATE => sub {rfc2822_timestamp($MSGINFO->rx_time)},
  10340. d => sub {rfc2822_timestamp($MSGINFO->rx_time)}, # RFC 5322 local time
  10341. U => sub {iso8601_utc_timestamp($MSGINFO->rx_time)}, # iso8601 UTC
  10342. u => sub {sprintf("%010d",int($MSGINFO->rx_time))},# s since Unix epoch,UTC
  10343. # equivalent, but with more descriptive macro names:
  10344. date_unix_utc => sub {sprintf("%010d",int($MSGINFO->rx_time))},
  10345. date_iso8601_utc => sub {iso8601_utc_timestamp($MSGINFO->rx_time)},
  10346. date_iso8601_local => sub {iso8601_timestamp($MSGINFO->rx_time)},
  10347. date_rfc2822_local => sub {rfc2822_timestamp($MSGINFO->rx_time)},
  10348. week_iso8601 => sub {iso8601_week($MSGINFO->rx_time)},
  10349. weekday => sub {iso8601_weekday($MSGINFO->rx_time)},
  10350. y => sub {sprintf("%.0f", 1000*get_time_so_far())}, # elapsed time in ms
  10351. h => sub {c('myhostname')}, # fqdn name of this host
  10352. HOSTNAME => sub {c('myhostname')},
  10353. l => sub {$MSGINFO->originating ? 1 : undef}, # our client (mynets/roaming)
  10354. s => sub {$MSGINFO->sender_smtp}, # orig. unmodified env. sender addr in <>
  10355. S => sub {$MSGINFO->sender_smtp}, # kept for compatibility, avoid!
  10356. o => sub { # best attempt at determining true sender (origin) of the virus,
  10357. sanitize_str($MSGINFO->sender_source) }, # normally same as %s
  10358. R => sub {$MSGINFO->recips}, # original message recipients list
  10359. D => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $y}, #succ. delivrd
  10360. O => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $n}, #failed recips
  10361. N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, #short dsn
  10362. actions_performed => sub {join(',',@{$MSGINFO->actions_performed||[]})},
  10363. Q => sub {$MSGINFO->queue_id}, # MTA queue ID of the message if known
  10364. m => sub {macro_header_field($MSGINFO,'header','Message-ID')},
  10365. r => sub {macro_header_field($MSGINFO,'header','Resent-Message-ID')},
  10366. j => sub {macro_header_field($MSGINFO,'header','Subject')},
  10367. log_domains => sub {
  10368. my %domains;
  10369. # $domains{'ORIG'} = 1 if $MSGINFO->originating;
  10370. for my $r (@{$MSGINFO->per_recip_data}) {
  10371. if (!$r->recip_is_local) {
  10372. $domains{'EXT'} = 1;
  10373. } else {
  10374. my($localpart,$domain) = split_address($r->recip_addr);
  10375. $domain =~ s/^\@//; $domains{lc($domain)} = 1;
  10376. }
  10377. }
  10378. join(',', sort {$a cmp $b} keys %domains);
  10379. },
  10380. rfc2822_sender => sub {my $s = $MSGINFO->rfc2822_sender;
  10381. !defined($s) ? undef : qquote_rfc2821_local($s) },
  10382. rfc2822_from => sub {my $f = $MSGINFO->rfc2822_from;
  10383. !defined($f) ? undef :
  10384. qquote_rfc2821_local(ref $f ? @$f : $f)},
  10385. rfc2822_resent_sender => sub {my $rs = $MSGINFO->rfc2822_resent_sender;
  10386. !defined($rs) ? undef :
  10387. qquote_rfc2821_local(grep(defined $_, @$rs))},
  10388. rfc2822_resent_from => sub {my $rf = $MSGINFO->rfc2822_resent_from;
  10389. !defined($rf) ? undef :
  10390. qquote_rfc2821_local(grep(defined $_, @$rf))},
  10391. header_field => sub {macro_header_field($MSGINFO,@_)},
  10392. HEADER => sub {macro_header_field($MSGINFO,@_)},
  10393. useragent => # argument: 'name' or 'body', or empty to return entire field
  10394. sub { my($macro_name,$which_part) = @_; my($head,$body);
  10395. $body = macro_header_field($MSGINFO,'header', $head='User-Agent');
  10396. $body = macro_header_field($MSGINFO,'header', $head='X-Mailer')
  10397. if !defined $body;
  10398. !defined($body) ? undef
  10399. : lc($which_part) eq 'name' ? $head
  10400. : lc($which_part) eq 'body' ? $body : "$head: $body";
  10401. },
  10402. ccat =>
  10403. sub { # somewhat expensive! #**
  10404. my($name,$attr,$which) = @_;
  10405. $attr = lc($attr); # name | major | minor | <empty>
  10406. # | is_blocking | is_nonblocking
  10407. # | is_blocked_by_nonmain
  10408. $which = lc($which); # main | blocking | auto
  10409. my $result = ''; my $blocking_ccat = $MSGINFO->blocking_ccat;
  10410. if ($attr eq 'is_blocking') {
  10411. $result = defined($blocking_ccat) ? 1 : '';
  10412. } elsif ($attr eq 'is_nonblocking') {
  10413. $result = !defined($blocking_ccat) ? 1 : '';
  10414. } elsif ($attr eq 'is_blocked_by_nonmain') {
  10415. if (defined($blocking_ccat)) {
  10416. my $aref = $MSGINFO->contents_category;
  10417. $result = 1 if ref($aref) && @$aref > 0
  10418. && $blocking_ccat ne $aref->[0];
  10419. }
  10420. } elsif ($attr eq 'name') {
  10421. $result =
  10422. $which eq 'main' ?
  10423. $MSGINFO->setting_by_main_contents_category(\%ccat_display_names)
  10424. : $which eq 'blocking' ?
  10425. $MSGINFO->setting_by_blocking_contents_category(
  10426. \%ccat_display_names)
  10427. : $MSGINFO->setting_by_contents_category( \%ccat_display_names);
  10428. } else { # attr = major, minor, or anything else returns a pair
  10429. my($maj,$min) = ccat_split(
  10430. ($which eq 'blocking' ||
  10431. $which ne 'main' && defined $blocking_ccat)
  10432. ? $blocking_ccat : $MSGINFO->contents_category);
  10433. $result = $attr eq 'major' ? $maj
  10434. : $attr eq 'minor' ? sprintf("%d",$min)
  10435. : sprintf("(%d,%d)",$maj,$min);
  10436. }
  10437. $result;
  10438. },
  10439. ccat_maj => # deprecated, use [:ccat|major]
  10440. sub { my $blocking_ccat = $MSGINFO->blocking_ccat;
  10441. (ccat_split(defined $blocking_ccat ? $blocking_ccat
  10442. : $MSGINFO->contents_category))[0];
  10443. },
  10444. ccat_min => # deprecated, use [:ccat|minor]
  10445. sub { my $blocking_ccat = $MSGINFO->blocking_ccat;
  10446. (ccat_split(defined $blocking_ccat ? $blocking_ccat
  10447. : $MSGINFO->contents_category))[1];
  10448. },
  10449. ccat_name => # deprecated, use [:ccat|name]
  10450. sub { $MSGINFO->setting_by_contents_category(\%ccat_display_names) },
  10451. dsn_notify => sub {
  10452. return 'NEVER' if $MSGINFO->sender eq '';
  10453. my(%merged);
  10454. for my $r (@{$MSGINFO->per_recip_data}) {
  10455. my $dn = $r->dsn_notify;
  10456. for ($dn ? @$dn : ('FAILURE')) { $merged{uc($_)} = 1 }
  10457. }
  10458. uc(join(',', sort keys %merged));
  10459. },
  10460. attachment_password => sub {
  10461. my $password = $MSGINFO->attachment_password; # already have it?
  10462. if (!defined $password) { # make one, and store it for later
  10463. $password = make_password(c('attachment_password'), $MSGINFO);
  10464. $MSGINFO->attachment_password($password);
  10465. }
  10466. $password;
  10467. },
  10468. b => sub {$MSGINFO->body_digest}, # original message body digest, hex enc
  10469. body_digest => sub { # original message body digest, raw bytes (binary!)
  10470. my $bd = $MSGINFO->body_digest; # hex digits, high nybble first
  10471. !defined $bd ? '' : pack('H*',$bd);
  10472. },
  10473. n => sub {$MSGINFO->log_id}, # amavis internal task id (in log and nanny)
  10474. i => sub {$MSGINFO->mail_id}, # long-term unique mail id on this system
  10475. mail_id => sub {$MSGINFO->mail_id}, # synonym for %i, base64url (RFC 4648)
  10476. secret_id => sub {$MSGINFO->secret_id}, # mail_id's counterpart, base64url
  10477. log_id => sub {$MSGINFO->log_id}, # synonym for %n
  10478. MAILID => sub {$MSGINFO->mail_id}, # synonym for %i (no equivalent in SA)
  10479. LOGID => sub {$MSGINFO->log_id}, # synonym for %n (no equivalent in SA)
  10480. P => sub {$MSGINFO->partition_tag}, # SQL partition tag
  10481. partition_tag => sub {$MSGINFO->partition_tag}, # synonym for %P
  10482. q => sub {my $q = $MSGINFO->quarantined_to;
  10483. $q && [map { my $m=$_; $m=~s{^\Q$QUARANTINEDIR\E/}{}; $m } @$q];
  10484. }, # list of quarantine mailboxes
  10485. v => sub {!defined $av_output ? undef # anti-virus scanner output
  10486. : [split(/[ \t]*\r?\n/, $av_output)]},
  10487. V => sub {my $vn = $MSGINFO->virusnames; # unique virus names
  10488. $vn && unique_ref($vn) },
  10489. F => sub { my $b;
  10490. # first banned part name with a comment from a rule regexp
  10491. for my $r (@{$MSGINFO->per_recip_data}) {
  10492. $b = $r->banning_reason_short;
  10493. last if defined $b;
  10494. }
  10495. $b },
  10496. banning_rule_key => sub {
  10497. # regexp of a matching banning rules yielding a true rhs result
  10498. unique_ref(map { my $v = $_->banning_rule_key;
  10499. !defined($v) ? () : @$v }
  10500. @{$MSGINFO->per_recip_data});
  10501. },
  10502. banning_rule_comment => sub {
  10503. # just a comment (or a whole regexp if it contains no comments)
  10504. # from matching banning regexp rules yielding a true rhs result
  10505. unique_ref(map { my $v = $_->banning_rule_comment;
  10506. !defined($v) ? () : @$v }
  10507. @{$MSGINFO->per_recip_data});
  10508. },
  10509. banning_rule_rhs => sub {
  10510. # right-hand-side of those matching banning rules yielding true
  10511. # (a r.h.s. of a rule can be a string, is treated as a boolean,
  10512. # but often it is just an implicit 0 or 1)
  10513. unique_ref(map { my $v = $_->banning_rule_rhs;
  10514. !defined($v) ? () : @$v }
  10515. @{$MSGINFO->per_recip_data});
  10516. },
  10517. banned_parts => sub { # list of banned parts with their full paths
  10518. my $b = unique_ref(map(@{$_->banned_parts},
  10519. grep(defined($_->banned_parts),@{$MSGINFO->per_recip_data})));
  10520. my $b_chopped = @$b > 2; @$b = (@$b[0,1],'...') if $b_chopped;
  10521. s/[ \t]{6,}/ ... /g for @$b;
  10522. $b },
  10523. banned_parts_as_attr => sub { # list of banned parts with their full paths
  10524. my $b = unique_ref(map(@{$_->banned_parts_as_attr},
  10525. grep(defined($_->banned_parts_as_attr),
  10526. @{$MSGINFO->per_recip_data})));
  10527. my $b_chopped = @$b > 2; @$b = (@$b[0,1],'...') if $b_chopped;
  10528. s/[ \t]{6,}/ ... /g for @$b;
  10529. $b },
  10530. X => sub {\@bad_headers},
  10531. W => sub {\@detecting_scanners}, # list of av scanners detecting a virus
  10532. H => sub {[map(split(/\n/,$_), @{$MSGINFO->orig_header})]}, # arry of lines
  10533. A => sub {[split(/\r?\n/, $MSGINFO->spam_summary)]}, # SA report text
  10534. SUMMARY => sub {$MSGINFO->spam_summary},
  10535. REPORT => sub {sanitize_str($MSGINFO->spam_report,1)}, #contains any octet
  10536. TESTSSCORES => sub {macro_tests($MSGINFO,undef,@_)}, # tests with scores
  10537. TESTS => sub {macro_tests($MSGINFO,undef,@_)}, # tests without scores
  10538. z => sub {$MSGINFO->msg_size}, #mail size as defined by RFC 1870, or approx
  10539. t => sub { # first entry in the Received trace
  10540. sanitize_str(first_received_from($MSGINFO)) },
  10541. e => sub { # first valid public IP in the Received trace - expensive! #**
  10542. sanitize_str(parse_ip_address_from_received($MSGINFO)) },
  10543. a => sub { $MSGINFO->client_addr }, # original SMTP session client IP addr
  10544. client_addr => sub { $MSGINFO->client_addr }, # synonym with 'a'
  10545. client_port => sub { $MSGINFO->client_port },
  10546. client_addr_port => sub { # original SMTP session client IP addr & port no.
  10547. my($a,$p) = ($MSGINFO->client_addr, $MSGINFO->client_port);
  10548. !defined $a || $a eq '' ? undef : ('[' . $a . ']' . ($p ? ":$p" : ''));
  10549. },
  10550. g => sub { # original SMTP session client DNS name
  10551. sanitize_str($MSGINFO->client_name) },
  10552. client_helo => sub { # original SMTP session EHLO/HELO name
  10553. sanitize_str($MSGINFO->client_helo) },
  10554. remote_mta => sub { unique_ref(map($_->recip_remote_mta,
  10555. @{$MSGINFO->per_recip_data})) },
  10556. smtp_response => sub { unique_ref(map($_->recip_smtp_response,
  10557. @{$MSGINFO->per_recip_data})) },
  10558. remote_mta_smtp_response =>
  10559. sub { unique_ref(map($_->recip_remote_mta_smtp_response,
  10560. @{$MSGINFO->per_recip_data})) },
  10561. REMOTEHOSTADDR => # where the request was sent from
  10562. sub { my $c = $MSGINFO->conn_obj; !$c ? '' : $c->client_ip },
  10563. REMOTEHOSTNAME =>
  10564. sub { my $c = $MSGINFO->conn_obj;
  10565. my $ip = !$c ? '' : $c->client_ip;
  10566. $ip ne '' ? "[$ip]" : 'localhost' },
  10567. # VERSION => Mail::SpamAssassin->Version, # SA version
  10568. # SUBVERSION => $Mail::SpamAssassin::SUB_VERSION, # SA sub-version/revision
  10569. AUTOLEARN => sub {$MSGINFO->supplementary_info('AUTOLEARN')},
  10570. ADDEDHEADERHAM => sub {$MSGINFO->supplementary_info('ADDEDHEADERHAM')},
  10571. ADDEDHEADERSPAM => sub {$MSGINFO->supplementary_info('ADDEDHEADERSPAM')},
  10572. supplementary_info => # additional information from SA and other scanners
  10573. sub { my($name,$key,$fmt)=@_;
  10574. my $info = $MSGINFO->supplementary_info($key);
  10575. $info eq '' ? '' : $fmt eq '' ? $info : sprintf($fmt,$info);
  10576. },
  10577. rusage => sub { macro_rusage($MSGINFO,undef,@_) }, # resource usage
  10578. REQD => sub { my $tag2_level;
  10579. for (@{$MSGINFO->per_recip_data}) { # get minimal tag2_level
  10580. my $tag2_l = lookup2(0, $_->recip_addr,
  10581. ca('spam_tag2_level_maps'));
  10582. $tag2_level = $tag2_l if defined($tag2_l) &&
  10583. (!defined($tag2_level) || $tag2_l < $tag2_level);
  10584. }
  10585. !defined($tag2_level) ? '-' : 0+sprintf("%.3f",$tag2_level);
  10586. },
  10587. '1'=> sub { # above tag level and not bypassed for any recipient?
  10588. grep($_->is_in_contents_category(CC_CLEAN,1),
  10589. @{$MSGINFO->per_recip_data}) ? 'Y' : '0' },
  10590. '2'=> sub { # above tag2 level and not bypassed for any recipient?
  10591. grep($_->is_in_contents_category(CC_SPAMMY),
  10592. @{$MSGINFO->per_recip_data}) ? 'Y' : '0' },
  10593. YESNO => sub { my($arg_spam, $arg_ham) = @_; # like %2, but gives: Yes/No
  10594. grep($_->is_in_contents_category(CC_SPAMMY),
  10595. @{$MSGINFO->per_recip_data})
  10596. ? (defined $arg_spam ? $arg_spam : 'Yes')
  10597. : (defined $arg_ham ? $arg_ham : 'No') },
  10598. YESNOCAPS =>
  10599. sub { my($arg_spam, $arg_ham) = @_; # like %2, but gives: YES/NO
  10600. grep($_->is_in_contents_category(CC_SPAMMY),
  10601. @{$MSGINFO->per_recip_data})
  10602. ? (defined $arg_spam ? $arg_spam : 'YES')
  10603. : (defined $arg_ham ? $arg_ham : 'NO') },
  10604. 'k'=> sub { # above kill level and not bypassed for any recipient?
  10605. grep($_->is_in_contents_category(CC_SPAM),
  10606. @{$MSGINFO->per_recip_data}) ? 'Y' : '0' },
  10607. score_boost => 0, # legacy
  10608. c => sub {macro_score($MSGINFO,undef,@_)}, # info on all recipients
  10609. SCORE => sub {macro_score($MSGINFO,undef,@_)}, # info on all recipients
  10610. STARS => sub {macro_score($MSGINFO,undef,@_)}, # info on all recipients
  10611. dkim => \&dkim_test,
  10612. tls_in => sub {$MSGINFO->tls_cipher}, # currently only shows ciphers in use
  10613. report_format => undef, # notification message format, supplied elsewhere
  10614. feedback_type => undef, # (ARF) feedback type or empty, supplied elsewhere
  10615. wrap => sub {my($name,$width,$prefix,$indent,$str) = @_;
  10616. wrap_string($str,$width,$prefix,$indent)},
  10617. lc => sub {my $name=shift; lc(join('',@_))}, # to lowercase
  10618. uc => sub {my $name=shift; uc(join('',@_))}, # to uppercase
  10619. substr => sub {my($name,$s,$ofs,$len) = @_;
  10620. defined $len ? substr($s,$ofs,$len) : substr($s,$ofs)},
  10621. index => sub {my($name,$s,$substr,$pos) = @_;
  10622. index($s, $substr, defined $pos ? $pos : 0)},
  10623. len => sub {my($name,$s) = @_; length($s)},
  10624. incr => sub {my($name,$v,@rest) = @_;
  10625. if (!@rest) { $v++ } else { $v += $_ for @rest }; "$v"},
  10626. decr => sub {my($name,$v,@rest) = @_;
  10627. if (!@rest) { $v-- } else { $v -= $_ for @rest }; "$v"},
  10628. min => sub {my($name,@args) = @_; min(map(/^\s*\z/?undef:$_, @args))},
  10629. max => sub {my($name,@args) = @_; max(map(/^\s*\z/?undef:$_, @args))},
  10630. sprintf=> sub {my($name,$fmt,@args) = @_; sprintf($fmt,@args)},
  10631. join => sub {my($name,$sep,@args) = @_; join($sep,@args)},
  10632. limit => sub {my($name,$lim,$s) = @_; $lim < 6 || length($s) <= $lim ? $s
  10633. : substr($s,0,$lim-5).'[...]' },
  10634. dquote => sub {my $nm=shift;
  10635. join('', map { my $s=$_; $s=~s{"}{""}g; '"'.$s.'"' } @_)},
  10636. uquote => sub {my $nm=shift;
  10637. join('', map { my $s=$_; $s=~s{[ \t]+}{_}g; $s } @_)},
  10638. hexenc => sub {my $nm=shift; join('', map(unpack('H*',$_), @_))},
  10639. b64encode => sub {my $nm=shift; join(' ', map(encode_base64($_,''),@_))},
  10640. b64enc => sub {my $nm=shift; # preferred over b64encode
  10641. join('', map { my $s=encode_base64($_,'');
  10642. $s=~s/=+\z//; $s } @_)},
  10643. b64urlenc => sub {my $nm=shift;
  10644. join('', map { my $s=encode_base64($_,'');
  10645. $s=~s/=+\z//; $s=~tr{+/}{-_}; $s } @_)},
  10646. mime2utf8 => sub { # convert to UTF-8 octets, truncate to $max_len if given
  10647. my($nm,$str,$max_len,$both_if_diff) = @_;
  10648. if (!defined $str || $str eq '') {
  10649. $str = '';
  10650. } else {
  10651. eval {
  10652. my $chars = safe_decode('MIME-Header',$str); # logical characters
  10653. my $octets = safe_encode_utf8($chars); # bytes, UTF-8 encoded
  10654. if (defined $max_len && $max_len > 0 && length($octets) > $max_len) {
  10655. local($1);
  10656. if ($octets =~ /^(.{0,$max_len})(?=[\x00-\x7F\xC0-\xFF]|\z)/s) {
  10657. $octets = $1; # cleanly chop a UTF-8 byte sequence, RFC 3629
  10658. }
  10659. }
  10660. if (!$both_if_diff) {
  10661. $str = $octets;
  10662. } else {
  10663. # only compare the visi