/amavisd-new-2.8.0/amavisd

# · Perl · 31092 lines · 26372 code · 1027 blank · 3693 comment · 5399 complexity · 4fc193aa494cbf85ec5fb01922acc8f3 MD5 · raw file

  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 visible part
  10664. if (defined $max_len && $max_len > 0 && length($str) > $max_len) {
  10665. $str = substr($str,0,$max_len);
  10666. }
  10667. $str = $octets . ' (raw: ' . $str . ')' if $octets ne $str;
  10668. }
  10669. 1;
  10670. } or do {
  10671. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  10672. do_log(5, "macro mime2utf8: malformed string, keeping raw bytes: %s",
  10673. $eval_stat);
  10674. if (defined $max_len && $max_len > 0 && length($str) > $max_len) {
  10675. $str = substr($str,0,$max_len);
  10676. }
  10677. };
  10678. }
  10679. $str;
  10680. },
  10681. # macros f, T, C, B will be defined for each notification as appropriate
  10682. # (representing From:, To:, Cc:, and Bcc: respectively)
  10683. # remaining free letters: wxEGIJKLMYZ
  10684. );
  10685. }
  10686. # initialize %local_delivery_aliases
  10687. #
  10688. sub init_local_delivery_aliases() {
  10689. # The %local_delivery_aliases maps local virtual 'localpart' to a mailbox
  10690. # (e.g. to a quarantine filename or a directory). Used by method 'local:',
  10691. # i.e. in mail_to_local_mailbox(), for direct local quarantining.
  10692. # The hash value may be a ref to a pair of fixed strings, or a subroutine ref
  10693. # (which must return a pair of strings (a list, not a list ref)) which makes
  10694. # possible lazy evaluation when some part of the pair is not known before
  10695. # the final delivery time. The first string in a pair must be either:
  10696. # - empty or undef, which will disable saving the message,
  10697. # - a filename, indicating a Unix-style mailbox,
  10698. # - a directory name, indicating a maildir-style mailbox,
  10699. # in which case the second string may provide a suggested file name.
  10700. #
  10701. %Amavis::Conf::local_delivery_aliases = (
  10702. 'virus-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10703. 'banned-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10704. 'unchecked-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10705. 'spam-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10706. 'bad-header-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10707. 'clean-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10708. 'other-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10709. 'archive-quarantine' => sub { ($QUARANTINEDIR, undef) },
  10710. # some more examples:
  10711. 'archive-files' => sub { ("$QUARANTINEDIR", undef) },
  10712. 'archive-mbox' => sub { ("$QUARANTINEDIR/archive.mbox", undef) },
  10713. 'recip-quarantine' => sub { ("$QUARANTINEDIR/recip-archive.mbox",undef) },
  10714. 'sender-quarantine' =>
  10715. sub { my $s = $MSGINFO->sender;
  10716. $s = substr($s,0,100)."..." if length($s) > 100+3;
  10717. $s =~ tr/a-zA-Z0-9@._+-/=/c; $s =~ s/\@/_at_/g;
  10718. untaint_inplace($s) if $s =~ /^(?:[a-zA-Z0-9%=._+-]+)\z/; # untaint
  10719. ($QUARANTINEDIR, "sender-$s-%m.gz"); # suggested file name
  10720. },
  10721. # 'recip-quarantine2' => sub {
  10722. # my(@fnames);
  10723. # my $myfield =
  10724. # Amavis::Lookup::SQLfield->new($sql_lookups,'some_field_name','S');
  10725. # for my $r (@{$MSGINFO->recips}) {
  10726. # my $field_value = lookup(0,$r,$myfield);
  10727. # my $fname = $field_value; # or perhaps: my $fname = $r;
  10728. # local($1); $fname =~ s/[^a-zA-Z0-9._\@]/=/g; $fname =~ s/\@/%/g;
  10729. # untaint_inplace($fname) if $fname =~ /^([a-zA-Z0-9._=%]+)\z/;
  10730. # $fname =~ s/%/%%/g; # protect %
  10731. # do_log(3, "Recipient: %s, field: %s, fname: %s",
  10732. # $r, $field_value, $fname);
  10733. # push(@fnames, $fname);
  10734. # }
  10735. # # ???what file name to choose if there is more than one recipient???
  10736. # ( $QUARANTINEDIR, "sender-$fnames[0]-%i-%n.gz" ); # suggested file name
  10737. # },
  10738. );
  10739. }
  10740. # tokenize templates (input to macro expansion), after dropping privileges
  10741. #
  10742. sub init_tokenize_templates() {
  10743. my(@templ_names) = qw(log_templ log_recip_templ
  10744. notify_sender_templ notify_virus_recips_templ
  10745. notify_virus_sender_templ notify_virus_admin_templ
  10746. notify_spam_sender_templ notify_spam_admin_templ
  10747. notify_release_templ notify_report_templ notify_autoresp_templ);
  10748. for my $bank_name (keys %policy_bank) {
  10749. for my $n (@templ_names) { # tokenize templates to speed up macro expansion
  10750. my $s = $policy_bank{$bank_name}{$n}; $s = $$s if ref($s) eq 'SCALAR';
  10751. $policy_bank{$bank_name}{$n} = tokenize(\$s) if defined $s;
  10752. }
  10753. }
  10754. }
  10755. # pre-parse IP lookup tables to speed up lookups, after dropping privileges
  10756. #
  10757. sub init_preparse_ip_lookups() {
  10758. for my $bank_name (keys %policy_bank) {
  10759. my $r = $policy_bank{$bank_name}{'inet_acl'};
  10760. if (ref($r) eq 'ARRAY') # should be a ref to a single IP lookup table
  10761. { $policy_bank{$bank_name}{'inet_acl'} = Amavis::Lookup::IP->new(@$r) }
  10762. $r = $policy_bank{$bank_name}{'client_ipaddr_policy'}; # listref of pairs
  10763. if (ref($r) eq 'ARRAY') { # should be an array, test just to make sure
  10764. my $odd = 1;
  10765. for my $table (@$r) { # replace plain lists with pre-parsed objects
  10766. $table = Amavis::Lookup::IP->new(@$table)
  10767. if $odd && ref($table) eq 'ARRAY';
  10768. $odd = !$odd;
  10769. }
  10770. }
  10771. }
  10772. }
  10773. # initialize some remaining global variables in a master process;
  10774. # invoked after chroot and after privileges have been dropped, before forking
  10775. #
  10776. sub after_chroot_init() {
  10777. $child_invocation_count = $child_task_count = 0;
  10778. %modules_basic = %INC; # helps to track missing modules in chroot
  10779. do_log(5,"after_chroot_init: EUID: %s (%s); EGID: %s (%s)", $>,$<, $),$( );
  10780. my(@msg);
  10781. my $euid = $>; # effective UID
  10782. $> = 0; # try to become root
  10783. POSIX::setuid(0) if $> != 0; # and try some more
  10784. if ($> == 0 || $euid == 0) { # succeeded? panic!
  10785. @msg = ("It is possible to change EUID from $euid to root, ABORTING!",
  10786. "Please use a recent version of Net::Server",
  10787. "or start as non-root, e.g. by su(1) or using option -u user");
  10788. } elsif ($daemon_chroot_dir eq '') {
  10789. # A quick check on vulnerability/protection of a config file
  10790. # (non-exhaustive: doesn't test for symlink tricks and higher directories).
  10791. # The config file has already been executed by now, so it may be
  10792. # too late to feel sorry now, but better late then never.
  10793. my(@actual_c_f) = Amavis::Conf::get_config_files_read();
  10794. do_log(2,"config files read: %s", join(", ",@actual_c_f));
  10795. for my $config_file (@actual_c_f) {
  10796. local($1); # IO::Handle::_open_mode_string can taint $1 if mode is '+<'
  10797. my $fh = IO::File->new;
  10798. my $errn = stat($config_file) ? 0 : 0+$!;
  10799. if ($errn) { # not accessible, don't bother to test further
  10800. } elsif ($fh->open($config_file,O_RDWR)) {
  10801. push(@msg, "Config file \"$config_file\" is writable, ".
  10802. "UID $<, EUID $>, EGID $)" );
  10803. $fh->close; # close, ignoring status
  10804. } elsif (rename($config_file, $config_file.'.moved')) {
  10805. my $m = 'appears writable (unconfirmed)';
  10806. my $errn_cf_orig = stat($config_file) ? 0 : 0+$!;
  10807. my $errn_cf_movd = stat($config_file.'.moved') ? 0 : 0+$!;
  10808. if ($errn_cf_orig==ENOENT && $errn_cf_movd!=ENOENT) {
  10809. # try to rename back, ignoring status
  10810. rename($config_file.'.moved', $config_file);
  10811. $m = 'is writable (confirmed)';
  10812. }
  10813. push(@msg, "Directory of a config file \"$config_file\" $m, ".
  10814. "UID $<, EUID $>, EGID $)" );
  10815. }
  10816. last if @msg;
  10817. }
  10818. }
  10819. if (@msg) {
  10820. do_log(-3,"FATAL: %s",$_) for @msg;
  10821. print STDERR (map("$_\n", @msg));
  10822. die "SECURITY PROBLEM, ABORTING";
  10823. exit 1; # just in case
  10824. }
  10825. init_tokenize_templates();
  10826. init_preparse_ip_lookups();
  10827. # report versions of some (more interesting) modules
  10828. for my $m ('Amavis::Conf',
  10829. sort map { my $s = $_; $s =~ s/\.pm\z//; $s =~ s{/}{::}g; $s }
  10830. grep(/\.pm\z/, keys %INC)) {
  10831. next if !grep($_ eq $m, qw(Amavis::Conf
  10832. Archive::Tar Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib
  10833. MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet
  10834. Digest::MD5 Digest::SHA Digest::SHA1 Crypt::OpenSSL::RSA
  10835. Authen::SASL Authen::SASL::XS Authen::SASL::Cyrus Authen::SASL::Perl
  10836. Encode Scalar::Util Time::HiRes File::Temp Unix::Syslog Unix::Getrusage
  10837. Socket Socket6 IO::Socket::INET6 IO::Socket::IP IO::Socket::SSL
  10838. Net::Server NetAddr::IP Net::DNS Net::SSLeay Net::Patricia Net::LDAP
  10839. Mail::ClamAV Mail::SpamAssassin Mail::DKIM::Verifier Mail::DKIM::Signer
  10840. Mail::SPF Mail::SPF::Query URI Razor2::Client::Version
  10841. DBI DBD::mysql DBD::Pg DBD::SQLite BerkeleyDB DB_File
  10842. ZMQ ZMQ::LibZMQ2 ZMQ::LibZMQ3 ZeroMQ SAVI Anomy::Sanitizer));
  10843. do_log(0, "Module %-19s %s", $m, eval{$m->VERSION} || '?');
  10844. }
  10845. do_log(0,"Amavis::ZMQ code %s loaded", $extra_code_zmq ?'':" NOT");
  10846. do_log(0,"Amavis::DB code %s loaded", $extra_code_db ?'':" NOT");
  10847. do_log(0,"SQL base code %s loaded", $extra_code_sql_base ?'':" NOT");
  10848. do_log(0,"SQL::Log code %s loaded", $extra_code_sql_log ?'':" NOT");
  10849. do_log(0,"SQL::Quarantine %s loaded", $extra_code_sql_quar ?'':" NOT");
  10850. do_log(0,"Lookup::SQL code %s loaded", $extra_code_sql_lookup ?'':" NOT");
  10851. do_log(0,"Lookup::LDAP code %s loaded", $extra_code_ldap ?'':" NOT");
  10852. do_log(0,"AM.PDP-in proto code%s loaded", $extra_code_in_ampdp ?'':" NOT");
  10853. do_log(0,"SMTP-in proto code %s loaded", $extra_code_in_smtp ?'':" NOT");
  10854. do_log(0,"Courier proto code %s loaded", $extra_code_in_courier ?'':" NOT");
  10855. do_log(0,"SMTP-out proto code %s loaded", $extra_code_out_smtp ?'':" NOT");
  10856. do_log(0,"Pipe-out proto code %s loaded", $extra_code_out_pipe ?'':" NOT");
  10857. do_log(0,"BSMTP-out proto code%s loaded", $extra_code_out_bsmtp ?'':" NOT");
  10858. do_log(0,"Local-out proto code%s loaded", $extra_code_out_local ?'':" NOT");
  10859. do_log(0,"OS_Fingerprint code %s loaded", $extra_code_p0f ?'':" NOT");
  10860. do_log(0,"ANTI-VIRUS code %s loaded", $extra_code_antivirus ?'':" NOT");
  10861. do_log(0,"ANTI-SPAM code %s loaded", $extra_code_antispam ?'':" NOT");
  10862. do_log(0,"ANTI-SPAM-EXT code %s loaded",
  10863. $extra_code_antispam_extprog ?'':" NOT");
  10864. do_log(0,"ANTI-SPAM-C code %s loaded",
  10865. $extra_code_antispam_spamc ?'':" NOT");
  10866. do_log(0,"ANTI-SPAM-SA code %s loaded", $extra_code_antispam_sa?'':" NOT");
  10867. do_log(0,"Unpackers code %s loaded", $extra_code_unpackers ?'':" NOT");
  10868. do_log(0,"DKIM code %s loaded", $extra_code_dkim ?'':" NOT");
  10869. do_log(0,"Tools code %s loaded", $extra_code_tools ?'':" NOT");
  10870. # store policy names into 'policy_bank_name' fields, if not explicitly set
  10871. for my $name (keys %policy_bank) {
  10872. if (ref($policy_bank{$name}) eq 'HASH' &&
  10873. !exists($policy_bank{$name}{'policy_bank_name'})) {
  10874. $policy_bank{$name}{'policy_bank_name'} = $name;
  10875. $policy_bank{$name}{'policy_bank_path'} = $name;
  10876. }
  10877. }
  10878. };
  10879. # overlay the current policy bank by settings from the
  10880. # $policy_bank{$policy_bank_name}, or load the default policy bank (empty name)
  10881. #
  10882. sub load_policy_bank($;$) {
  10883. my($policy_bank_name,$msginfo) = @_;
  10884. if (!exists $policy_bank{$policy_bank_name}) {
  10885. do_log(-1,'policy bank "%s" does not exist, ignored', $policy_bank_name);
  10886. } elsif ($policy_bank_name eq '') {
  10887. %current_policy_bank = %{$policy_bank{$policy_bank_name}}; # copy base
  10888. update_current_log_level();
  10889. do_log(4,'loaded base policy bank');
  10890. } else {
  10891. my $cpbp = c('policy_bank_path'); # currently loaded bank
  10892. my $new_bank_ref = $policy_bank{$policy_bank_name};
  10893. my $do_log5 = ll(5);
  10894. for my $k (keys %$new_bank_ref) {
  10895. if ($k eq 'ACTION') {
  10896. if (ref $new_bank_ref->{$k} eq 'CODE') {
  10897. do_log(5,'invoking user ACTION on loading a policy bank %s',
  10898. $policy_bank_name);
  10899. eval {
  10900. # $msginfo may be undef when a policy bank load takes place early
  10901. &{$new_bank_ref->{$k}}($msginfo,$policy_bank_name); 1;
  10902. } or do {
  10903. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  10904. do_log(-1,'failed ACTION on loading a policy bank %s: %s',
  10905. $policy_bank_name, $eval_stat);
  10906. };
  10907. }
  10908. } elsif (!exists $current_policy_bank{$k}) {
  10909. do_log(-1,'loading policy bank "%s": unknown field "%s"',
  10910. $policy_bank_name,$k);
  10911. } elsif (ref($new_bank_ref->{$k}) ne 'HASH' ||
  10912. ref($current_policy_bank{$k}) ne 'HASH') {
  10913. $current_policy_bank{$k} = $new_bank_ref->{$k};
  10914. # do_log(5,"loading policy bank %s, curr{%s} replaced by %s",
  10915. # $policy_bank_name, $k, $current_policy_bank{$k}) if $do_log5;
  10916. } else { # new hash to be merged into or replacing an existing hash
  10917. if ($new_bank_ref->{$k}{REPLACE}) { # replace the entire hash
  10918. $current_policy_bank{$k} = { %{$new_bank_ref->{$k}} }; # copy of new
  10919. do_log(5,"loading policy bank %s, curr{%s} hash replaced",
  10920. $policy_bank_name, $k) if $do_log5;
  10921. } else { # merge field-by-field, old fields missing in new are retained
  10922. $current_policy_bank{$k} = { %{$current_policy_bank{$k}} }; # copy
  10923. while (my($key,$val) = each %{$new_bank_ref->{$k}}) {
  10924. do_log(5,"loading policy bank %s, curr{%s}{%s} = %s, %s",
  10925. $policy_bank_name, $k, $key, $val,
  10926. !exists($current_policy_bank{$k}{$key}) ? 'new'
  10927. : 'replaces '.$current_policy_bank{$k}{$key}
  10928. ) if $do_log5;
  10929. $current_policy_bank{$k}{$key} = $val;
  10930. }
  10931. }
  10932. delete $current_policy_bank{$k}{REPLACE};
  10933. }
  10934. }
  10935. $current_policy_bank{'policy_bank_path'} =
  10936. ($cpbp eq '' ? '' : $cpbp.'/') . $policy_bank_name;
  10937. update_current_log_level();
  10938. do_log(2,'loaded policy bank "%s"%s', $policy_bank_name,
  10939. $cpbp eq '' ? '' : " over \"$cpbp\"");
  10940. }
  10941. }
  10942. ### Net::Server hook
  10943. ### Occurs in the parent (master) process after (possibly) opening a log file,
  10944. ### creating pid file, reopening STDIN/STDOUT to /dev/null and daemonizing;
  10945. ### but before binding to sockets
  10946. #
  10947. sub post_configure_hook {
  10948. # umask(0007); # affect protection of Unix sockets created by Net::Server
  10949. }
  10950. sub set_sockets_access() {
  10951. if (defined $unix_socket_mode && $unix_socket_mode ne '') {
  10952. for my $s (@listen_sockets) {
  10953. local($1);
  10954. if ($s =~ m{^(/.+)\|unix\z}si) {
  10955. my $path = $1;
  10956. chmod($unix_socket_mode,$path)
  10957. or do_log(-1, "Error setting mode 0%o on a socket %s: %s",
  10958. $unix_socket_mode, $path, $!);
  10959. }
  10960. }
  10961. }
  10962. }
  10963. ### Net::Server hook
  10964. ### Occurs in the parent (master) process after binding to sockets,
  10965. ### but before chrooting and dropping privileges
  10966. #
  10967. sub post_bind_hook {
  10968. umask(0027); # restore our preferred umask
  10969. set_sockets_access() if defined $warm_restart && !$warm_restart;
  10970. }
  10971. ### Net::Server hook
  10972. ### This hook occurs in the parent (master) process after chroot,
  10973. ### after change of user, and change of group has occurred.
  10974. ### It allows for preparation before forking and looping begins.
  10975. #
  10976. sub pre_loop_hook {
  10977. my($self) = @_;
  10978. local $SIG{CHLD} = 'DEFAULT';
  10979. # do_log(5, "entered pre_loop_hook");
  10980. eval {
  10981. after_chroot_init(); # the rest of the top-level initialization
  10982. # this needs to be done after chroot, otherwise paths will be wrong
  10983. find_external_programs([split(/:/,$path,-1)]); # path, decoders, scanners
  10984. # do some sanity checking
  10985. my $name = $TEMPBASE;
  10986. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  10987. my $errn = stat($TEMPBASE) ? 0 : 0+$!;
  10988. if ($errn==ENOENT) { die "No TEMPBASE directory: $name" }
  10989. elsif ($errn) { die "TEMPBASE directory inaccessible, $!: $name" }
  10990. elsif (!-d _) { die "TEMPBASE is not a directory: $name" }
  10991. elsif (!-w _) { die "TEMPBASE directory is not writable: $name" }
  10992. if ($enable_db && $extra_code_db) {
  10993. my $name = $db_home;
  10994. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  10995. $errn = stat($db_home) ? 0 : 0+$!;
  10996. if ($errn == ENOENT) {
  10997. die "Please create an empty directory $name to hold a database".
  10998. " (config variable \$db_home)\n" }
  10999. elsif ($errn) { die "db_home $name inaccessible: $!" }
  11000. elsif (!-d _) { die "db_home $name is not a directory" }
  11001. elsif (!-w _) { die "db_home $name directory is not writable" }
  11002. Amavis::DB::init(1, !$warm_restart);
  11003. }
  11004. if (!defined($sql_quarantine_chunksize_max)) {
  11005. die "Variable \$sql_quarantine_chunksize_max is undefined\n";
  11006. } elsif ($sql_quarantine_chunksize_max < 1024) {
  11007. die "Setting of \$sql_quarantine_chunksize_max is too small: ".
  11008. "$sql_quarantine_chunksize_max bytes, it would be inefficient\n";
  11009. } elsif ($sql_quarantine_chunksize_max > 1024*1024) {
  11010. do_log(-1, "Setting of %s is quite large: %d KiB, it unnecessarily ".
  11011. "wastes memory", '$sql_quarantine_chunksize_max',
  11012. $sql_quarantine_chunksize_max/1024);
  11013. }
  11014. if ($QUARANTINEDIR ne '') {
  11015. my $name = $QUARANTINEDIR;
  11016. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  11017. $errn = stat($QUARANTINEDIR) ? 0 : 0+$!;
  11018. if ($errn == ENOENT) { } # ok
  11019. elsif ($errn) { die "QUARANTINEDIR $name inaccessible: $!" }
  11020. elsif (-d _ && !-w _){ die "QUARANTINEDIR directory $name not writable"}
  11021. }
  11022. $spamcontrol_obj->init_pre_fork if $spamcontrol_obj;
  11023. my(@modules_extra) = grep(!exists $modules_basic{$_}, keys %INC);
  11024. if (@modules_extra) {
  11025. do_log(1, "extra modules loaded after daemonizing/chrooting: %s",
  11026. join(", ", sort @modules_extra));
  11027. %modules_basic = %INC;
  11028. }
  11029. if (!grep { my $v = $policy_bank{$_}{'enable_dkim_verification'};
  11030. defined(!ref $v ? $v : $$v) } keys %policy_bank)
  11031. { do_log(0,'DKIM signature verification disabled, corresponding features '.
  11032. 'not available. If not intentional, consider enabling it by setting: '.
  11033. '$enable_dkim_verification to 1, or explicitly disable it by setting '.
  11034. 'it to 0 to mute this warning.');
  11035. }
  11036. 1;
  11037. } or do {
  11038. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  11039. my $msg = "TROUBLE in pre_loop_hook: $eval_stat";
  11040. do_log(-2,"%s",$msg);
  11041. die("Suicide (" . am_id() . ") " . $msg . "\n");
  11042. };
  11043. 1;
  11044. }
  11045. # (!)_DIE: Unable to create sub named "" at /usr/local/sbin/amavisd line 9947.
  11046. # The line 9947 was in sub write_to_log_hook: local $SIG{CHLD} = 'DEFAULT';
  11047. # perl #60360: local $SIG{FOO} = sub {...}; sets signal handler to SIG_DFL
  11048. # # http://www.perlmonks.org/?node_id=721692
  11049. # # non-atomic, clears to SIG_DFL, then sets: local $SIG{ALRM} = sub {...};
  11050. # use Sub::ScopeFinalizer qw( scope_finalizer );
  11051. # my $sentry = local_sassign $SIG{ALRM}, \&alarm_handler;
  11052. # sub local_sassign {
  11053. # my $r = \($_[0]);
  11054. # my $sentry = scope_finalizer { $$r = $_[0] } { args => [ $$r ] };
  11055. # $$r = $_[1]; return $sentry;
  11056. # }
  11057. # or use:
  11058. # use POSIX qw(:signal_h) ;
  11059. # my $sigset = POSIX::SigSet->new ;
  11060. # my $blockset = POSIX::SigSet->new( SIGALRM ) ;
  11061. # sigprocmask(SIG_BLOCK, $blockset, $sigset );
  11062. # local $SIG{ALRM} = sub .... ;
  11063. # sigprocmask(SIG_SETMASK, $sigset );
  11064. ### log routine Net::Server hook
  11065. ### (Sys::Syslog MUST NOT be specified as a value of 'log_file'!)
  11066. #
  11067. # Redirect Net::Server logging to use Amavis' do_log().
  11068. # The main reason is that Net::Server uses Sys::Syslog
  11069. # (and has two bugs in doing it, at least the Net-Server-0.82),
  11070. # and Amavis users are accustomed to Unix::Syslog.
  11071. #
  11072. sub write_to_log_hook {
  11073. my($self,$level,$msg) = @_;
  11074. my $prop = $self->{server};
  11075. local $SIG{CHLD} = 'DEFAULT';
  11076. $level = 0 if $level < 0; $level = 4 if $level > 4;
  11077. # my $ll = (-2,-1,0,1,3)[$level]; # 0=err, 1=warn, 2=notice, 3=info, 4=debug
  11078. my $ll = (-1, 0,1,3,4)[$level]; # 0=err, 1=warn, 2=notice, 3=info, 4=debug
  11079. chomp($msg); # just call Amavis' traditional logging
  11080. ll($ll) && do_log($ll, "Net::Server: %s", $msg);
  11081. 1;
  11082. }
  11083. ### user customizable Net::Server hook (Net::Server 0.88 or later),
  11084. ### hook occurs in the master process !!!
  11085. #
  11086. sub run_n_children_hook {
  11087. # do_log(5, "entered run_n_children_hook");
  11088. Amavis::AV::sophos_savi_reload()
  11089. if $extra_code_antivirus && Amavis::AV::sophos_savi_stale();
  11090. add_entropy(Time::HiRes::gettimeofday);
  11091. }
  11092. ### compatibility with patched Net::Server by SAVI patch (Net::Server <= 0.87)
  11093. #
  11094. sub parent_fork_hook { my $self = $_[0]; $self->run_n_children_hook }
  11095. ### user customizable Net::Server hook,
  11096. ### run by every child process during its startup
  11097. #
  11098. sub child_init_hook {
  11099. my($self) = @_;
  11100. local $SIG{CHLD} = 'DEFAULT';
  11101. $child_init_hook_was_called = 1;
  11102. do_log(5, "entered child_init_hook");
  11103. $my_pid = $$; $0 = c('myprogram_name') . ' (virgin child)';
  11104. stir_random();
  11105. log_capture_enabled(1) if $enable_log_capture;
  11106. # reset log counters inherited from a master process
  11107. collect_log_stats();
  11108. # my(@signames) = qw(HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV
  11109. # SYS PIPE ALRM TERM URG TSTP CONT TTIN TTOU IO
  11110. # XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2);
  11111. # my $h = sub { my $s = $_[0]; $got_signals{$s}++;
  11112. # local($SIG{$s})='IGNORE'; kill($my_pid,$s) };
  11113. # @SIG{@signames} = ($h) x @signames;
  11114. my $inherited_entropy;
  11115. eval {
  11116. # if ($> == 0 || $< == 0) { # last resort, in case Net::Server didn't do it
  11117. # do_log(2, "child_init_hook: dropping privileges, user=%s, group=%s",
  11118. # $daemon_user,$daemon_group);
  11119. # drop_priv($daemon_user,$daemon_group);
  11120. # }
  11121. undef $db_env; undef $snmp_db; # just in case
  11122. Amavis::Timing::init(); snmp_counters_init();
  11123. close_log(); open_log(); # reopen syslog or log file to get per-process fd
  11124. if ($enable_zmq && $extra_code_zmq && @zmq_sockets) {
  11125. do_log(5, "child_init_hook: zmq socket: %s", join(', ',@zmq_sockets));
  11126. $zmq_obj = Amavis::ZMQ->new(@zmq_sockets);
  11127. if ($zmq_obj) {
  11128. sleep 1; # a crude way to avoid a "slow joiner" syndrome #***
  11129. $zmq_obj->register_proc(0,1,'');
  11130. }
  11131. }
  11132. if ($extra_code_db) {
  11133. # Berkeley DB handles should not be shared across process forks,
  11134. # each forked child should acquire its own Berkeley DB handles
  11135. $db_env = Amavis::DB->new; # get access to a bdb environment
  11136. $snmp_db = Amavis::DB::SNMP->new($db_env);
  11137. $snmp_db->register_proc(0,1,'') if $snmp_db; # alive and idle
  11138. my $var_ref = $snmp_db->read_snmp_variables('entropy');
  11139. $inherited_entropy = $var_ref->[0] if $var_ref && @$var_ref;
  11140. }
  11141. # if ($extra_code_db) { # is it worth reporting the timing? (probably not)
  11142. # section_time('bdb-open');
  11143. # do_log(2, "%s", Amavis::Timing::report()); # report elapsed times
  11144. # }
  11145. # Prepare permanent SQL dataset connection objects, does not connect yet!
  11146. # $sql_dataset_conn_lookups and $sql_dataset_conn_storage may be the
  11147. # same dataset (one connection used), or they may be separate objects,
  11148. # which will make separate connections to (same or distinct) datasets,
  11149. # possibly using different SQL engine types or servers
  11150. if ($extra_code_sql_lookup && @lookup_sql_dsn) {
  11151. $sql_dataset_conn_lookups =
  11152. Amavis::Out::SQL::Connection->new(@lookup_sql_dsn);
  11153. }
  11154. if ($extra_code_sql_log && @storage_sql_dsn) {
  11155. if (!$sql_dataset_conn_lookups || @storage_sql_dsn != @lookup_sql_dsn
  11156. || grep($storage_sql_dsn[$_] ne $lookup_sql_dsn[$_],
  11157. (0..$#storage_sql_dsn)) )
  11158. { # DSN differs or no SQL lookups, storage needs its own connection
  11159. $sql_dataset_conn_storage =
  11160. Amavis::Out::SQL::Connection->new(@storage_sql_dsn);
  11161. if ($sql_dataset_conn_lookups) {
  11162. do_log(2,"storage and lookups will use separate connections to SQL");
  11163. } else {
  11164. do_log(5,"only storage connections to SQL, no lookups");
  11165. }
  11166. } else { # same dataset, use the same database connection object
  11167. $sql_dataset_conn_storage = $sql_dataset_conn_lookups;
  11168. do_log(2,"storage and lookups will use the same connection to SQL");
  11169. }
  11170. }
  11171. # create storage/lookup objs to hold DBI handles and 'prepared' statements
  11172. $sql_storage = Amavis::Out::SQL::Log->new($sql_dataset_conn_storage)
  11173. if $sql_dataset_conn_storage;
  11174. $sql_lookups = Amavis::Lookup::SQL->new($sql_dataset_conn_lookups,
  11175. 'sel_policy') if $sql_dataset_conn_lookups;
  11176. $sql_wblist = Amavis::Lookup::SQL->new($sql_dataset_conn_lookups,
  11177. 'sel_wblist') if $sql_dataset_conn_lookups;
  11178. $spamcontrol_obj->init_child if $spamcontrol_obj;
  11179. 1;
  11180. } or do {
  11181. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  11182. do_log(-2, "TROUBLE in child_init_hook: %s", $eval_stat);
  11183. die "Suicide in child_init_hook: $eval_stat\n";
  11184. };
  11185. add_entropy($inherited_entropy, Time::HiRes::gettimeofday, rand());
  11186. Amavis::Timing::go_idle('vir');
  11187. }
  11188. ### user customizable Net::Server hook
  11189. #
  11190. sub post_accept_hook {
  11191. my($self) = @_;
  11192. local $SIG{CHLD} = 'DEFAULT';
  11193. # do_log(5, "entered post_accept_hook");
  11194. if (!$child_init_hook_was_called) {
  11195. # this can happen with base Net::Server (not PreFork nor PreForkSiple)
  11196. do_log(5, "post_accept_hook: invoking child_init_hook which was skipped");
  11197. $self->child_init_hook;
  11198. }
  11199. $child_invocation_count++;
  11200. $0 = sprintf("%s (ch%d-accept)",
  11201. c('myprogram_name'), $child_invocation_count);
  11202. Amavis::Util::am_id(undef);
  11203. Amavis::Timing::go_busy('hi ');
  11204. # establish initial time right after 'accept'
  11205. Amavis::Timing::init(); snmp_counters_init();
  11206. $zmq_obj->register_proc(1,1,'A') if $zmq_obj; # enter 'accept' state
  11207. $snmp_db->register_proc(1,1,'A') if $snmp_db;
  11208. load_policy_bank(''); # start with a builtin baseline policy bank
  11209. }
  11210. ### user customizable Net::Server hook, load a by-interface policy bank;
  11211. ### if this hook returns 1 the request is processed
  11212. ### if this hook returns 0 the request is denied
  11213. #
  11214. sub allow_deny_hook {
  11215. my($self) = @_;
  11216. local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server !
  11217. local $SIG{CHLD} = 'DEFAULT';
  11218. # do_log(5, "entered allow_deny_hook");
  11219. my($prop, $sock, $is_ux, @bank_names);
  11220. $prop = $self->{server}; $sock = $prop->{client};
  11221. $is_ux = $sock && $sock->UNIVERSAL::can('NS_proto') &&
  11222. $sock->NS_proto eq 'UNIX';
  11223. if ($is_ux) {
  11224. push(@bank_names, $interface_policy{"SOCK"});
  11225. my $path = Net::Server->VERSION >= 2 ? $sock->NS_port
  11226. : $sock->NS_unix_path;
  11227. push(@bank_names, $interface_policy{$path}) if defined $path;
  11228. } else {
  11229. my($myaddr,$myport) = ($prop->{sockaddr}, $prop->{sockport});
  11230. $myaddr = '[' . lc($myaddr) . ']' if $myaddr =~ /:/; # IPv6?
  11231. push(@bank_names, $interface_policy{$myport});
  11232. push(@bank_names, $interface_policy{"$myaddr:$myport"});
  11233. }
  11234. for my $bank_name (@bank_names) {
  11235. load_policy_bank($bank_name) if defined $bank_name &&
  11236. $bank_name ne c('policy_bank_name');
  11237. }
  11238. # note that the new policy bank may have replaced the inet_acl access table
  11239. if ($is_ux) {
  11240. # always permit access - unix sockets are immune to this check
  11241. } else {
  11242. my($permit,$fullkey,$err) = lookup_ip_acl($prop->{peeraddr},
  11243. Amavis::Lookup::Label->new('inet_acl'), ca('inet_acl'));
  11244. if (defined($err) && $err ne '') {
  11245. do_log(-1, "DENIED ACCESS due to INVALID PEER IP ADDRESS %s: %s",
  11246. $prop->{peeraddr}, $err);
  11247. return 0;
  11248. } elsif (!$permit) {
  11249. do_log(-1, "DENIED ACCESS from IP %s, policy bank '%s'%s",
  11250. $prop->{peeraddr}, c('policy_bank_path'),
  11251. !defined $fullkey ? '' : ", blocked by rule $fullkey");
  11252. return 0;
  11253. }
  11254. }
  11255. 1;
  11256. }
  11257. ### The heart of the program
  11258. ### user customizable Net::Server hook
  11259. #
  11260. sub process_request {
  11261. my $self = shift;
  11262. local $SIG{CHLD} = 'DEFAULT';
  11263. # do_log(5, "entered process_request");
  11264. local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server !
  11265. my $prop = $self->{server}; my $sock = $prop->{client};
  11266. ll(3) && do_log(3, "process_request: fileno sock=%s, STDIN=%s, STDOUT=%s",
  11267. fileno($sock), fileno(STDIN), fileno(STDOUT));
  11268. # Net::Server 0.91 dups a socket to STDIN and STDOUT, which we do not want;
  11269. # it also forgets to close STDIN & STDOUT afterwards, so session remains
  11270. # open (smtp QUIT does not work), fixed in 0.92;
  11271. # Net::Server 0.92 introduced option no_client_stdout, but it
  11272. # breaks Net::Server::get_client_info by setting it, so we can't use it;
  11273. # On NetBSD closing fh STDIN (on fd0) somehow leaves fd0 still assigned to
  11274. # a socket (Net::Server 0.91) and cannot be closed even by a POSIX::close
  11275. # Let's just leave STDIN and STDOUT as they are, which works for versions
  11276. # of Net::Server 0.90 and older, is wasteful with 0.91 and 0.92, and is
  11277. # fine with 0.93.
  11278. if (ref($sock) !~ /^(?:IO::Socket::SSL|Net::Server::Proto::SSL)\z/) {
  11279. # binmode not implemented in IO::Socket::SSL and returns false
  11280. binmode($sock) or die "Can't set socket $sock to binmode: $!";
  11281. }
  11282. local $SIG{ALRM} = sub { die "timed out\n" }; # do not modify the sig text!
  11283. my $eval_stat;
  11284. eval {
  11285. # if ($] < 5.006) # Perl older than 5.6.0 did not set FD_CLOEXEC on sockets
  11286. # { cloexec($_,1,$_) for @{$prop->{sock}} }
  11287. switch_to_my_time('new request'); # timer init
  11288. if ($extra_code_ldap && !$ldap_lookups) {
  11289. # make LDAP lookup object
  11290. $ldap_connection = Amavis::LDAP::Connection->new($default_ldap);
  11291. $ldap_lookups = Amavis::Lookup::LDAP->new($default_ldap,$ldap_connection)
  11292. if $ldap_connection;
  11293. }
  11294. if ($ldap_lookups &&
  11295. $lookup_maps_imply_sql_and_ldap && !$implicit_maps_inserted) {
  11296. # make LDAP field lookup objects with incorporated field names
  11297. # fieldtype: B=boolean, N=numeric, S=string, L=list
  11298. # B-, N-, S-, L- returns undef if field does not exist
  11299. # B0: boolean, nonexistent field treated as false,
  11300. # B1: boolean, nonexistent field treated as true
  11301. my $lf = sub{Amavis::Lookup::LDAPattr->new($ldap_lookups,@_)};
  11302. unshift(@Amavis::Conf::local_domains_maps, $lf->('amavisLocal', 'B1'));
  11303. unshift(@Amavis::Conf::virus_lovers_maps, $lf->('amavisVirusLover', 'B-'));
  11304. unshift(@Amavis::Conf::spam_lovers_maps, $lf->('amavisSpamLover', 'B-'));
  11305. unshift(@Amavis::Conf::unchecked_lovers_maps, $lf->('amavisUncheckedLover', 'B-'));
  11306. unshift(@Amavis::Conf::banned_files_lovers_maps, $lf->('amavisBannedFilesLover', 'B-'));
  11307. unshift(@Amavis::Conf::bad_header_lovers_maps, $lf->('amavisBadHeaderLover', 'B-'));
  11308. unshift(@Amavis::Conf::bypass_virus_checks_maps, $lf->('amavisBypassVirusChecks', 'B-'));
  11309. unshift(@Amavis::Conf::bypass_spam_checks_maps, $lf->('amavisBypassSpamChecks', 'B-'));
  11310. unshift(@Amavis::Conf::bypass_banned_checks_maps,$lf->('amavisBypassBannedChecks', 'B-'));
  11311. unshift(@Amavis::Conf::bypass_header_checks_maps,$lf->('amavisBypassHeaderChecks', 'B-'));
  11312. unshift(@Amavis::Conf::spam_tag_level_maps, $lf->('amavisSpamTagLevel', 'N-'));
  11313. unshift(@Amavis::Conf::spam_tag2_level_maps, $lf->('amavisSpamTag2Level', 'N-'));
  11314. unshift(@Amavis::Conf::spam_tag3_level_maps, $lf->('amavisSpamTag3Level', 'N-'));
  11315. unshift(@Amavis::Conf::spam_kill_level_maps, $lf->('amavisSpamKillLevel', 'N-'));
  11316. unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$lf->('amavisSpamDsnCutoffLevel','N-'));
  11317. unshift(@Amavis::Conf::spam_quarantine_cutoff_level_maps,$lf->('amavisSpamQuarantineCutoffLevel','N-'));
  11318. unshift(@Amavis::Conf::spam_subject_tag_maps, $lf->('amavisSpamSubjectTag', 'S-'));
  11319. unshift(@Amavis::Conf::spam_subject_tag2_maps, $lf->('amavisSpamSubjectTag2', 'S-'));
  11320. unshift(@Amavis::Conf::spam_subject_tag3_maps, $lf->('amavisSpamSubjectTag3', 'S-'));
  11321. unshift(@Amavis::Conf::virus_quarantine_to_maps, $lf->('amavisVirusQuarantineTo', 'S-'));
  11322. unshift(@Amavis::Conf::spam_quarantine_to_maps, $lf->('amavisSpamQuarantineTo', 'S-'));
  11323. unshift(@Amavis::Conf::banned_quarantine_to_maps, $lf->('amavisBannedQuarantineTo','S-'));
  11324. unshift(@Amavis::Conf::unchecked_quarantine_to_maps, $lf->('amavisUncheckedQuarantineTo','S-'));
  11325. unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $lf->('amavisBadHeaderQuarantineTo', 'S-'));
  11326. unshift(@Amavis::Conf::clean_quarantine_to_maps, $lf->('amavisCleanQuarantineTo', 'S-'));
  11327. unshift(@Amavis::Conf::archive_quarantine_to_maps, $lf->('amavisArchiveQuarantineTo', 'S-'));
  11328. unshift(@Amavis::Conf::message_size_limit_maps, $lf->('amavisMessageSizeLimit', 'N-'));
  11329. unshift(@Amavis::Conf::addr_extension_virus_maps, $lf->('amavisAddrExtensionVirus', 'S-'));
  11330. unshift(@Amavis::Conf::addr_extension_spam_maps, $lf->('amavisAddrExtensionSpam', 'S-'));
  11331. unshift(@Amavis::Conf::addr_extension_banned_maps, $lf->('amavisAddrExtensionBanned','S-'));
  11332. unshift(@Amavis::Conf::addr_extension_bad_header_maps, $lf->('amavisAddrExtensionBadHeader','S-'));
  11333. unshift(@Amavis::Conf::warnvirusrecip_maps, $lf->('amavisWarnVirusRecip', 'B-'));
  11334. unshift(@Amavis::Conf::warnbannedrecip_maps, $lf->('amavisWarnBannedRecip', 'B-'));
  11335. unshift(@Amavis::Conf::warnbadhrecip_maps, $lf->('amavisWarnBadHeaderRecip', 'B-'));
  11336. unshift(@Amavis::Conf::newvirus_admin_maps, $lf->('amavisNewVirusAdmin', 'S-'));
  11337. unshift(@Amavis::Conf::virus_admin_maps, $lf->('amavisVirusAdmin', 'S-'));
  11338. unshift(@Amavis::Conf::spam_admin_maps, $lf->('amavisSpamAdmin', 'S-'));
  11339. unshift(@Amavis::Conf::banned_admin_maps, $lf->('amavisBannedAdmin', 'S-'));
  11340. unshift(@Amavis::Conf::bad_header_admin_maps, $lf->('amavisBadHeaderAdmin', 'S-'));
  11341. unshift(@Amavis::Conf::banned_filename_maps, $lf->('amavisBannedRuleNames', 'S-'));
  11342. unshift(@Amavis::Conf::disclaimer_options_bysender_maps,
  11343. $lf->('amavisDisclaimerOptions', 'S-'));
  11344. unshift(@Amavis::Conf::forward_method_maps, $lf->('amavisForwardMethod', 'S-'));
  11345. unshift(@Amavis::Conf::sa_userconf_maps, $lf->('amavisSaUserConf', 'S-'));
  11346. unshift(@Amavis::Conf::sa_username_maps, $lf->('amavisSaUserName', 'S-'));
  11347. section_time('ldap-prepare');
  11348. }
  11349. if ($sql_lookups &&
  11350. $lookup_maps_imply_sql_and_ldap && !$implicit_maps_inserted) {
  11351. # make SQL field lookup objects with incorporated field names
  11352. # fieldtype: B=boolean, N=numeric, S=string,
  11353. # B-, N-, S- returns undef if field does not exist
  11354. # B0: boolean, nonexistent field treated as false,
  11355. # B1: boolean, nonexistent field treated as true
  11356. my $nf = sub{Amavis::Lookup::SQLfield->new($sql_lookups,@_)}; # shorthand
  11357. $user_id_sql = $nf->('id', 'S-');
  11358. $user_policy_id_sql = $nf->('policy_id', 'S-');
  11359. unshift(@Amavis::Conf::local_domains_maps, $nf->('local', 'B1'));
  11360. unshift(@Amavis::Conf::virus_lovers_maps, $nf->('virus_lover', 'B-'));
  11361. unshift(@Amavis::Conf::spam_lovers_maps, $nf->('spam_lover', 'B-'));
  11362. unshift(@Amavis::Conf::unchecked_lovers_maps, $nf->('unchecked_lover', 'B-'));
  11363. unshift(@Amavis::Conf::banned_files_lovers_maps, $nf->('banned_files_lover', 'B-'));
  11364. unshift(@Amavis::Conf::bad_header_lovers_maps, $nf->('bad_header_lover', 'B-'));
  11365. unshift(@Amavis::Conf::bypass_virus_checks_maps, $nf->('bypass_virus_checks', 'B-'));
  11366. unshift(@Amavis::Conf::bypass_spam_checks_maps, $nf->('bypass_spam_checks', 'B-'));
  11367. unshift(@Amavis::Conf::bypass_banned_checks_maps, $nf->('bypass_banned_checks', 'B-'));
  11368. unshift(@Amavis::Conf::bypass_header_checks_maps, $nf->('bypass_header_checks', 'B-'));
  11369. unshift(@Amavis::Conf::spam_tag_level_maps, $nf->('spam_tag_level', 'N-'));
  11370. unshift(@Amavis::Conf::spam_tag2_level_maps, $nf->('spam_tag2_level', 'N-'));
  11371. unshift(@Amavis::Conf::spam_tag3_level_maps, $nf->('spam_tag3_level', 'N-'));
  11372. unshift(@Amavis::Conf::spam_kill_level_maps, $nf->('spam_kill_level', 'N-'));
  11373. unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$nf->('spam_dsn_cutoff_level','N-'));
  11374. unshift(@Amavis::Conf::spam_quarantine_cutoff_level_maps,$nf->('spam_quarantine_cutoff_level','N-'));
  11375. unshift(@Amavis::Conf::spam_subject_tag_maps, $nf->('spam_subject_tag', 'S-'));
  11376. unshift(@Amavis::Conf::spam_subject_tag2_maps, $nf->('spam_subject_tag2', 'S-'));
  11377. unshift(@Amavis::Conf::spam_subject_tag3_maps, $nf->('spam_subject_tag3', 'S-'));
  11378. unshift(@Amavis::Conf::virus_quarantine_to_maps, $nf->('virus_quarantine_to', 'S-'));
  11379. unshift(@Amavis::Conf::spam_quarantine_to_maps, $nf->('spam_quarantine_to', 'S-'));
  11380. unshift(@Amavis::Conf::banned_quarantine_to_maps, $nf->('banned_quarantine_to', 'S-'));
  11381. unshift(@Amavis::Conf::unchecked_quarantine_to_maps, $nf->('unchecked_quarantine_to', 'S-'));
  11382. unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $nf->('bad_header_quarantine_to','S-'));
  11383. unshift(@Amavis::Conf::clean_quarantine_to_maps, $nf->('clean_quarantine_to', 'S-'));
  11384. unshift(@Amavis::Conf::archive_quarantine_to_maps,$nf->('archive_quarantine_to','S-'));
  11385. unshift(@Amavis::Conf::message_size_limit_maps, $nf->('message_size_limit', 'N-'));
  11386. unshift(@Amavis::Conf::addr_extension_virus_maps, $nf->('addr_extension_virus', 'S-'));
  11387. unshift(@Amavis::Conf::addr_extension_spam_maps, $nf->('addr_extension_spam', 'S-'));
  11388. unshift(@Amavis::Conf::addr_extension_banned_maps,$nf->('addr_extension_banned','S-'));
  11389. unshift(@Amavis::Conf::addr_extension_bad_header_maps,$nf->('addr_extension_bad_header','S-'));
  11390. unshift(@Amavis::Conf::warnvirusrecip_maps, $nf->('warnvirusrecip', 'B-'));
  11391. unshift(@Amavis::Conf::warnbannedrecip_maps, $nf->('warnbannedrecip', 'B-'));
  11392. unshift(@Amavis::Conf::warnbadhrecip_maps, $nf->('warnbadhrecip', 'B-'));
  11393. unshift(@Amavis::Conf::newvirus_admin_maps, $nf->('newvirus_admin', 'S-'));
  11394. unshift(@Amavis::Conf::virus_admin_maps, $nf->('virus_admin', 'S-'));
  11395. unshift(@Amavis::Conf::spam_admin_maps, $nf->('spam_admin', 'S-'));
  11396. unshift(@Amavis::Conf::banned_admin_maps, $nf->('banned_admin', 'S-'));
  11397. unshift(@Amavis::Conf::bad_header_admin_maps, $nf->('bad_header_admin', 'S-'));
  11398. unshift(@Amavis::Conf::banned_filename_maps, $nf->('banned_rulenames', 'S-'));
  11399. unshift(@Amavis::Conf::disclaimer_options_bysender_maps,
  11400. $nf->('disclaimer_options', 'S-'));
  11401. unshift(@Amavis::Conf::forward_method_maps, $nf->('forward_method', 'S-'));
  11402. unshift(@Amavis::Conf::sa_userconf_maps, $nf->('sa_userconf', 'S-'));
  11403. unshift(@Amavis::Conf::sa_username_maps, $nf->('sa_username', 'S-'));
  11404. section_time('sql-prepare');
  11405. }
  11406. $implicit_maps_inserted = 1;
  11407. if (!$maps_have_been_labeled)
  11408. { Amavis::Conf::label_default_maps(); $maps_have_been_labeled = 1 }
  11409. my $ns_proto = $sock->NS_proto; # Net::Server::Proto submodules
  11410. my $conn = Amavis::In::Connection->new; # keeps info about connection
  11411. $conn->socket_proto($ns_proto);
  11412. my $suggested_protocol = c('protocol'); # suggested by the policy bank
  11413. $suggested_protocol = '' if !defined $suggested_protocol;
  11414. do_log(5,"process_request: suggested_protocol=\"%s\" on a %s socket",
  11415. $suggested_protocol, $ns_proto);
  11416. $zmq_obj->register_proc(2,0,'b') if $zmq_obj; # begin protocol
  11417. # $snmp_db->register_proc(2,0,'b') if $snmp_db;
  11418. if ($ns_proto eq 'UNIX') {
  11419. my $path = Net::Server->VERSION >= 2 ? $sock->NS_port
  11420. : $sock->NS_unix_path;
  11421. $conn->socket_path($path);
  11422. # how to test: $ socat stdio unix-connect:/var/amavis/amavisd.sock,crnl
  11423. } else { # TCP, UDP, UNIXDGRAM, SSLEAY, SSL (Net::Server::Proto modules)
  11424. my $sock_addr = $prop->{sockaddr};
  11425. my $peer_addr = $prop->{peeraddr};
  11426. if ($sock_addr eq $peer_addr) { # common, small optimization
  11427. $peer_addr = $sock_addr = normalize_ip_addr($sock_addr);
  11428. } else {
  11429. $sock_addr = normalize_ip_addr($sock_addr);
  11430. $peer_addr = normalize_ip_addr($peer_addr);
  11431. }
  11432. $conn->socket_port($prop->{sockport});
  11433. $conn->socket_ip($sock_addr);
  11434. $conn->client_ip($peer_addr);
  11435. }
  11436. if ($suggested_protocol eq 'SMTP' || $suggested_protocol eq 'LMTP' ||
  11437. ($suggested_protocol eq '' && $ns_proto =~ /^(?:TCP|SSLEAY|SSL)\z/)) {
  11438. if (!$extra_code_in_smtp) {
  11439. die "incoming TCP connection, but dynamic SMTP/LMTP code not loaded";
  11440. }
  11441. $smtp_in_obj = Amavis::In::SMTP->new if !$smtp_in_obj;
  11442. $smtp_in_obj->process_smtp_request(
  11443. $sock, ($suggested_protocol eq 'LMTP'?1:0), $conn, \&check_mail);
  11444. } elsif ($suggested_protocol eq 'AM.PDP') {
  11445. # amavis policy delegation protocol (e.g. new milter or amavisd-release)
  11446. $ampdp_in_obj = Amavis::In::AMPDP->new if !$ampdp_in_obj;
  11447. $ampdp_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
  11448. } elsif ($suggested_protocol eq 'COURIER') {
  11449. die "unavailable support for protocol: $suggested_protocol";
  11450. } elsif ($suggested_protocol eq 'QMQPqq') {
  11451. die "unavailable support for protocol: $suggested_protocol";
  11452. } elsif ($suggested_protocol eq 'TCP-LOOKUP') { #postfix maps, experimental
  11453. process_tcp_lookup_request($sock, $conn);
  11454. do_log(2, "%s", Amavis::Timing::report()); # report elapsed times
  11455. # } elsif ($suggested_protocol eq 'AM.CL') {
  11456. # # defaults to old amavis helper program protocol
  11457. # $ampdp_in_obj = Amavis::In::AMPDP->new if !$ampdp_in_obj;
  11458. # $ampdp_in_obj->process_policy_request($sock, $conn, \&check_mail, 1);
  11459. } elsif ($suggested_protocol eq '') {
  11460. die "protocol not specified, $ns_proto";
  11461. } else {
  11462. die "unsupported protocol: $suggested_protocol, $ns_proto";
  11463. }
  11464. Amavis::Out::SMTP::Session::rundown_stale_sessions(0)
  11465. if $extra_code_out_smtp;
  11466. 1;
  11467. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  11468. alarm(0); # stop the timer
  11469. if (defined $eval_stat) {
  11470. chomp $eval_stat; my $timed_out = $eval_stat =~ /^timed out\b/;
  11471. if ($timed_out) {
  11472. my $msg = "Requesting process rundown, task exceeded allowed time";
  11473. $msg .= " during waiting for input from client" if waiting_for_client();
  11474. do_log(-1, $msg);
  11475. } else {
  11476. do_log(-2, "TROUBLE in process_request: %s", $eval_stat);
  11477. $smtp_in_obj->preserve_evidence(1) if $smtp_in_obj;
  11478. do_log(-1, "Requesting process rundown after fatal error");
  11479. }
  11480. undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
  11481. $self->done(1);
  11482. } elsif ($max_requests > 0 && $child_task_count >= $max_requests) {
  11483. # in case of multiple-transaction protocols (e.g. SMTP, LMTP)
  11484. # we do not like to keep running indefinitely at the mercy of MTA
  11485. my $have_sawampersand= Devel::SawAmpersand->UNIVERSAL::can("sawampersand");
  11486. do_log(2, "Requesting process rundown after %d tasks (and %s sessions)%s",
  11487. $child_task_count, $child_invocation_count,
  11488. !$have_sawampersand ? '' : Devel::SawAmpersand::sawampersand() ?
  11489. ", SawAmpersand is TRUE!" : ", SawAmpersand is false");
  11490. undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
  11491. $self->done(1);
  11492. } elsif ($extra_code_antivirus && Amavis::AV::sophos_savi_stale() ) {
  11493. do_log(0, "Requesting process rundown due to stale Sophos virus data");
  11494. undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
  11495. $self->done(1);
  11496. }
  11497. my(@modules_extra) = grep(!exists $modules_basic{$_}, keys %INC);
  11498. # do_log(2, "modules loaded: %s", join(", ", sort keys %modules_basic));
  11499. if (@modules_extra) {
  11500. do_log(1, "extra modules loaded: %s", join(", ", sort @modules_extra));
  11501. %modules_basic = %INC;
  11502. }
  11503. ll(5) && do_log(5, 'exiting process_request');
  11504. }
  11505. sub child_goes_idle($) {
  11506. my($where) = @_;
  11507. do_log(5, 'child_goes_idle (%s)', $where);
  11508. my(@disconnected_what);
  11509. # $extra_code_out_smtp && eval {
  11510. # Amavis::Out::SMTP::Session::rundown_stale_sessions(1) &&
  11511. # push(@disconnected_what,'SMTP');
  11512. # };
  11513. $sql_dataset_conn_storage && eval {
  11514. $sql_dataset_conn_storage->disconnect_from_sql &&
  11515. push(@disconnected_what,'SQL-storage');
  11516. };
  11517. $sql_dataset_conn_lookups && eval {
  11518. # $sql_dataset_conn_lookups possibly the same as $sql_dataset_conn_storage,
  11519. # attempting to disconnect twice does no harm
  11520. $sql_dataset_conn_lookups->disconnect_from_sql &&
  11521. push(@disconnected_what,'SQL-lookup');
  11522. };
  11523. $ldap_connection && eval {
  11524. $ldap_connection->disconnect_from_ldap &&
  11525. push(@disconnected_what,'LDAP');
  11526. };
  11527. do_log(5, 'child_goes_idle: disconnected %s (%s)',
  11528. !@disconnected_what ? 'none' : join(', ',@disconnected_what),
  11529. $where);
  11530. }
  11531. ### After processing of a request, but before client connection has been closed
  11532. ### user customizable Net::Server hook
  11533. #
  11534. sub post_process_request_hook {
  11535. my($self) = @_;
  11536. my $prop = $self->{server}; my $sock = $prop->{client};
  11537. local $SIG{CHLD} = 'DEFAULT';
  11538. # do_log(5, "entered post_process_request_hook");
  11539. alarm(0); # stop the timer
  11540. child_goes_idle('post_process_request') if !$database_sessions_persistent;
  11541. debug_oneshot(0);
  11542. $0 = sprintf("%s (ch%d-avail)",
  11543. c('myprogram_name'), $child_invocation_count);
  11544. $zmq_obj->register_proc(1,0,'') if $zmq_obj; # alive and idle again
  11545. $snmp_db->register_proc(1,0,'') if $snmp_db;
  11546. Amavis::Timing::go_idle('bye');
  11547. if (ll(3)) {
  11548. my $load_report = Amavis::Timing::report_load();
  11549. do_log(3,$load_report) if defined $load_report;
  11550. }
  11551. dump_captured_log(1, c('enable_log_capture_dump'));
  11552. # workaround: Net::Server 0.91 forgets to disconnect session
  11553. if (Net::Server->VERSION == 0.91) { close STDIN; close STDOUT }
  11554. }
  11555. ### Child is about to be terminated
  11556. ### user customizable Net::Server hook
  11557. #
  11558. sub child_finish_hook {
  11559. my($self) = @_;
  11560. local $SIG{CHLD} = 'DEFAULT';
  11561. # do_log_safe(5, "entered child_finish_hook");
  11562. # for my $m (sort map { s/\.pm\z//; s[/][::]g; $_ } grep(/\.pm\z/, keys %INC)){
  11563. # do_log(0, "Module %-19s %s", $m, $m->VERSION || '?')
  11564. # if grep($m=~/^$_/, qw(Mail::ClamAV Mail::SpamAssassin Razor2 Net::DNS));
  11565. # }
  11566. child_goes_idle('child finishing');
  11567. $spamcontrol_obj->rundown_child if $spamcontrol_obj;
  11568. report_rusage();
  11569. $0 = sprintf("%s (ch%d-finish)",
  11570. c('myprogram_name'), $child_invocation_count);
  11571. do_log_safe(5,"child_finish_hook: invoking DESTROY methods");
  11572. undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
  11573. undef $sql_storage; undef $sql_wblist; undef $sql_lookups;
  11574. undef $sql_dataset_conn_lookups; undef $sql_dataset_conn_storage;
  11575. undef $ldap_lookups; undef $ldap_connection;
  11576. # unregister our process
  11577. if ($zmq_obj) {
  11578. eval { $zmq_obj->register_proc(0,0,undef); 1; }
  11579. or do_log_safe(-1, "child_finish_hook: ZMQ unregistering failed: %s",$@);
  11580. }
  11581. if ($snmp_db) {
  11582. eval { $snmp_db->register_proc(0,0,undef); 1; }
  11583. or do_log_safe(-1, "child_finish_hook: DB unregistering failed: %s",$@);
  11584. }
  11585. undef $snmp_db; undef $db_env; undef $zmq_obj;
  11586. log_capture_enabled(0);
  11587. }
  11588. sub END { # runs before exiting the module
  11589. local($@,$!);
  11590. # do_log_safe(5,"at the END handler: invoking DESTROY methods");
  11591. undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
  11592. undef $sql_storage; undef $sql_wblist; undef $sql_lookups;
  11593. undef $sql_dataset_conn_lookups; undef $sql_dataset_conn_storage;
  11594. undef $ldap_lookups; undef $ldap_connection;
  11595. # unregister our process
  11596. if ($zmq_obj) {
  11597. eval { $zmq_obj->register_proc(0,0,undef); 1; }
  11598. or do_log_safe(-1, "Amavis::END: ZMQ unregistering failed: %s", $@);
  11599. }
  11600. if ($snmp_db) {
  11601. eval { $snmp_db->register_proc(0,0,undef); 1; }
  11602. or do_log_safe(-1, "Amavis::END: DB unregistering failed: %s", $@);
  11603. }
  11604. undef $snmp_db; undef $db_env; undef $zmq_obj;
  11605. log_capture_enabled(0);
  11606. }
  11607. # implements Postfix TCP lookup server, see tcp_table(5) man page; experimental
  11608. #
  11609. sub process_tcp_lookup_request($$) {
  11610. my($sock, $conn) = @_;
  11611. local($/) = "\012"; # set line terminator to LF (regardless of platform)
  11612. my $req_cnt; my $ln;
  11613. for ($! = 0; defined($ln=$sock->getline); $! = 0) {
  11614. $req_cnt++; my $level = 0; local($1);
  11615. my($resp_code, $resp_msg) = (400, 'INTERNAL ERROR');
  11616. if ($ln =~ /^get (.*?)\015?\012\z/si) {
  11617. my $key = proto_decode($1);
  11618. my $sl = lookup2(0,$key, ca('spam_lovers_maps'));
  11619. $resp_code = 200; $level = 2;
  11620. $resp_msg = $sl ? "OK Recipient <$key> IS spam lover"
  11621. : "DUNNO Recipient <$key> is NOT spam lover";
  11622. } elsif ($ln =~ /^put ([^ ]*) (.*?)\015?\012\z/si) {
  11623. $resp_code = 500; $resp_msg = 'request not implemented: ' . $ln;
  11624. } else {
  11625. $resp_code = 500; $resp_msg = 'illegal request: ' . $ln;
  11626. }
  11627. do_log($level, "tcp_lookup(%s): %s %s", $req_cnt,$resp_code,$resp_msg);
  11628. $sock->printf("%03d %s\012", $resp_code, tcp_lookup_encode($resp_msg))
  11629. or die "Can't write to tcp_lookup socket: $!";
  11630. }
  11631. defined $ln || $! == 0 or die "Error reading from socket: $!";
  11632. do_log(0, "tcp_lookup: RUNDOWN after %d requests", $req_cnt);
  11633. }
  11634. sub tcp_lookup_encode($) {
  11635. my($str) = @_; local($1);
  11636. $str =~ s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/egs;
  11637. $str;
  11638. }
  11639. sub check_mail_begin_task() {
  11640. # The check_mail_begin_task (and check_mail) may be called several times
  11641. # per child lifetime and/or per-SMTP session. The variable $child_task_count
  11642. # is mainly used by AV-scanner interfaces, e.g. to initialize when invoked
  11643. # for the first time during child process lifetime
  11644. $child_task_count++;
  11645. do_log(4, "check_mail_begin_task: task_count=%d", $child_task_count);
  11646. # comment out to retain SQL/LDAP cache entries for the whole child lifetime:
  11647. $sql_wblist->clear_cache if $sql_wblist;
  11648. $sql_lookups->clear_cache if $sql_lookups;
  11649. $ldap_lookups->clear_cache if $ldap_lookups;
  11650. # reset certain global variables for each task
  11651. undef $av_output; @detecting_scanners = ();
  11652. @virusname = (); @bad_headers = ();
  11653. $banned_filename_any = $banned_filename_all = 0;
  11654. undef $MSGINFO; # just in case
  11655. }
  11656. # create a mail_id unique to a database and save preliminary info to SQL;
  11657. # if SQL is not enabled, just call a plain generate_mail_id() once
  11658. #
  11659. sub generate_unique_mail_id($) {
  11660. my($msginfo) = @_;
  11661. my($mail_id,$secret_id);
  11662. for (my $attempt = 5; ;) { # sanity limit on retries
  11663. ($mail_id,$secret_id) = generate_mail_id();
  11664. $msginfo->secret_id($secret_id);
  11665. $secret_id = 'X' x length($secret_id); # can't hurt to be conservative
  11666. $msginfo->mail_id($mail_id); # assign a long-term unique id to the msg
  11667. if (!$sql_storage) {
  11668. last; # no need to store and no way to check for uniqueness
  11669. } else {
  11670. # attempt to save a message placeholder to SQL, ensuring it is unique
  11671. $sql_storage->save_info_preliminary($msginfo) and last;
  11672. if (--$attempt <= 0) {
  11673. do_log(-2,"ERROR sql_storage: too many retries ".
  11674. "on storing preliminary, info not saved");
  11675. last;
  11676. } else {
  11677. snmp_count('GenMailIdRetries');
  11678. do_log(2,"sql_storage: retrying preliminary, %d attempts remain",
  11679. $attempt);
  11680. sleep(int(1+rand(3)));
  11681. add_entropy(Time::HiRes::gettimeofday, $attempt);
  11682. }
  11683. }
  11684. }
  11685. $mail_id;
  11686. }
  11687. # Collects some information derived from the envelope and the message,
  11688. # do some common lookups, storing the information into a $msginfo object
  11689. # to make commonly used information quickly and readily available to the
  11690. # rest of the program, e.g. avoiding a need for repeated lookups or parsing
  11691. # of the same attribute
  11692. #
  11693. sub collect_some_info($) {
  11694. my($msginfo) = @_;
  11695. my $partition_tag = c('partition_tag');
  11696. $partition_tag = &$partition_tag($msginfo) if ref $partition_tag eq 'CODE';
  11697. $partition_tag = 0 if !defined $partition_tag;
  11698. $msginfo->partition_tag($partition_tag);
  11699. my $sender = $msginfo->sender;
  11700. $msginfo->sender_source($sender);
  11701. # obtain RFC 5322 From and Sender from the mail header section, parsed/clean
  11702. my $rfc2822_sender = $msginfo->get_header_field_body('sender');
  11703. my $rfc2822_from_field = $msginfo->get_header_field_body('from');
  11704. my(@rfc2822_from); # RFC 5322 (ex RFC 2822) allows multiple author's addr
  11705. if (defined $rfc2822_sender) {
  11706. my(@sender_parsed) = map(unquote_rfc2821_local($_),
  11707. parse_address_list($rfc2822_sender));
  11708. $rfc2822_sender = !@sender_parsed ? '' : $sender_parsed[0]; # none or one
  11709. $msginfo->rfc2822_sender($rfc2822_sender);
  11710. }
  11711. if (defined $rfc2822_from_field) {
  11712. @rfc2822_from = map(unquote_rfc2821_local($_),
  11713. parse_address_list($rfc2822_from_field));
  11714. # rfc2822_from is a ref to a list when there are multiple author addresses!
  11715. $msginfo->rfc2822_from(@rfc2822_from < 1 ? undef :
  11716. @rfc2822_from < 2 ? $rfc2822_from[0]
  11717. : \@rfc2822_from);
  11718. }
  11719. my $rfc2822_to = $msginfo->get_header_field_body('to');
  11720. if (defined $rfc2822_to) {
  11721. my(@to_parsed) = map(unquote_rfc2821_local($_),
  11722. parse_address_list($rfc2822_to));
  11723. $msginfo->rfc2822_to(@to_parsed<2 ? $to_parsed[0] : \@to_parsed);
  11724. }
  11725. my $rfc2822_cc = $msginfo->get_header_field_body('cc');
  11726. if (defined $rfc2822_cc) {
  11727. my(@cc_parsed) = map(unquote_rfc2821_local($_),
  11728. parse_address_list($rfc2822_cc));
  11729. $msginfo->rfc2822_cc(@cc_parsed<2 ? $cc_parsed[0] : \@cc_parsed);
  11730. }
  11731. my(@rfc2822_resent_from, @rfc2822_resent_sender);
  11732. if (defined $msginfo->get_header_field2('resent-from') ||
  11733. defined $msginfo->get_header_field2('resent-sender')) { # triage
  11734. # Each Resent block should have exactly one Resent-From, and none or one
  11735. # Resent-Sender address. A HACK: undef in each list is used to separate
  11736. # addresses obtained from different resent blocks, for the benefit of
  11737. # those interested in traversing them block by block (e.g. when choosing
  11738. # a DKIM signing key). The RFC 5322 section 3.6.6 says: All of the resent
  11739. # fields corresponding to a particular resending of the message SHOULD be
  11740. # grouped together.
  11741. my(@r_from, @r_sender); local($1);
  11742. for (my $j = 0; ; $j++) { # traverse header section by fields, top-down
  11743. my($f_i,$f) = $msginfo->get_header_field2(undef,$j);
  11744. if ( @r_from && (
  11745. !defined($f) || # end of a header section
  11746. $f !~ /^Resent-/si || # presumably end of a resent block
  11747. $f =~ /^Resent-From\s*:/si || # another Resent-From encountered
  11748. $f =~ /^Resent-Sender\s*:/si && @r_sender # another Resent-Sender
  11749. ) ) { # end of a current resent block
  11750. # a hack: undef in a list is used to separate addresses
  11751. # from different resent blocks
  11752. push(@rfc2822_resent_from, undef, @r_from); @r_from = ();
  11753. push(@rfc2822_resent_sender, undef, @r_sender); @r_sender = ();
  11754. }
  11755. last if !defined $f;
  11756. if ($f =~ /^Resent-From\s*:(.*)\z/si) {
  11757. push(@r_from, map(unquote_rfc2821_local($_), parse_address_list($1)));
  11758. } elsif ($f =~ /^Resent-Sender\s*:(.*)\z/si) {
  11759. # multiple Resent-Sender in a block are illegal, store them all anyway
  11760. push(@r_sender,map(unquote_rfc2821_local($_), parse_address_list($1)));
  11761. }
  11762. }
  11763. if (@r_from || @r_sender) { # any leftovers not forming a resent block?
  11764. push(@rfc2822_resent_from, undef, @r_from);
  11765. push(@rfc2822_resent_sender, undef, @r_sender);
  11766. }
  11767. shift(@rfc2822_resent_from) if @rfc2822_resent_from; # remove undef
  11768. shift(@rfc2822_resent_sender) if @rfc2822_resent_sender; # remove undef
  11769. # rfc2822_resent_from and rfc2822_resent_sender are listrefs (or undef)
  11770. $msginfo->rfc2822_resent_from(\@rfc2822_resent_from)
  11771. if @rfc2822_resent_from;
  11772. $msginfo->rfc2822_resent_sender(\@rfc2822_resent_sender)
  11773. if @rfc2822_resent_sender;
  11774. }
  11775. my $mail_size = $msginfo->msg_size; # use corrected ESMTP size if avail.
  11776. if (!defined($mail_size) || $mail_size <= 0) { # not yet known?
  11777. $mail_size = $msginfo->orig_header_size + $msginfo->orig_body_size;
  11778. $msginfo->msg_size($mail_size); # store back
  11779. do_log(4,"message size unknown, size set to %d", $mail_size);
  11780. }
  11781. # check for mailing lists, bulk mail and auto-responses
  11782. my $is_mlist; # mail from a mailing list
  11783. my $is_auto; # bounce, auto-response, challenge-response, ...
  11784. my $is_bulk; # bulk mail or $is_mlist or $is_auto
  11785. if (defined $msginfo->get_header_field2('list-id')) { # RFC 2919
  11786. $is_mlist = $msginfo->get_header_field_body('list-id');
  11787. } elsif (defined $msginfo->get_header_field2('list-post')) {
  11788. $is_mlist = $msginfo->get_header_field_body('list-post');
  11789. } elsif (defined $msginfo->get_header_field2('list-unsubscribe')) {
  11790. $is_mlist = $msginfo->get_header_field_body('list-unsubscribe');
  11791. } elsif (defined $msginfo->get_header_field2('mailing-list')) {
  11792. $is_mlist = $msginfo->get_header_field_body('mailing-list'); # non-std.
  11793. } elsif ($sender =~ /^ (?: [^\@]+ -(?:request|bounces|owner|admin) |
  11794. owner- [^\@]+ ) (?: \@ | \z )/xsi) {
  11795. $is_mlist = 'sender=' . $sender;
  11796. } elsif ($rfc2822_from[0] =~ /^ (?: [^\@]+ -(?:request|bounces|owner) |
  11797. owner- [^\@]+ ) (?: \@ | \z )/xsi) {
  11798. $is_mlist = 'From:' . $rfc2822_from[0];
  11799. }
  11800. if (defined $is_mlist) { # sanitize a bit
  11801. local($1); $is_mlist = $1 if $is_mlist =~ / < (.*) > [^>]* \z/xs;
  11802. $is_mlist =~ s/\s+/ /g; $is_mlist =~ s/^ //; $is_mlist =~ s/ \z//;
  11803. $is_mlist =~ s/^mailto://i;
  11804. $is_mlist = 'ml:' . $is_mlist;
  11805. }
  11806. if (defined $msginfo->get_header_field2('precedence')) {
  11807. my $prec = $msginfo->get_header_field_body('precedence');
  11808. $prec =~ s/^[ \t]+//; local($1);
  11809. $is_mlist = $1 if !defined($is_mlist) && $prec =~ /^(list)/si;
  11810. $is_auto = $1 if $prec =~ /^(auto.?reply)\b/si;
  11811. $is_bulk = $1 if $prec =~ /^(bulk|junk)\b/si;
  11812. }
  11813. if (defined $is_auto) {
  11814. # already set
  11815. } elsif (defined $msginfo->get_header_field2('auto-submitted')) {
  11816. my $auto = $msginfo->get_header_field_body('auto-submitted');
  11817. $auto =~ s/ \( [^)]* \) //gx; $auto =~ s/^[ \t]+//; $auto =~ s/[ \t]+\z//;
  11818. $is_auto = 'Auto-Submitted:' . $auto if lc($auto) ne 'no';
  11819. } elsif ($sender eq '') {
  11820. $is_auto = 'sender=<>';
  11821. } elsif ($sender =~
  11822. /^ (?: mailer-daemon|double-bounce|mailer|autoreply )
  11823. (?: \@ | \z )/xsi) {
  11824. # 'postmaster' is also common, but a bit risky
  11825. $is_auto = 'sender=' . $sender;
  11826. } elsif ($rfc2822_from[0] =~ # just checks the first author, good enough
  11827. /^ (?: mailer-daemon|double-bounce|mailer|autoreply )
  11828. (?: \@ | \z )/xsi) {
  11829. $is_auto = 'From:' . $rfc2822_from[0];
  11830. }
  11831. if (defined $is_mlist) {
  11832. $is_bulk = $is_mlist;
  11833. } elsif (defined $is_auto) {
  11834. $is_bulk = $is_auto;
  11835. } elsif (defined $is_bulk) {
  11836. # already set
  11837. } elsif ($rfc2822_from[0] =~ # just checks the first author, good enough
  11838. /^ (?: [^\@]+ -relay | postmaster | uucp ) (?: \@ | \z )/xsi) {
  11839. $is_bulk = 'From:' . $rfc2822_from[0];
  11840. }
  11841. $is_mlist = 1 if defined $is_mlist && !$is_mlist; # make sure it is true
  11842. $is_auto = 1 if defined $is_auto && !$is_auto; # make sure it is true
  11843. $is_bulk = 1 if defined $is_bulk && !$is_bulk; # make sure it is true
  11844. $msginfo->is_mlist($is_mlist) if defined $is_mlist;
  11845. $msginfo->is_auto($is_auto) if defined $is_auto;
  11846. $msginfo->is_bulk($is_bulk) if defined $is_bulk;
  11847. # now that we have a parsed From, check if we have a valid
  11848. # author domain signature and do other DKIM pre-processing
  11849. if (c('enable_dkim_verification')) {
  11850. Amavis::DKIM::collect_some_dkim_info($msginfo);
  11851. }
  11852. if ($sender ne '') { # provide some initial default for sender_credible
  11853. my(@cred) = ( $msginfo->originating ? 'orig' : (),
  11854. $msginfo->dkim_envsender_sig ? 'dkim' : () );
  11855. $msginfo->sender_credible(join(',',@cred)) if @cred;
  11856. }
  11857. }
  11858. # Checks the message stored on a file. File must already
  11859. # be open on file handle $msginfo->mail_text; it need not be positioned
  11860. # properly, check_mail must not close the file handle.
  11861. # Alternatively, the $msginfo->mail_text can be a ref to a string
  11862. # containing an entire message - suitable for short messages.
  11863. #
  11864. sub check_mail($$) {
  11865. my($msginfo, $dsn_per_recip_capable) = @_;
  11866. my $which_section = 'check_init'; my(%elapsed,$t0_sect);
  11867. $elapsed{'TimeElapsedReceiving'} = Time::HiRes::time - $msginfo->rx_time;
  11868. my $point_of_no_return = 0; # past the point where mail or DSN was sent
  11869. my $mail_id = $msginfo->mail_id; # typically undef at this stage
  11870. my $am_id = $msginfo->log_id;
  11871. my $conn = $msginfo->conn_obj;
  11872. if (!defined($am_id)) { $am_id = am_id(); $msginfo->log_id($am_id) }
  11873. $zmq_obj->register_proc(1,0,'=',$am_id) if $zmq_obj; # check begins
  11874. $snmp_db->register_proc(1,0,'=',$am_id) if $snmp_db;
  11875. my($smtp_resp, $exit_code, $preserve_evidence);
  11876. my $custom_object;
  11877. my $hold; # set to some string causes the message to be placed on hold
  11878. # (frozen) by MTA. This can be used in cases when we stumble
  11879. # across some permanent problem making us unable to decide
  11880. # if the message is to be really delivered.
  11881. # is any mail component password protected or otherwise non-decodable?
  11882. my $any_undecipherable = 0;
  11883. my $mime_err; # undef, or MIME parsing error string as given by MIME::Parser
  11884. if (defined $last_task_completed_at) {
  11885. my $dt = $msginfo->rx_time - $last_task_completed_at;
  11886. do_log(3,"smtp connection cache, dt: %.1f, state: %d",
  11887. $dt, $smtp_connection_cache_enable);
  11888. if (!$smtp_connection_cache_on_demand) {}
  11889. elsif (!$smtp_connection_cache_enable && $dt < 5) {
  11890. do_log(3,"smtp connection cache, dt: %.1f -> enabling", $dt);
  11891. $smtp_connection_cache_enable = 1;
  11892. } elsif ($smtp_connection_cache_enable && $dt >= 15) {
  11893. do_log(3,"smtp connection cache, dt: %.1f -> disabling", $dt);
  11894. $smtp_connection_cache_enable = 0;
  11895. }
  11896. }
  11897. # ugly - save in a global to make it accessible to %builtins
  11898. $MSGINFO = $msginfo;
  11899. eval {
  11900. $msginfo->checks_performed({}) if !$msginfo->checks_performed;
  11901. $msginfo->add_contents_category(CC_CLEAN,0); # CC_CLEAN is always present
  11902. $_->add_contents_category(CC_CLEAN,0) for @{$msginfo->per_recip_data};
  11903. $msginfo->header_edits(Amavis::Out::EditHeader->new);
  11904. add_entropy(Time::HiRes::gettimeofday, $child_task_count, $am_id,
  11905. $msginfo->queue_id, $msginfo->mail_text_fn, $msginfo->sender);
  11906. section_time($which_section);
  11907. $which_section = 'check_init2';
  11908. { my $cwd = $msginfo->mail_tempdir;
  11909. if (!defined $cwd || $cwd eq '') { $cwd = $TEMPBASE }
  11910. chdir($cwd) or die "Can't chdir to $cwd: $!";
  11911. }
  11912. # compute body digest, measure mail size, check for 8-bit data, get entropy
  11913. get_body_digest($msginfo, $Amavis::Conf::mail_digest_algorithm);
  11914. $which_section = 'check_init3';
  11915. collect_some_info($msginfo);
  11916. my $mail_size = $msginfo->msg_size; # use corrected ESMTP size
  11917. if (!defined($msginfo->client_addr)) { # fetch missing address from header
  11918. my $cl_ip = parse_ip_address_from_received($msginfo,1);
  11919. if (defined $cl_ip && $cl_ip ne '') {
  11920. do_log(3,"client IP address unknown, fetched from Received: %s",
  11921. $cl_ip);
  11922. $msginfo->client_addr(normalize_ip_addr($cl_ip));
  11923. }
  11924. }
  11925. $which_section = 'check_init4';
  11926. my $file_generator_object = # maxfiles 0 disables the $MAXFILES limit
  11927. Amavis::Unpackers::NewFilename->new($MAXFILES?$MAXFILES:undef,$mail_size);
  11928. Amavis::Unpackers::Part::init($file_generator_object); # fudge: keep in var
  11929. my $parts_root = Amavis::Unpackers::Part->new;
  11930. $msginfo->parts_root($parts_root);
  11931. # section_time($which_section);
  11932. if (!defined $mail_id && ($sql_store_info_for_all_msgs || !$sql_storage)) {
  11933. $which_section = 'gen_mail_id';
  11934. $zmq_obj->register_proc(2,0,'G',$am_id) if $zmq_obj;
  11935. $snmp_db->register_proc(2,0,'G',$am_id) if $snmp_db;
  11936. # create a mail_id unique to a database and save preliminary info to SQL
  11937. generate_unique_mail_id($msginfo);
  11938. $mail_id = $msginfo->mail_id;
  11939. section_time($which_section) if $sql_storage;
  11940. }
  11941. $which_section = "custom-new";
  11942. eval {
  11943. my $old_orig = c('originating');
  11944. # may load policy banks
  11945. $custom_object = Amavis::Custom->new($conn,$msginfo);
  11946. my $new_orig = c('originating'); # may have changed by a p.b.load
  11947. $msginfo->originating($new_orig) if ($old_orig?1:0) != ($new_orig?1:0);
  11948. update_current_log_level(); 1;
  11949. } or do {
  11950. undef $custom_object;
  11951. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  11952. do_log(-1,"custom new err: %s", $eval_stat);
  11953. };
  11954. if (ref $custom_object) {
  11955. do_log(5,"Custom hooks enabled"); section_time($which_section);
  11956. }
  11957. my $cl_ip = $msginfo->client_addr;
  11958. my($os_fingerprint_obj,$os_fingerprint);
  11959. my $os_fingerprint_method = c('os_fingerprint_method');
  11960. if (!defined($os_fingerprint_method) || $os_fingerprint_method eq '') {
  11961. # no fingerprinting service configured
  11962. } elsif ($cl_ip eq '' || $cl_ip eq '0.0.0.0' || $cl_ip eq '::') {
  11963. # original client IP address not available, can't query p0f
  11964. } else { # launch a query
  11965. $which_section = "os_fingerprint";
  11966. my $dst = c('os_fingerprint_dst_ip_and_port');
  11967. my($dst_ip,$dst_port); local($1,$2,$3);
  11968. ($dst_ip,$dst_port) = ($1.$2, $3) if defined($dst) &&
  11969. $dst =~ m{^(?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) }six;
  11970. $os_fingerprint_obj = Amavis::OS_Fingerprint->new(
  11971. dynamic_destination($os_fingerprint_method,$conn),
  11972. 0.050, $cl_ip, $msginfo->client_port, $dst_ip, $dst_port,
  11973. defined $mail_id ? $mail_id : sprintf("%08x",rand(0x7fffffff)) );
  11974. }
  11975. my $sender = $msginfo->sender;
  11976. my(@recips) = map($_->recip_addr, @{$msginfo->per_recip_data});
  11977. my $rfc2822_sender = $msginfo->rfc2822_sender;
  11978. my $fm = $msginfo->rfc2822_from;
  11979. my(@rfc2822_from) = !defined($fm) ? () : ref $fm ? @$fm : $fm;
  11980. $mail_size = $msginfo->msg_size; # refresh after custom hook, just in case
  11981. add_entropy("$cl_ip $mail_size $sender", \@recips);
  11982. if (ll(1)) {
  11983. my $pbn = c('policy_bank_path');
  11984. ll(1) && do_log(1,"Checking: %s %s%s%s -> %s", $mail_id||'',
  11985. $pbn eq '' ? '' : "$pbn ", $cl_ip eq '' ? '' : "[$cl_ip] ",
  11986. qquote_rfc2821_local($sender),
  11987. join(',', qquote_rfc2821_local(@recips)) );
  11988. }
  11989. if (ll(3)) {
  11990. my $envsender = qquote_rfc2821_local($sender);
  11991. my $hdrsender = qquote_rfc2821_local($rfc2822_sender),
  11992. my $hdrfrom = qquote_rfc2821_local(@rfc2822_from);
  11993. do_log(3,"2822.From: %s%s%s", $hdrfrom,
  11994. !defined($rfc2822_sender) ? '' : ", 2822.Sender: $hdrsender",
  11995. defined $rfc2822_sender && $envsender eq $hdrsender ? ''
  11996. : $envsender eq $hdrfrom ? '' : ", 2821.Mail_From: $envsender");
  11997. }
  11998. my $cnt_local = 0; my $cnt_remote = 0;
  11999. for my $r (@{$msginfo->per_recip_data}) {
  12000. my $recip = $r->recip_addr;
  12001. my $is_local = lookup2(0,$recip, ca('local_domains_maps'));
  12002. $is_local ? $cnt_local++ : $cnt_remote++;
  12003. $r->recip_is_local($is_local ? 1 : 0); # canonical boolean, untainted
  12004. if (!defined($r->bypass_virus_checks)) {
  12005. my $bypassed_v = lookup2(0,$recip, ca('bypass_virus_checks_maps'));
  12006. $r->bypass_virus_checks($bypassed_v);
  12007. }
  12008. if (!defined($r->bypass_banned_checks)) {
  12009. my $bypassed_b = lookup2(0,$recip, ca('bypass_banned_checks_maps'));
  12010. $r->bypass_banned_checks($bypassed_b);
  12011. }
  12012. if (!defined($r->bypass_spam_checks)) {
  12013. my $bypassed_s = lookup2(0,$recip, ca('bypass_spam_checks_maps'));
  12014. $r->bypass_spam_checks($bypassed_s);
  12015. }
  12016. if (defined $user_id_sql) {
  12017. my($user_id_ref,$mk_ref) = # list of all id's that match
  12018. lookup2(1, $recip, [$user_id_sql], Label=>"users.id");
  12019. $r->user_id($user_id_ref) if ref $user_id_ref; # listref or undef
  12020. }
  12021. if (defined $user_policy_id_sql) {
  12022. my $user_policy_id = lookup2(0, $recip, [$user_policy_id_sql],
  12023. Label=>"users.policy_id");
  12024. $r->user_policy_id($user_policy_id); # just the first match
  12025. }
  12026. }
  12027. # update message count and message size snmp counters
  12028. # orig local
  12029. # 0 0 InMsgsOpenRelay
  12030. # 0 1 InMsgsInbound
  12031. # 0 x (non-originating: inbound or open relay)
  12032. # 1 0 InMsgsOutbound
  12033. # 1 1 InMsgsInternal
  12034. # 1 x InMsgsOriginating (outbound or internal)
  12035. # x 0 (departing: outbound or open relay)
  12036. # x 1 (local: inbound or internal)
  12037. # x x InMsgs
  12038. snmp_count('InMsgs');
  12039. snmp_count('InMsgsBounceNullRPath') if $sender eq '';
  12040. snmp_count( ['InMsgsRecips', $cnt_local+$cnt_remote]); # recipients count
  12041. snmp_count( ['InMsgsSize', $mail_size, 'C64'] );
  12042. if ($msginfo->originating) {
  12043. snmp_count('InMsgsOriginating');
  12044. snmp_count( ['InMsgsRecipsOriginating', $cnt_local+$cnt_remote]);
  12045. snmp_count( ['InMsgsSizeOriginating', $mail_size, 'C64'] );
  12046. }
  12047. if ($cnt_local > 0) {
  12048. my $d = $msginfo->originating ? 'Internal' : 'Inbound';
  12049. snmp_count('InMsgs'.$d);
  12050. snmp_count( ['InMsgsRecips'.$d, $cnt_local]);
  12051. snmp_count( ['InMsgsRecipsLocal', $cnt_local]);
  12052. snmp_count( ['InMsgsSize'.$d, $mail_size, 'C64'] );
  12053. }
  12054. if ($cnt_remote > 0) {
  12055. my $d = $msginfo->originating ? 'Outbound' : 'OpenRelay';
  12056. snmp_count('InMsgs'.$d);
  12057. snmp_count( ['InMsgsRecips'.$d, $cnt_remote]);
  12058. snmp_count( ['InMsgsSize'.$d, $mail_size, 'C64'] );
  12059. if (!$msginfo->originating) {
  12060. do_log(1,'Open relay? Nonlocal recips but not originating: %s',
  12061. join(', ', map($_->recip_addr,
  12062. grep(!$_->recip_is_local, @{$msginfo->per_recip_data}))));
  12063. }
  12064. }
  12065. # mkdir can be a costly operation (must be atomic, flushes buffers).
  12066. # If we can re-use directory 'parts' from the previous invocation it saves
  12067. # us precious time. Together with matching rmdir this can amount to 10-15 %
  12068. # of total elapsed time on some traditional file systems (no spam checking)
  12069. $which_section = "creating_partsdir";
  12070. { my $tempdir = $msginfo->mail_tempdir;
  12071. my $errn = lstat("$tempdir/parts") ? 0 : 0+$!;
  12072. if ($errn == ENOENT) { # needs to be created
  12073. mkdir("$tempdir/parts", 0750)
  12074. or die "Can't create directory $tempdir/parts: $!";
  12075. section_time('mkdir parts'); }
  12076. elsif ($errn != 0) { die "$tempdir/parts is not accessible: $!" }
  12077. elsif (!-d _) { die "$tempdir/parts is not a directory" }
  12078. else {} # fine, directory already exists and is accessible
  12079. }
  12080. # FIRST: what kind of e-mail did we get? call content scanners
  12081. my($virus_presence_checked,$spam_presence_checked);
  12082. my $virus_dejavu = 0;
  12083. my($will_do_virus_scanning, $all_bypass_virus_checks);
  12084. if ($extra_code_antivirus) {
  12085. $all_bypass_virus_checks =
  12086. !grep(!$_->bypass_virus_checks, @{$msginfo->per_recip_data});
  12087. $will_do_virus_scanning =
  12088. !$virus_presence_checked && !$all_bypass_virus_checks;
  12089. }
  12090. my $will_do_banned_checking = # banned name checking will be needed?
  12091. @{ca('banned_filename_maps')} || cr('banned_namepath_re');
  12092. my($bounce_header_fields_ref,$bounce_msgid,$bounce_type);
  12093. if (c('bypass_decode_parts')) {
  12094. do_log(5, 'decoding bypassed');
  12095. } elsif (!$will_do_virus_scanning && !$will_do_banned_checking &&
  12096. c('bounce_killer_score') <= 0) {
  12097. do_log(5, 'decoding not needed');
  12098. } else {
  12099. # decoding parts can take a lot of time
  12100. $which_section = "mime_decode-1";
  12101. $zmq_obj->register_proc(2,0,'D',$am_id) if $zmq_obj; # decoding
  12102. $snmp_db->register_proc(2,0,'D',$am_id) if $snmp_db;
  12103. $t0_sect = Time::HiRes::time;
  12104. $mime_err = ensure_mime_entity($msginfo)
  12105. if !defined($msginfo->mime_entity);
  12106. prolong_timer($which_section);
  12107. if (c('bounce_killer_score') > 0) {
  12108. $which_section = "dsn_parse";
  12109. # analyze a bounce after MIME decoding but before further archive
  12110. # decoding (which often replaces original MIME parts by decoded files)
  12111. eval { # just in case
  12112. ($bounce_header_fields_ref,$bounce_type) =
  12113. inspect_a_bounce_message($msginfo);
  12114. 1;
  12115. } or do {
  12116. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  12117. do_log(-1, "inspect_a_bounce_message failed: %s", $eval_stat);
  12118. };
  12119. if ($bounce_header_fields_ref &&
  12120. exists $bounce_header_fields_ref->{'message-id'}) {
  12121. $bounce_msgid = $bounce_header_fields_ref->{'message-id'};
  12122. }
  12123. prolong_timer($which_section);
  12124. }
  12125. $which_section = "parts_decode_ext";
  12126. snmp_count('OpsDec');
  12127. ($hold,$any_undecipherable) =
  12128. Amavis::Unpackers::decompose_mail($msginfo->mail_tempdir,
  12129. $file_generator_object);
  12130. if ($hold ne '' || $any_undecipherable) {
  12131. $msginfo->add_contents_category(CC_UNCHECKED,0);
  12132. for my $r (@{$msginfo->per_recip_data}) {
  12133. $r->add_contents_category(CC_UNCHECKED,0)
  12134. if !$r->bypass_virus_checks;
  12135. }
  12136. }
  12137. $elapsed{'TimeElapsedDecoding'} = Time::HiRes::time - $t0_sect;
  12138. }
  12139. my $bphcm = ca('bypass_header_checks_maps');
  12140. if (grep(!lookup2(0,$_->recip_addr,$bphcm), @{$msginfo->per_recip_data})) {
  12141. $which_section = "check_header";
  12142. my $allowed_tests = cr('allowed_header_tests');
  12143. my($badh_ref,$minor_badh_cc);
  12144. if ($allowed_tests && %$allowed_tests) {
  12145. ($badh_ref,$minor_badh_cc) = check_header_validity($msginfo);
  12146. $msginfo->checks_performed->{H} = 1;
  12147. if (@$badh_ref) {
  12148. push(@bad_headers, @$badh_ref);
  12149. $msginfo->add_contents_category(CC_BADH,$minor_badh_cc);
  12150. }
  12151. }
  12152. my $allowed_mime_test = $allowed_tests && $allowed_tests->{'mime'};
  12153. # check for bad headers and for bad MIME subheaders / bad MIME structure
  12154. if ($allowed_mime_test && defined $mime_err && $mime_err ne '') {
  12155. push(@bad_headers, "MIME error: ".$mime_err);
  12156. $msginfo->add_contents_category(CC_BADH,1);
  12157. }
  12158. for my $r (@{$msginfo->per_recip_data}) {
  12159. my $bypassed = lookup2(0,$r->recip_addr,$bphcm);
  12160. if (!$bypassed && @$badh_ref) {
  12161. $r->add_contents_category(CC_BADH,$minor_badh_cc);
  12162. }
  12163. if (!$bypassed && $allowed_mime_test &&
  12164. defined $mime_err && $mime_err ne '') {
  12165. $r->add_contents_category(CC_BADH,1); # CC_BADH min: 1=broken mime
  12166. }
  12167. }
  12168. section_time($which_section);
  12169. }
  12170. if ($will_do_banned_checking) { # check for banned file contents
  12171. $which_section = "check-banned";
  12172. check_for_banned_names($msginfo); # saves results in $msginfo
  12173. $msginfo->checks_performed->{B} = 1;
  12174. $banned_filename_any = 0; $banned_filename_all = 1;
  12175. for my $r (@{$msginfo->per_recip_data}) {
  12176. next if $r->bypass_banned_checks;
  12177. my $a = $r->banned_parts;
  12178. if (!defined $a || !@$a) {
  12179. $banned_filename_all = 0;
  12180. } else {
  12181. my $rhs = $r->banning_rule_rhs;
  12182. if (defined $rhs) {
  12183. for my $j (0..$#{$a}) {
  12184. $r->dsn_suppress_reason(sprintf("BANNED:%s suggested by rule",
  12185. $rhs->[$j])) if $rhs->[$j] =~ /^DISCARD/;
  12186. }
  12187. }
  12188. $banned_filename_any = 1;
  12189. $r->add_contents_category(CC_BANNED,0);
  12190. }
  12191. }
  12192. $msginfo->add_contents_category(CC_BANNED,0) if $banned_filename_any;
  12193. ll(4) && do_log(4,"banned check: any=%d, all=%s (%d)",
  12194. $banned_filename_any, $banned_filename_all?'Y':'N',
  12195. scalar(@{$msginfo->per_recip_data}));
  12196. }
  12197. my $virus_checking_failed = 0;
  12198. if (!$extra_code_antivirus) {
  12199. do_log(5, "no anti-virus code loaded, skipping virus_scan");
  12200. } elsif ($all_bypass_virus_checks) {
  12201. do_log(5, "bypassing of virus checks requested");
  12202. } elsif (defined $hold && $hold ne '') { # protect virus scanner from bombs
  12203. do_log(0, "NOTICE: Virus scanning skipped: %s", $hold);
  12204. $will_do_virus_scanning = 0;
  12205. } else {
  12206. if (!$will_do_virus_scanning)
  12207. { do_log(-1, "NOTICE: will_do_virus_scanning is false???") }
  12208. $mime_err = ensure_mime_entity($msginfo)
  12209. if !defined($msginfo->mime_entity) && !c('bypass_decode_parts');
  12210. # special case to make available a complete mail file for inspection
  12211. if ((defined $mime_err && $mime_err ne '') ||
  12212. !defined($msginfo->mime_entity) ||
  12213. lookup2(0,'MAIL',\@keep_decoded_original_maps) ||
  12214. $any_undecipherable && lookup2(0,'MAIL-UNDECIPHERABLE',
  12215. \@keep_decoded_original_maps)) {
  12216. if (!defined($msginfo->mail_text_fn)) {
  12217. do_log(5,"can't present full original message to scanners, no file");
  12218. } else {
  12219. # keep the email.txt by making a hard link to it in ./parts/
  12220. $which_section = "linking-to-MAIL";
  12221. my $tempdir = $msginfo->mail_tempdir;
  12222. my $newpart_obj =
  12223. Amavis::Unpackers::Part->new("$tempdir/parts",$parts_root,1);
  12224. my $newpart = $newpart_obj->full_name;
  12225. ll(3) && do_log(3,'presenting full original message to scanners '.
  12226. 'as %s%s%s%s',
  12227. $newpart,
  12228. !$any_undecipherable ? '' : ", $any_undecipherable undecipherable",
  12229. defined $msginfo->mime_entity ? '' : ', MIME not decoded',
  12230. !defined $mime_err || $mime_err eq '' ? ''
  12231. : ", MIME error: $mime_err");
  12232. link($msginfo->mail_text_fn, $newpart)
  12233. or die sprintf("Can't create hard link %s to %s: %s",
  12234. $newpart, $msginfo->mail_text_fn, $!);
  12235. $newpart_obj->type_short('MAIL'); # case sensitive
  12236. $newpart_obj->type_declared('message/rfc822');
  12237. }
  12238. }
  12239. $which_section = "virus_scan";
  12240. $zmq_obj->register_proc(2,0,'V',$am_id) if $zmq_obj; # virus scan
  12241. $snmp_db->register_proc(2,0,'V',$am_id) if $snmp_db;
  12242. my $av_ret; $t0_sect = Time::HiRes::time;
  12243. $virus_checking_failed = 1;
  12244. eval {
  12245. my($vn, $ds);
  12246. ($av_ret, $av_output, $vn, $ds) =
  12247. Amavis::AV::virus_scan($msginfo, $child_task_count==1);
  12248. @virusname = @$vn; @detecting_scanners = @$ds; # copy
  12249. if (defined $av_ret) {
  12250. $virus_presence_checked = 1; $virus_checking_failed = 0;
  12251. $msginfo->checks_performed->{V} = 1;
  12252. }
  12253. 1;
  12254. } or do {
  12255. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  12256. do_log(-2, "AV: %s", $eval_stat);
  12257. $virus_checking_failed = $eval_stat;
  12258. $virus_checking_failed = 1 if !$virus_checking_failed;
  12259. };
  12260. $elapsed{'TimeElapsedVirusCheck'} = Time::HiRes::time - $t0_sect;
  12261. snmp_count('OpsVirusCheck');
  12262. if ($virus_presence_checked && @virusname && $snmp_db) {
  12263. $which_section = "read_snmp_variables";
  12264. # true if none found with a counter value of zero or undef
  12265. $virus_dejavu = 1 if !grep(!defined($_) || $_ == 0,
  12266. @{$snmp_db->read_snmp_variables(
  12267. map("virus.byname.$_", @virusname))});
  12268. section_time($which_section);
  12269. }
  12270. }
  12271. if ($virus_checking_failed) {
  12272. $msginfo->add_contents_category(CC_UNCHECKED,0);
  12273. for my $r (@{$msginfo->per_recip_data}) {
  12274. $r->add_contents_category(CC_UNCHECKED,0) if !$r->bypass_virus_checks;
  12275. }
  12276. if (c('virus_scanners_failure_is_fatal')) {
  12277. $hold = 'AV: ' . $virus_checking_failed;
  12278. die "$hold\n"; # TEMPFAIL
  12279. }
  12280. }
  12281. $which_section = "post_virus_scan";
  12282. if (@virusname) {
  12283. my $virus_suppress_reason;
  12284. my($ccat_maj,$ccat_min) = (CC_VIRUS,0);
  12285. my $vtfsm = ca('viruses_that_fake_sender_maps');
  12286. if (@$vtfsm) {
  12287. for my $vn (@virusname) {
  12288. my($result,$matchingkey) = lookup2(0,$vn,$vtfsm);
  12289. if ($result) { # is a virus known to fake a sender address
  12290. do_log(3,"Virus %s matches %s, sender addr ignored",
  12291. $vn,$matchingkey);
  12292. # try to get some info on sender source from his IP address
  12293. my $first_rcvd_from_ip = parse_ip_address_from_received($msginfo);
  12294. if (defined $first_rcvd_from_ip && $first_rcvd_from_ip ne '') {
  12295. $msginfo->sender_source(sprintf('?@[%s]', $first_rcvd_from_ip));
  12296. } else {
  12297. $msginfo->sender_source(undef);
  12298. }
  12299. $virus_suppress_reason = 'INFECTED';
  12300. # $ccat_min = 1;
  12301. last;
  12302. }
  12303. }
  12304. }
  12305. $msginfo->add_contents_category($ccat_maj,$ccat_min);
  12306. for my $r (@{$msginfo->per_recip_data}) {
  12307. $r->add_contents_category(
  12308. $ccat_maj,$ccat_min) if !$r->bypass_virus_checks;
  12309. if (defined $virus_suppress_reason) {
  12310. $r->dsn_suppress_reason($virus_suppress_reason .
  12311. (!defined $_ ? '' : ", $_")) for $r->dsn_suppress_reason;
  12312. }
  12313. }
  12314. $msginfo->virusnames([@virusname]); # save a copy of virus names
  12315. my $vntpbm = ca('virus_name_to_policy_bank_maps');
  12316. if (@$vntpbm) {
  12317. my(@bank_names, %bank_names);
  12318. for my $vn (@virusname) {
  12319. my($result,$matchingkey) = lookup2(0,$vn,$vntpbm);
  12320. if ($result) {
  12321. if ($result eq '1') {
  12322. # a handy usability trick to supply a hardwired policy bank
  12323. # name when acl-style lookup table is used, which can only
  12324. # return a boolean (undef, 0, or 1)
  12325. $result = 'VIRUS';
  12326. }
  12327. # $result is a list of policy banks as a comma-separated string
  12328. my(@pbn); # collect list of newly encountered policy bank names
  12329. for (map { my $s = $_; $s =~ s/^[ \t]+//; $s =~ s/[ \t]+\z//; $s }
  12330. split(/,/, $result)) {
  12331. next if $_ eq '' || $bank_names{$_};
  12332. push(@pbn,$_); $bank_names{$_} = 1;
  12333. }
  12334. if (@pbn) {
  12335. push(@bank_names,@pbn);
  12336. ll(2) && do_log(2, "virus %s loads policy bank(s) %s, match: %s",
  12337. $vn, join(',',@pbn), $matchingkey);
  12338. }
  12339. }
  12340. }
  12341. if (@bank_names) {
  12342. # ignore nonexisting bank names, skip duplicates
  12343. @bank_names = grep(defined $policy_bank{$_},
  12344. unique_list(\@bank_names));
  12345. if (@bank_names) {
  12346. load_policy_bank($_,$msginfo) for @bank_names;
  12347. $msginfo->originating(c('originating')); # may have changed
  12348. }
  12349. }
  12350. }
  12351. }
  12352. if (defined($os_fingerprint_obj)) {
  12353. $which_section = "fingerprint_collect";
  12354. $os_fingerprint = $os_fingerprint_obj->collect_response;
  12355. if (defined $os_fingerprint && $os_fingerprint ne '') {
  12356. $msginfo->checks_performed->{F} = 1;
  12357. if ($msginfo->originating)
  12358. { $os_fingerprint = 'MYNETWORKS' } # blank-out our smtp clients info
  12359. $msginfo->client_os_fingerprint($os_fingerprint); # store info
  12360. }
  12361. }
  12362. my($bypass_spam_checks_by_bounce_killer);
  12363. if (!$bounce_header_fields_ref) {
  12364. # not a bounce
  12365. } elsif ($msginfo->originating) {
  12366. # will be rescued from bounce killing by the originating flag
  12367. } elsif (defined($bounce_msgid) &&
  12368. $bounce_msgid =~ /(\@[^\@>() \t][^\@>]*?)[ \t]*>?\z/ &&
  12369. lookup2(0,$1, ca('local_domains_maps'))) {
  12370. # will be rescued from bounce killing by a local domain
  12371. # in referenced Message-ID
  12372. } elsif (!defined($sql_storage) || !$sql_store_info_for_all_msgs ||
  12373. c('penpals_bonus_score') <= 0 || c('penpals_halflife') <= 0) {
  12374. # will be rescued from bounce killing by pen pals disabled
  12375. } elsif (c('bounce_killer_score') > 20) {
  12376. # is a bounce and is eligible to bounce killing, no need for spam scan
  12377. $bypass_spam_checks_by_bounce_killer = 1;
  12378. }
  12379. # consider doing spam scanning
  12380. if (!$extra_code_antispam) {
  12381. do_log(5, "no anti-spam code loaded, skipping spam_scan");
  12382. } elsif ($bypass_spam_checks_by_bounce_killer) {
  12383. do_log(5, "bypassing of spam checks by a bounce killer");
  12384. } elsif (!grep(!$_->bypass_spam_checks, @{$msginfo->per_recip_data})) {
  12385. do_log(5, "bypassing of spam checks requested for all recips");
  12386. } else {
  12387. # preliminary test - would a message be allowed to pass for any recipient
  12388. # based on evidence collected so far (virus, banned)
  12389. my $any_pass = 0; my $prelim_blocking_ccat;
  12390. for my $r (@{$msginfo->per_recip_data}) {
  12391. my $final_destiny = D_PASS;
  12392. my(@fd_tuples) = $r->setting_by_main_contents_category_all(
  12393. cr('final_destiny_by_ccat'), cr('lovers_maps_by_ccat'));
  12394. for my $tuple (@fd_tuples) {
  12395. my($cc, $fd, $lovers_map_ref) = @$tuple;
  12396. if (!defined($fd) || $fd == D_PASS) {
  12397. } elsif (defined($lovers_map_ref) &&
  12398. lookup2(0, $r->recip_addr, $lovers_map_ref,
  12399. Label=>'Lovers1')) {
  12400. } else {
  12401. $prelim_blocking_ccat = $cc; $final_destiny = $fd;
  12402. last;
  12403. }
  12404. }
  12405. $any_pass = 1 if $final_destiny == D_PASS;
  12406. }
  12407. if (!$any_pass) {
  12408. do_log(5, "bypassing of spam checks, message will be blocked anyway ".
  12409. "due to %s", $prelim_blocking_ccat);
  12410. } else {
  12411. $which_section = "spam-wb-list";
  12412. my($any_wbl, $all_wbl) = Amavis::SpamControl::white_black_list(
  12413. $msginfo, $sql_wblist, $user_id_sql, $ldap_lookups);
  12414. section_time($which_section);
  12415. if ($all_wbl) {
  12416. do_log(5, "sender white/blacklisted, skipping spam_scan");
  12417. } elsif (!$spamcontrol_obj) {
  12418. do_log(5, "spam scanning disabled, no spamcontrol_obj");
  12419. } else {
  12420. $which_section = "spam_scan";
  12421. $zmq_obj->register_proc(2,0,'S',$am_id) if $zmq_obj;
  12422. $snmp_db->register_proc(2,0,'S',$am_id) if $snmp_db;
  12423. $t0_sect = Time::HiRes::time;
  12424. # sets $msginfo->spam_level, spam_status,
  12425. # spam_report, spam_summary, supplementary_info
  12426. $spamcontrol_obj->spam_scan($msginfo);
  12427. eval { # treat any failures there as non-fatal, just in case
  12428. $spamcontrol_obj->auto_learn($msginfo); 1;
  12429. } or do {
  12430. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  12431. do_log(-1, "Auto-learn failed: %s", $eval_stat);
  12432. };
  12433. $msginfo->checks_performed->{S} = 1;
  12434. prolong_timer($which_section);
  12435. $elapsed{'TimeElapsedSpamCheck'} = Time::HiRes::time - $t0_sect;
  12436. snmp_count('OpsSpamCheck');
  12437. $spam_presence_checked = 1;
  12438. }
  12439. }
  12440. }
  12441. if (ref $custom_object) {
  12442. $which_section = "custom-checks";
  12443. eval {
  12444. $custom_object->checks($conn,$msginfo);
  12445. update_current_log_level(); 1;
  12446. } or do {
  12447. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  12448. do_log(-1,"custom checks error: %s", $eval_stat);
  12449. };
  12450. section_time($which_section);
  12451. }
  12452. snmp_count("virus.byname.$_") for @virusname;
  12453. my(@sa_tests,%sa_tests);
  12454. { my $tests = $msginfo->supplementary_info('TESTS');
  12455. if (defined($tests) && $tests ne 'none') {
  12456. @sa_tests = $tests =~ /([^=,;]+)(?==)/g;
  12457. %sa_tests = map(($_,1), @sa_tests);
  12458. }
  12459. }
  12460. # SECOND: now that we know what we got, decide what to do with it
  12461. $which_section = 'after_scanning';
  12462. Amavis::DKIM::adjust_score_by_signer_reputation($msginfo)
  12463. if $msginfo->dkim_signatures_valid;
  12464. my($min_spam_level, $max_spam_level) =
  12465. minmax(map($_->spam_level, @{$msginfo->per_recip_data}));
  12466. $min_spam_level = 0 if !defined $min_spam_level;
  12467. $max_spam_level = 0 if !defined $max_spam_level;
  12468. $which_section = "penpals_check";
  12469. my $pp_age;
  12470. if (!defined $sql_storage || !$sql_store_info_for_all_msgs) {
  12471. # pen pals disabled - SQL data not available or incomplete
  12472. } elsif ($msginfo->is_in_contents_category(CC_VIRUS)) {
  12473. # pen pals disabled, not needed for infected messages
  12474. } else {
  12475. my $pp_bonus = c('penpals_bonus_score'); # score points
  12476. my $pp_halflife = c('penpals_halflife'); # seconds
  12477. if ($pp_bonus <= 0 || $pp_halflife <= 0) {
  12478. # penpals disabled
  12479. } elsif (defined($penpals_threshold_low) && !defined($bounce_msgid) &&
  12480. $max_spam_level < $penpals_threshold_low) {
  12481. # low score for all recipients, no need for aid
  12482. do_log(5,"penpals: low score, no need for penpals aid");
  12483. } elsif (defined($penpals_threshold_high) && !defined($bounce_msgid) &&
  12484. $min_spam_level - $pp_bonus > $penpals_threshold_high) {
  12485. # spam, can't get below threshold_high even under best circumstances
  12486. do_log(5,"penpals: high score, penpals won't help");
  12487. } elsif ($sender ne '' && !$msginfo->originating &&
  12488. lookup2(0,$sender, ca('local_domains_maps'))) {
  12489. # no bonus to unauthent. senders from outside claiming a local domain
  12490. do_log(5,"penpals: local sender from outside, ignored: %s", $sender);
  12491. } else {
  12492. $t0_sect = Time::HiRes::time;
  12493. $zmq_obj->register_proc(2,0,'P',$am_id) if $zmq_obj; # penpals
  12494. $snmp_db->register_proc(2,0,'P',$am_id) if $snmp_db;
  12495. my $sid = $msginfo->sender_maddr_id;
  12496. for my $r (@{$msginfo->per_recip_data}) {
  12497. next if $r->recip_done; # already dealt with
  12498. my $recip = $r->recip_addr;
  12499. my $rid = $r->recip_maddr_id;
  12500. if (defined($rid) && $sid ne $rid && $r->recip_is_local) {
  12501. # inbound or internal_to_internal, except self_to_self
  12502. my $refs_str = $msginfo->get_header_field_body('in-reply-to') .
  12503. $msginfo->get_header_field_body('references');
  12504. my(@refs) = $refs_str eq '' ? () : parse_message_id($refs_str);
  12505. push(@refs,$bounce_msgid) if defined $bounce_msgid &&
  12506. $bounce_msgid ne '';
  12507. do_log(4,"penpals: references: %s", join(", ",@refs)) if @refs;
  12508. # NOTE: swap $rid and $sid as args here, as we are now checking
  12509. # for a potential reply mail - whether the current recipient has
  12510. # recently sent any mail to the sender of the current mail:
  12511. my($pp_mail_id,$pp_subj);
  12512. ($pp_age,$pp_mail_id,$pp_subj) =
  12513. $sql_storage->penpals_find($rid,$sid,\@refs,$msginfo->rx_time);
  12514. $msginfo->checks_performed->{P} = 1;
  12515. if (defined $pp_age) { # found info about previous correspondence
  12516. $r->recip_penpals_age($pp_age); # save the information
  12517. my $weight = exp(-($pp_age/$pp_halflife) * log(2));
  12518. # weight is a factor between 1 and 0, representing
  12519. # exponential decay: weight(t) = 1 / 2^(t/halflife)
  12520. # i.e. factors 1, 1/2, 1/4, 1/8... at age 0, hl, 2*hl, 3*hl...
  12521. my $adj = - $weight * $pp_bonus;
  12522. $r->recip_penpals_score($adj);
  12523. $r->spam_level( ($r->spam_level || 0) + $adj);
  12524. { my $spam_tests = 'AM.PENPAL=' . (0+sprintf("%.3f",$adj));
  12525. if (!defined($r->spam_tests)) {
  12526. $r->spam_tests([ \$spam_tests ]);
  12527. } else {
  12528. unshift(@{$r->spam_tests}, \$spam_tests);
  12529. }
  12530. }
  12531. if (ll(2)) {
  12532. do_log(2,"penpals: bonus %.3f, age %s (%d), ".
  12533. "SA score %.3f, <%s> replying to <%s>, ref mail_id: %s",
  12534. -$adj, format_time_interval($pp_age), $pp_age,
  12535. $r->spam_level, $sender, $recip, $pp_mail_id);
  12536. my $this_subj = $msginfo->get_header_field_body('subject');
  12537. $this_subj = $1 if $this_subj =~ /^\s*(.*?)\s*$/;
  12538. do_log(2,"penpals: prev Subject: %s", $pp_subj);
  12539. do_log(2,"penpals: this Subject: %s", $this_subj);
  12540. }
  12541. }
  12542. }
  12543. }
  12544. section_time($which_section);
  12545. $elapsed{'TimeElapsedPenPals'} = Time::HiRes::time - $t0_sect;
  12546. }
  12547. }
  12548. $which_section = "bounce_killer";
  12549. if ($bounce_header_fields_ref) { # message looks like a DSN (= bounce)
  12550. snmp_count('InMsgsBounce');
  12551. my $bounce_rescued;
  12552. if (defined $pp_age && $pp_age < 8*24*3600) { # less than 8 days ago
  12553. # found by pen pals by a Message-ID in attachment and recip. address;
  12554. # is a bounce, refers to our previous outgoing message, treat it kindly
  12555. snmp_count('InMsgsBounceRescuedByPenPals');
  12556. $bounce_rescued = 'by penpals';
  12557. } elsif ($msginfo->originating) {
  12558. snmp_count('InMsgsBounceRescuedByOriginating');
  12559. $bounce_rescued = 'by originating';
  12560. } elsif (defined($bounce_msgid) &&
  12561. $bounce_msgid =~ /(\@[^\@>() \t][^\@>]*?)[ \t]*>?\z/ &&
  12562. lookup2(0,$1, ca('local_domains_maps'))) {
  12563. # not in pen pals, but domain in Message-ID is a local domain;
  12564. # it is only useful until spammers figure out the trick,
  12565. # then it should be disabled
  12566. snmp_count('InMsgsBounceRescuedByDomain');
  12567. $bounce_rescued = 'by domain';
  12568. } elsif (!defined($sql_storage) ||
  12569. c('penpals_bonus_score') <= 0 || c('penpals_halflife') <= 0) {
  12570. $bounce_rescued = 'by: pen pals disabled';
  12571. }
  12572. ll(2) && do_log(2, "bounce %s (%s), %s -> %s, %s",
  12573. defined $bounce_rescued ?'rescued '.$bounce_rescued :'killed',
  12574. $bounce_type, qquote_rfc2821_local($sender),
  12575. join(',', qquote_rfc2821_local(@recips)),
  12576. join(', ', map { $_ . ': ' . $bounce_header_fields_ref->{$_} }
  12577. sort( grep(/^(?:From|Return-Path|Message-ID|Date)\z/i,
  12578. keys %$bounce_header_fields_ref) )) );
  12579. if (!$bounce_rescued) {
  12580. snmp_count('InMsgsBounceKilled');
  12581. my $bounce_killer_score = c('bounce_killer_score');
  12582. for my $r (@{$msginfo->per_recip_data}) {
  12583. $r->spam_level( ($r->spam_level || 0) + $bounce_killer_score);
  12584. my $spam_tests = 'AM.BOUNCE=' . $bounce_killer_score;
  12585. if (!defined($r->spam_tests)) {
  12586. $r->spam_tests([ \$spam_tests ]);
  12587. } else {
  12588. unshift(@{$r->spam_tests}, \$spam_tests);
  12589. }
  12590. }
  12591. }
  12592. # else: not a recognizable bounce
  12593. } elsif ($msginfo->is_auto ||
  12594. $sender =~ /^postmaster(?:\@|\z)/si ||
  12595. $rfc2822_from[0] =~ /^postmaster(?:\@|\z)/si ||
  12596. $sa_tests{'ANY_BOUNCE_MESSAGE'} ) {
  12597. # message could be some kind of a non-standard bounce or autoresponse,
  12598. # but lacks recognizable structure and a header section from orig. mail
  12599. ll(2) && do_log(2, "bounce unverifiable%s, %s -> %s",
  12600. !$msginfo->originating ? '' : ', originating',
  12601. qquote_rfc2821_local($sender),
  12602. join(',', qquote_rfc2821_local(@recips)));
  12603. snmp_count('InMsgsBounce'); snmp_count('InMsgsBounceUnverifiable');
  12604. }
  12605. $which_section = "decide_mail_destiny";
  12606. $zmq_obj->register_proc(2,0,'r',$am_id) if $zmq_obj; # results...
  12607. $snmp_db->register_proc(2,0,'r',$am_id) if $snmp_db;
  12608. my $considered_oversize_by_some_recips;
  12609. my $mslm = ca('message_size_limit_maps');
  12610. for my $r (@{$msginfo->per_recip_data}) {
  12611. next if $r->recip_done; # already dealt with
  12612. my $recip = $r->recip_addr;
  12613. my $spam_level = $r->spam_level;
  12614. # consider adding CC_SPAM or CC_SPAMMY to the contents_category list;
  12615. # spaminess is an individual matter, we must compare spam level
  12616. # with each recipient setting, there is no single global criterion
  12617. my($tag_level,$tag2_level,$tag3_level,$kill_level);
  12618. my $bypassed = $r->bypass_spam_checks;
  12619. if (!$bypassed) {
  12620. $tag_level = lookup2(0,$recip, ca('spam_tag_level_maps'));
  12621. $tag2_level = lookup2(0,$recip, ca('spam_tag2_level_maps'));
  12622. $tag3_level = lookup2(0,$recip, ca('spam_tag3_level_maps'));
  12623. $kill_level = lookup2(0,$recip, ca('spam_kill_level_maps'));
  12624. }
  12625. my $blacklisted = $r->recip_blacklisted_sender;
  12626. my $whitelisted = $r->recip_whitelisted_sender;
  12627. # penpals_score is already accounted for in spam_level,
  12628. # it is provided here separately for informational/logging purposes
  12629. my $penpals_score = $r->recip_penpals_score; # is zero or negative!
  12630. my $do_tag = !$bypassed && (
  12631. $blacklisted || !defined $tag_level || $tag_level eq '' ||
  12632. ($spam_level + ($whitelisted?-10:0) >= $tag_level));
  12633. my($do_tag2,$do_tag3,$do_kill) =
  12634. map { !$bypassed && !$whitelisted &&
  12635. ($blacklisted || (defined($_) && $spam_level >= $_) ) }
  12636. ($tag2_level,$tag3_level,$kill_level);
  12637. $do_tag2 = $do_tag2 || $do_tag3; # tag3 implies tag2, just in case
  12638. if ($do_tag) { # spaminess is at or above tag level
  12639. $msginfo->add_contents_category(CC_CLEAN,1);
  12640. $r->add_contents_category(CC_CLEAN,1) if !$bypassed;
  12641. }
  12642. if ($do_tag2) { # spaminess is at or above tag2 level
  12643. $msginfo->add_contents_category(CC_SPAMMY);
  12644. $r->add_contents_category(CC_SPAMMY) if !$bypassed;
  12645. }
  12646. if ($do_tag3) { # spaminess is at or above tag3 level
  12647. $msginfo->add_contents_category(CC_SPAMMY,1);
  12648. $r->add_contents_category(CC_SPAMMY,1) if !$bypassed;
  12649. }
  12650. if ($do_kill) { # spaminess is at or above kill level
  12651. $msginfo->add_contents_category(CC_SPAM,0);
  12652. $r->add_contents_category(CC_SPAM,0) if !$bypassed;
  12653. }
  12654. # consider adding CC_OVERSIZED to the contents_category list;
  12655. if (@$mslm) { # checking of mail size is needed?
  12656. my $size_limit = lookup2(0,$r->recip_addr,$mslm);
  12657. if ($enforce_smtpd_message_size_limit_64kb_min &&
  12658. $size_limit && $size_limit < 65536)
  12659. { $size_limit = 65536 } # RFC 5321 requires at least 64k
  12660. if ($size_limit && $mail_size > $size_limit) {
  12661. do_log(1,"OVERSIZED from %s to %s: size %s B, limit %s B",
  12662. $msginfo->sender_smtp, $r->recip_addr_smtp,
  12663. $mail_size, $size_limit)
  12664. if !$considered_oversize_by_some_recips;
  12665. $considered_oversize_by_some_recips = 1;
  12666. $r->add_contents_category(CC_OVERSIZED,0);
  12667. $msginfo->add_contents_category(CC_OVERSIZED,0);
  12668. }
  12669. }
  12670. # determine true reason for blocking,considering lovers and final_destiny
  12671. my $blocking_ccat; my $final_destiny = D_PASS; my $to_be_mangled;
  12672. my(@fd_tuples) = $r->setting_by_main_contents_category_all(
  12673. cr('final_destiny_by_ccat'), cr('lovers_maps_by_ccat'),
  12674. cr('defang_maps_by_ccat') );
  12675. for my $tuple (@fd_tuples) {
  12676. my($cc, $fd, $lovers_map_ref, $mangle_map_ref) = @$tuple;
  12677. if (!defined($fd) || $fd == D_PASS) {
  12678. do_log(5, "final_destiny (ccat=%s) is PASS, recip %s", $cc,$recip);
  12679. } elsif (defined($lovers_map_ref) &&
  12680. lookup2(0,$recip,$lovers_map_ref, Label=>'Lovers2')) {
  12681. do_log(5, "contents lover (ccat=%s) %s", $cc,$recip);
  12682. } elsif ($fd == D_BOUNCE &&
  12683. ($sender eq '' || defined($msginfo->is_bulk)) &&
  12684. ccat_maj($cc) == CC_BADH) {
  12685. # have mercy on bad header section in mail from mailing lists and
  12686. # in DSN: since a bounce for such mail will be suppressed, it is
  12687. # probably better to just let a mail with a bad header section pass,
  12688. # it is rather innocent
  12689. my $is_bulk = $msginfo->is_bulk;
  12690. do_log(1, "allow bad header section from %s<%s> -> <%s>: %s",
  12691. !defined($is_bulk) ? '' : "($is_bulk) ",
  12692. $sender, $recip, $bad_headers[0]);
  12693. } else {
  12694. $blocking_ccat = $cc; $final_destiny = $fd;
  12695. my $cc_main = $r->contents_category;
  12696. $cc_main = $cc_main->[0] if $cc_main;
  12697. if ($blocking_ccat eq $cc_main) {
  12698. do_log(3, "blocking contents category is (%s) for %s",
  12699. $blocking_ccat,$recip);
  12700. } else {
  12701. do_log(3, "blocking ccat (%s) differs from ccat_maj=%s, %s",
  12702. $blocking_ccat,$cc_main,$recip);
  12703. }
  12704. last; # first blocking wins, also skips turning on mangling
  12705. }
  12706. # topmost mangling reason wins
  12707. if (!defined($to_be_mangled) && defined($mangle_map_ref)) {
  12708. my $mangle_type =
  12709. !ref($mangle_map_ref) ? $mangle_map_ref # compatibility
  12710. : lookup2(0,$recip,$mangle_map_ref, Label=>'Mangling1');
  12711. $to_be_mangled = $mangle_type if $mangle_type ne '';
  12712. }
  12713. }
  12714. $r->recip_destiny($final_destiny);
  12715. if (defined $blocking_ccat) { # save a blocking contents category
  12716. $r->blocking_ccat($blocking_ccat);
  12717. # summarize per-recipient blocking_ccat to a message level
  12718. my $msg_bl_ccat = $msginfo->blocking_ccat;
  12719. if (!defined($msg_bl_ccat) || cmp_ccat($blocking_ccat,$msg_bl_ccat)>0)
  12720. { $msginfo->blocking_ccat($blocking_ccat) }
  12721. } else { # defanging/mangling only has effect on passed mail
  12722. # defang_all serves mostly for testing purposes and compatibility
  12723. $to_be_mangled = 1 if !$to_be_mangled && c('defang_all');
  12724. if ($to_be_mangled) {
  12725. my $orig_to_be_mangled = $to_be_mangled;
  12726. if ($to_be_mangled =~ /^(?:disclaimer|nulldisclaimer)\z/i) {
  12727. # disclaimers can only go to mail originating from internal
  12728. # networks - the 'allow_disclaimers' should (only) be enabled
  12729. # by an appropriate policy bank, e.g. MYNETS and/or ORIGINATING
  12730. if (!c('allow_disclaimers')) {
  12731. $to_be_mangled = 0; # not for remote or unauthorized clients
  12732. do_log(5,"will not add disclaimer, allow_disclaimers is false");
  12733. } else {
  12734. my $rf = $msginfo->rfc2822_resent_from;
  12735. my $rs = $msginfo->rfc2822_resent_sender;
  12736. # disclaimers should only go to mail with 2822.From or
  12737. # 2822.Sender or 2822.Resent-From or 2822.Resent-Sender
  12738. # or 2821.mail_from address matching local domains
  12739. if (!grep(defined($_) && $_ ne '' &&
  12740. lookup2(0,$_, ca('local_domains_maps')),
  12741. unique_list( (!$rf ? () : @$rf), (!$rs ? () : @$rs),
  12742. @rfc2822_from, $rfc2822_sender, $sender))) {
  12743. $to_be_mangled = 0; # not for foreign 'Sender:' or 'From:'
  12744. do_log(5,"will not add disclaimer, sender not local");
  12745. }
  12746. }
  12747. } else { # defanging (not disclaiming)
  12748. # defanging and other mail mangling/munging only applies to
  12749. # incoming mail, i.e. for recipients matching local_domains_maps
  12750. $to_be_mangled = 0 if !$r->recip_is_local;
  12751. }
  12752. # store a boolean or a mangling name (defang, disclaimer, ...)
  12753. $r->mail_body_mangle($to_be_mangled) if $to_be_mangled;
  12754. ll(2) && do_log(2, "mangling %s: %s (was: %s), ".
  12755. "discl_allowed=%d, <%s> -> <%s>", $to_be_mangled ? 'YES' : 'NO',
  12756. $to_be_mangled, $orig_to_be_mangled, c('allow_disclaimers'),
  12757. $sender, $recip);
  12758. }
  12759. }
  12760. if ($penpals_score < 0) {
  12761. # only for logging and statistics purposes
  12762. my($do_tag2_nopp, $do_tag3_nopp, $do_kill_nopp) =
  12763. map { !$whitelisted &&
  12764. ($blacklisted ||
  12765. (defined($_) && $spam_level-$penpals_score >= $_) ) }
  12766. ($tag2_level, $tag3_level, $kill_level);
  12767. $do_tag2_nopp ||= $do_tag3_nopp;
  12768. my $which = $do_kill_nopp && !$do_kill ? 'kill'
  12769. : $do_tag3_nopp && !$do_tag3 ? 'tag3'
  12770. : $do_tag2_nopp && !$do_tag2 ? 'tag2' : undef;
  12771. if (defined $which) {
  12772. snmp_count("PenPalsSavedFrom\u$which") if $final_destiny==D_PASS;
  12773. do_log(2, "PenPalsSavedFrom%s %.3f%.3f%s, <%s> -> <%s>", "\u$which",
  12774. $spam_level-$penpals_score, $penpals_score,
  12775. ($final_destiny==D_PASS ? '' : ', but mail still blocked'),
  12776. $sender, $recip);
  12777. }
  12778. }
  12779. if ($final_destiny == D_PASS) {
  12780. # recipient wants this message, malicious or not
  12781. do_log(5, "final_destiny PASS, recip %s", $recip);
  12782. } else { # recipient does not want this content
  12783. do_log(5, "final_destiny %s, recip %s", $final_destiny, $recip);
  12784. # supply RFC 3463 enhanced status codes
  12785. my $status = setting_by_given_contents_category(
  12786. $blocking_ccat,
  12787. { CC_VIRUS, "554 5.7.0",
  12788. CC_BANNED, "554 5.7.0",
  12789. CC_UNCHECKED, "554 5.7.0",
  12790. CC_SPAM, "554 5.7.0",
  12791. CC_SPAMMY, "554 5.7.0",
  12792. CC_BADH.",2", "554 5.6.3", # nonencoded 8-bit character
  12793. CC_BADH, "554 5.6.0",
  12794. CC_OVERSIZED, "552 5.3.4",
  12795. CC_MTA, "550 5.3.5",
  12796. CC_CATCHALL, "554 5.7.0",
  12797. });
  12798. my($statoverride,$softfailed); $softfailed = '';
  12799. if ($status =~ /^[24]/) { # just in case
  12800. # keep unchanged
  12801. } elsif ($final_destiny == D_TEMPFAIL) {
  12802. $statoverride = '450'; # 5xx -> 450
  12803. } elsif (c('soft_bounce')) {
  12804. $statoverride = '450'; # 5xx -> 450
  12805. $softfailed = ' (soft_bounce)';
  12806. ll(5) && do_log(5, "soft_bounce: %s %s -> %s",
  12807. $final_destiny == D_DISCARD ? 'discard' : 'bounce',
  12808. $status, $statoverride);
  12809. } elsif ($final_destiny == D_DISCARD) {
  12810. $statoverride = '250'; # 5xx -> 250
  12811. }
  12812. if (defined $statoverride) {
  12813. my $code = substr($statoverride,0,1); local($1,$2);
  12814. $status =~ s{^\d(\d\d) \d(\.\d\.\d)}{$statoverride $code$2};
  12815. }
  12816. # get the custom smtp response reason text
  12817. my $smtp_reason = setting_by_given_contents_category(
  12818. $blocking_ccat, cr('smtp_reason_by_ccat'));
  12819. $smtp_reason = '' if !defined $smtp_reason;
  12820. if ($smtp_reason ne '') {
  12821. my(%mybuiltins) = %builtins; # make a local copy
  12822. $smtp_reason = expand(\$smtp_reason, \%mybuiltins);
  12823. $smtp_reason = !ref($smtp_reason) ? '' : $$smtp_reason;
  12824. chomp($smtp_reason); $smtp_reason = sanitize_str($smtp_reason,1);
  12825. $smtp_reason = substr($smtp_reason,0,100) . "..."
  12826. if length($smtp_reason) > 100+3;
  12827. }
  12828. my $response = sprintf("%s %s%s%s", $status,
  12829. ($final_destiny == D_PASS ? "Ok" :
  12830. $final_destiny == D_DISCARD ? "Ok, discarded" :
  12831. $final_destiny == D_REJECT ? "Reject" :
  12832. $final_destiny == D_BOUNCE ? "Bounce" :
  12833. $final_destiny == D_TEMPFAIL ? "Temporary failure" :
  12834. "Not ok ($final_destiny)" ),
  12835. $softfailed,
  12836. $smtp_reason eq '' ? '' : ', '.$smtp_reason);
  12837. ll(4) && do_log(4, "blocking ccat=%s, SMTP response: %s",
  12838. $blocking_ccat,$response);
  12839. $r->recip_smtp_response($response);
  12840. $r->recip_done(1); # fake a delivery (confirm delivery to a bit bucket)
  12841. # note that 5xx status rejects may later be converted to bounces
  12842. }
  12843. }
  12844. section_time($which_section);
  12845. $which_section = "quar+notif"; $t0_sect = Time::HiRes::time;
  12846. $zmq_obj->register_proc(2,0,'Q',$am_id) if $zmq_obj; # notify, quar
  12847. $snmp_db->register_proc(2,0,'Q',$am_id) if $snmp_db;
  12848. do_notify_and_quarantine($msginfo, $virus_dejavu);
  12849. # $which_section = "aux_quarantine";
  12850. # do_quarantine($msginfo, undef, ['archive-files'], 'local:archive/%m');
  12851. # do_quarantine($msginfo, undef, ['archive@localhost'], 'local:all-%m');
  12852. # do_quarantine($msginfo, undef, ['sender-quarantine'], 'local:user-%m'
  12853. # ) if lookup(0,$sender, ['user1@domain','user2@domain']);
  12854. # section_time($which_section);
  12855. $elapsed{'TimeElapsedQuarantineAndNotify'} = Time::HiRes::time - $t0_sect;
  12856. if (defined $hold && $hold ne '')
  12857. { do_log(-1, "NOTICE: HOLD reason: %s", $hold) }
  12858. # THIRD: now that we know what to do with it, do it! (deliver or bounce)
  12859. { # update Content*Msgs* counters
  12860. my $ccat_name =
  12861. $msginfo->setting_by_contents_category(\%ccat_display_names_major);
  12862. my $counter_name = 'Content'.$ccat_name.'Msgs';
  12863. snmp_count($counter_name);
  12864. if ($msginfo->originating) {
  12865. snmp_count($counter_name.'Originating');
  12866. }
  12867. if ($cnt_local > 0) {
  12868. my $d = $msginfo->originating ? 'Internal' : 'Inbound';
  12869. snmp_count($counter_name.$d);
  12870. }
  12871. if ($cnt_remote > 0) {
  12872. my $d = $msginfo->originating ? 'Outbound' : 'OpenRelay';
  12873. snmp_count($counter_name.$d);
  12874. }
  12875. }
  12876. # set $r->delivery_method according to forward_method_maps_by_ccat lookup
  12877. # or defaults
  12878. for my $r (@{$msginfo->per_recip_data}) {
  12879. next if defined($r->delivery_method);
  12880. my $fwd_map = $r->setting_by_contents_category(
  12881. cr('forward_method_maps_by_ccat'));
  12882. my $fwd_m;
  12883. $fwd_m = lookup2(0, $r->recip_addr, $fwd_map,
  12884. Label=>"forward_method") if ref $fwd_map;
  12885. $fwd_m = '' if !defined $fwd_m;
  12886. $r->delivery_method($fwd_m);
  12887. }
  12888. # a custom hook may change $r->delivery_method
  12889. if (ref $custom_object) {
  12890. $which_section = "custom-before_send";
  12891. eval {
  12892. $custom_object->before_send($conn,$msginfo);
  12893. update_current_log_level(); 1;
  12894. } or do {
  12895. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  12896. do_log(-1,"custom before_send error: %s", $eval_stat);
  12897. };
  12898. section_time($which_section);
  12899. }
  12900. if (ll(3)) { # log delivery method by recipients
  12901. my(%fwd_m_displ_log);
  12902. for my $r (@{$msginfo->per_recip_data}) {
  12903. my $fwd_m = $r->delivery_method;
  12904. my $fwd_m_displ =
  12905. !defined $fwd_m ? "undefined, mail will not be forwarded"
  12906. : map(ref eq 'ARRAY' ? '('.join(', ',@$_).')' : $_, $fwd_m);
  12907. if (!$fwd_m_displ_log{$fwd_m_displ}) {
  12908. $fwd_m_displ_log{$fwd_m_displ} = [ $r ];
  12909. } else {
  12910. push(@{$fwd_m_displ_log{$fwd_m_displ}}, $r);
  12911. }
  12912. }
  12913. for my $log_msg (sort keys %fwd_m_displ_log) {
  12914. do_log(3, "delivery method is %s, recips: %s", $log_msg,
  12915. join(', ', map($_->recip_addr, @{$fwd_m_displ_log{$log_msg}})));
  12916. }
  12917. }
  12918. my $bcc = $msginfo->setting_by_contents_category(cr('always_bcc_by_ccat'));
  12919. if (defined $bcc && $bcc ne '') {
  12920. my $recip_obj = Amavis::In::Message::PerRecip->new;
  12921. # leave recip_addr and recip_addr_smtp undefined!
  12922. $recip_obj->recip_addr_modified($bcc);
  12923. $recip_obj->recip_destiny(D_PASS);
  12924. $recip_obj->dsn_notify(['NEVER']);
  12925. $recip_obj->contents_category($msginfo->contents_category);
  12926. # $recip_obj->add_contents_category(CC_CLEAN,0);
  12927. $msginfo->per_recip_data([@{$msginfo->per_recip_data}, $recip_obj]);
  12928. do_log(2,"adding recipient - always_bcc: %s", $bcc);
  12929. }
  12930. my $hdr_edits = $msginfo->header_edits;
  12931. # to be delivered explicitly (not by an AM.PDP client)
  12932. if (grep(!$_->recip_done && $_->delivery_method ne '',
  12933. @{$msginfo->per_recip_data})) { # forwarding is needed
  12934. $which_section = "forwarding"; $t0_sect = Time::HiRes::time;
  12935. $zmq_obj->register_proc(2,0,'F',$am_id) if $zmq_obj;
  12936. $snmp_db->register_proc(2,0,'F',$am_id) if $snmp_db;
  12937. $hdr_edits = add_forwarding_header_edits_common(
  12938. $msginfo, $hdr_edits, $hold, $any_undecipherable,
  12939. $virus_presence_checked, $spam_presence_checked);
  12940. for (;;) { # do the delivery, in batches if necessary
  12941. my $r_hdr_edits = Amavis::Out::EditHeader->new; # per-recip edits set
  12942. $r_hdr_edits->inherit_header_edits($hdr_edits);
  12943. my $done_all;
  12944. my $recip_cl; # ref to a list of recip objects needing same mail edits
  12945. # prepare header section edits, clusterize
  12946. ($r_hdr_edits, $recip_cl, $done_all) =
  12947. add_forwarding_header_edits_per_recip(
  12948. $msginfo, $r_hdr_edits, $hold, $any_undecipherable,
  12949. $virus_presence_checked, $spam_presence_checked, undef);
  12950. last if !@$recip_cl;
  12951. $msginfo->header_edits($r_hdr_edits); # store edits for this batch
  12952. # preserve information that may be changed by prepare_modified_mail()
  12953. my($m_t,$m_tfn,$m_ofs) =
  12954. ($msginfo->mail_text, $msginfo->mail_text_fn, $msginfo->skip_bytes);
  12955. my(@m_dm) = map($_->delivery_method, @{$msginfo->per_recip_data});
  12956. # mail body mangling/defanging/sanitizing
  12957. my $body_modified =
  12958. prepare_modified_mail($msginfo,$hold,$any_undecipherable,$recip_cl);
  12959. # defanged_mime_entity have modified header edits, refetch just in case
  12960. $r_hdr_edits = $msginfo->header_edits;
  12961. if ($body_modified) {
  12962. my $resend_m = c('resend_method');
  12963. if (defined $resend_m && $resend_m ne '') {
  12964. $_->delivery_method($resend_m) for @{$msginfo->per_recip_data};
  12965. do_log(3,"mail body mangling in effect, resend_m: %s", $resend_m);
  12966. } else {
  12967. do_log(3,"mail body mangling in effect");
  12968. }
  12969. }
  12970. if (mail_dispatch($msginfo, 0, $dsn_per_recip_capable,
  12971. sub { my($r) = @_; grep($_ eq $r, @$recip_cl) })) {
  12972. $point_of_no_return = 1; # now past the point where mail was sent
  12973. }
  12974. # close and delete replacement file, if any
  12975. my $tmp_fh = $msginfo->mail_text; # replacement file, to be removed
  12976. if ($tmp_fh && !$tmp_fh->isa('MIME::Entity') && $tmp_fh ne $m_t) {
  12977. $tmp_fh->close or do_log(-1,"Can't close replacement: %s", $!);
  12978. if (debug_oneshot()) {
  12979. do_log(5, "defanging+debug, preserving %s",$msginfo->mail_text_fn);
  12980. } else {
  12981. unlink($msginfo->mail_text_fn)
  12982. or do_log(-1,"Can't remove %s: %s", $msginfo->mail_text_fn, $!);
  12983. }
  12984. }
  12985. # restore temporarily modified settings
  12986. $msginfo->mail_text($m_t); $msginfo->mail_text_fn($m_tfn);
  12987. $msginfo->skip_bytes($m_ofs);
  12988. $msginfo->mail_text_str(undef); $msginfo->body_start_pos(undef);
  12989. $_->delivery_method(shift @m_dm) for @{$msginfo->per_recip_data};
  12990. last if $done_all;
  12991. }
  12992. # turn on CC_MTA in case of MTA trouble (e.g, rejected by MTA on fwding)
  12993. for my $r (@{$msginfo->per_recip_data}) {
  12994. my $smtp_resp = $r->recip_smtp_response;
  12995. # skip successful deliveries and non- MTA-generated status codes
  12996. next if $smtp_resp =~ /^2/ || $r->recip_done != 2;
  12997. my $min_ccat = $smtp_resp =~ /^5/ ? 2 : $smtp_resp =~ /^4/ ? 1 : 0;
  12998. $r->add_contents_category(CC_MTA,$min_ccat);
  12999. $msginfo->add_contents_category(CC_MTA,$min_ccat);
  13000. my $blocking_ccat = sprintf("%d,%d", CC_MTA,$min_ccat);
  13001. $r->blocking_ccat($blocking_ccat);
  13002. $msginfo->blocking_ccat($blocking_ccat)
  13003. if !defined($msginfo->blocking_ccat);
  13004. my $final_destiny =
  13005. $r->setting_by_contents_category(cr('final_destiny_by_ccat'));
  13006. if ($final_destiny == D_PASS) {
  13007. $final_destiny = D_REJECT; # impossible to pass, change to reject
  13008. }
  13009. $r->recip_destiny($final_destiny);
  13010. local($1,$2);
  13011. if ($smtp_resp !~ /^5/) {
  13012. # keep unchanged
  13013. } elsif ($final_destiny == D_DISCARD) {
  13014. $smtp_resp =~ s{^\d(\d\d) \d(\.\d\.\d)}{250 2$2}; # 5xx -> 250
  13015. } elsif (c('soft_bounce')) {
  13016. do_log(5, "soft_bounce: (mta) %s -> 450", $smtp_resp);
  13017. $smtp_resp =~ s{^\d(\d\d) \d(\.\d\.\d)}{450 4$2}; # 5xx -> 450
  13018. }
  13019. my $smtp_reason = # get the custom smtp response reason text
  13020. $r->setting_by_contents_category(cr('smtp_reason_by_ccat'));
  13021. $smtp_reason = '' if !defined $smtp_reason;
  13022. if ($smtp_reason ne '') {
  13023. my(%mybuiltins) = %builtins; # make a local copy
  13024. $smtp_reason = expand(\$smtp_reason, \%mybuiltins);
  13025. $smtp_reason = !ref($smtp_reason) ? '' : $$smtp_reason;
  13026. chomp($smtp_reason); $smtp_reason = sanitize_str($smtp_reason,1);
  13027. $smtp_reason = substr($smtp_reason,0,100) . "..."
  13028. if length($smtp_reason) > 100+3;
  13029. }
  13030. $smtp_resp =~ /^(\d\d\d(?: \d\.\d\.\d)?)\s*(.*)\z/s;
  13031. my $dis = $final_destiny == D_DISCARD ? ' Discarded' : '';
  13032. $r->recip_smtp_response("$1$dis $smtp_reason, $2");
  13033. $r->recip_done(1); # fake a delivery (confirm delivery to a bit bucket)
  13034. # note that 5xx status rejects may later be converted to bounces
  13035. }
  13036. $msginfo->header_edits($hdr_edits); # restore original edits just in case
  13037. $elapsed{'TimeElapsedForwarding'} = Time::HiRes::time - $t0_sect;
  13038. }
  13039. # AM.PDP or AM.CL (milter)
  13040. if (grep(!$_->recip_done && $_->delivery_method eq '',
  13041. @{$msginfo->per_recip_data})) {
  13042. $which_section = "AM.PDP headers";
  13043. $hdr_edits = add_forwarding_header_edits_common(
  13044. $msginfo, $hdr_edits, $hold, $any_undecipherable,
  13045. $virus_presence_checked, $spam_presence_checked);
  13046. my $done_all;
  13047. my $recip_cl; # ref to a list of similar recip objects
  13048. ($hdr_edits, $recip_cl, $done_all) =
  13049. add_forwarding_header_edits_per_recip(
  13050. $msginfo, $hdr_edits, $hold, $any_undecipherable,
  13051. $virus_presence_checked, $spam_presence_checked, undef);
  13052. if (c('enable_dkim_signing')) { # add DKIM signatures
  13053. my(@signatures) = Amavis::DKIM::dkim_make_signatures($msginfo,0);
  13054. $msginfo->dkim_signatures_new(\@signatures) if @signatures;
  13055. for my $signature (@signatures) {
  13056. my $s = $signature->as_string;
  13057. local($1); $s =~ s{\015\012}{\n}gs; $s =~ s{\n+\z}{}gs;
  13058. $s =~ s/^((?:DKIM|DomainKey)-Signature):[ \t]*//si;
  13059. $hdr_edits->prepend_header($1, $s, 2);
  13060. }
  13061. }
  13062. $msginfo->header_edits($hdr_edits); # store edits (redundant)
  13063. if (@$recip_cl && !$done_all) {
  13064. do_log(-1, "AM.PDP: RECIPIENTS REQUIRE DIFFERENT HEADERS");
  13065. };
  13066. }
  13067. prolong_timer($which_section);
  13068. if (ref $custom_object) {
  13069. $which_section = "custom-after_send";
  13070. eval {
  13071. $custom_object->after_send($conn,$msginfo);
  13072. update_current_log_level(); 1;
  13073. } or do {
  13074. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  13075. do_log(-1,"custom after_send error: %s", $eval_stat);
  13076. };
  13077. section_time($which_section);
  13078. }
  13079. $which_section = "delivery-notification"; $t0_sect = Time::HiRes::time;
  13080. # generate a delivery status notification according to RFC 3462 & RFC 3464
  13081. my($notification,$suppressed) = delivery_status_notification(
  13082. $msginfo, $dsn_per_recip_capable, \%builtins,
  13083. [$sender], 'dsn', undef, undef);
  13084. my $ndn_needed;
  13085. ($smtp_resp, $exit_code, $ndn_needed) =
  13086. one_response_for_all($msginfo, $dsn_per_recip_capable,
  13087. $suppressed && !defined($notification) );
  13088. do_log(4, "notif=%s, suppressed=%d, ndn_needed=%s, exit=%s, %s",
  13089. defined $notification ? 'Y' : 'N', $suppressed,
  13090. $ndn_needed, $exit_code, $smtp_resp);
  13091. section_time('prepare-dsn');
  13092. if ($suppressed && !defined($notification)) {
  13093. $msginfo->dsn_sent(2); # would-be-bounced, but bounce was suppressed
  13094. } elsif (defined $notification) { # dsn needed, send delivery notification
  13095. mail_dispatch($notification, 'Dsn', 0);
  13096. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  13097. one_response_for_all($notification, 0); # check status
  13098. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # dsn successful?
  13099. $msginfo->dsn_sent(1); # mark the message as bounced
  13100. $point_of_no_return = 2; # now past the point where DSN was sent
  13101. } elsif ($n_smtp_resp =~ /^4/) {
  13102. die sprintf("temporarily unable to send DSN to <%s>: %s",
  13103. $msginfo->sender, $n_smtp_resp);
  13104. } else {
  13105. do_log(-1,"NOTICE: UNABLE TO SEND DSN to <%s>: %s",
  13106. $sender, $n_smtp_resp);
  13107. # # if dsn cannot be sent, try to send it to postmaster
  13108. # $notification->recips(['postmaster']);
  13109. # # attempt double bounce
  13110. # mail_dispatch($notification, 'Notif', 0);
  13111. }
  13112. # $notification->purge;
  13113. }
  13114. { # increment appropriate InMsgsStatus* SNMP counters and do some sanity
  13115. # checking along the way; also sets $msginfo->actions_performed
  13116. #
  13117. my($err, %which_counts);
  13118. my $orig = $msginfo->originating;
  13119. my $dsn_sent = $msginfo->dsn_sent; # 1=bounced, 2=suppressed
  13120. for my $r (@{$msginfo->per_recip_data}) {
  13121. my $which;
  13122. my $done = $r->recip_done; # 2=relayed to MTA, 1=faked deliv/quarant
  13123. my $dest = $r->recip_destiny;
  13124. my $resp_code = $smtp_resp; # per-msg status (one_response_for_all)
  13125. $resp_code = $r->recip_smtp_response if $dsn_per_recip_capable;
  13126. my $resp_class = substr($resp_code||'0', 0, 1);
  13127. if (!$done) {
  13128. $which = 'Accepted';
  13129. my $fwd_m = $r->delivery_method; # double-checking our sanity
  13130. if (defined $fwd_m && $fwd_m ne '') {
  13131. $err = "Recip not done, nonempty delivery method: $fwd_m";
  13132. }
  13133. } elsif ($resp_class !~ /^[245]\z/) {
  13134. $err = "Bad response code: $resp_code";
  13135. } elsif ($resp_class eq '4') {
  13136. $which = 'TempFailed';
  13137. } elsif ($resp_class eq '5' && $dest == D_REJECT) {
  13138. $which = 'Rejected';
  13139. } else { # $resp_class eq '2' || $resp_class eq '5' && $dest!=D_REJECT
  13140. # a 2xx SMTP response code is set both by internal Discard and
  13141. # by a genuine successful delivery. To distinguish between the two
  13142. # we need to check $r->recip_destiny
  13143. if ($done == 2) { # successful genuine forwarding
  13144. $which = $r->recip_tagged ? 'RelayedTagged' : 'RelayedUntagged';
  13145. $err = "Forwarded, but destiny not D_PASS? ($dest)"
  13146. if $dest != D_PASS;
  13147. $err = "Forwarded, but status not 2xx? ($resp_code)"
  13148. if $resp_class ne '2';
  13149. } elsif ($dest == D_DISCARD) { # forwarded to a bit bucket
  13150. $which = 'Discarded';
  13151. } elsif ( $dest == D_BOUNCE ||
  13152. ($dest == D_REJECT && $resp_class eq '2') ) {
  13153. if ($dsn_sent && $dsn_sent == 1) {
  13154. $which = 'Bounced'; # genuine bounce (DSN) sent
  13155. } elsif ($dsn_sent) {
  13156. $which = 'NoBounce'; # bounce suppressed
  13157. } else { # sanity check
  13158. $err = "To be bounced, but DSN was neither sent nor suppressed?";
  13159. }
  13160. } elsif ($dest == D_REJECT) {
  13161. $which = 'Rejected';
  13162. $err = "Rejected, but status not 5xx? ($resp_code)"
  13163. if $resp_class ne '5';
  13164. } else { # sanity check
  13165. $err = "Recip forwarding suppressed but not DISCARD?";
  13166. }
  13167. }
  13168. $which = 'Unknown' if !defined $which;
  13169. $which_counts{$which}++; # counts status without a direction
  13170. $which_counts{'Relayed'}++ if $which eq 'RelayedTagged' ||
  13171. $which eq 'RelayedUntagged';
  13172. my $islocal = $r->recip_is_local;
  13173. if ($orig) {
  13174. if ($islocal) { $which_counts{$which.'Internal'}++ }
  13175. else { $which_counts{$which.'Outbound'}++ }
  13176. $which_counts{$which.'Originating'}++;
  13177. } else {
  13178. if ($islocal) { $which_counts{$which.'Inbound'}++ }
  13179. else { $which_counts{$which.'OpenRelay'}++ }
  13180. }
  13181. do_log(0, "unexpected status/result, please verify: %s, %s",
  13182. $err, $r->recip_addr_smtp) if defined $err;
  13183. }
  13184. my @which_list = sort keys %which_counts;
  13185. # prefer this status in the list first, before a 'Quarantined' entry;
  13186. # ignore a plain status name without mail direction to reduce clutter;
  13187. # ignore Originating, as it is always paired with Internal or Outbound
  13188. $msginfo->actions_performed([]) if !$msginfo->actions_performed;
  13189. unshift(@{$msginfo->actions_performed},
  13190. map(/^RelayedUntagged(.*)/ ? "Relayed$1" : $_, # short log name
  13191. grep(/(?:Inbound|Internal|Outbound|OpenRelay)\z/, @which_list)));
  13192. snmp_count('InMsgsStatus'.$_) for @which_list;
  13193. ll(3) && do_log(3, 'status counters: InMsgsStatus{%s}',
  13194. join(',', @which_list));
  13195. }
  13196. prolong_timer($which_section);
  13197. $elapsed{'TimeElapsedDSN'} = Time::HiRes::time - $t0_sect;
  13198. # generate customized log report at log level 0 - this is usually the
  13199. # only log entry interesting to administrators during normal operation
  13200. $which_section = 'main_log_entry';
  13201. my(%mybuiltins) = %builtins; # make a local copy
  13202. { # do a per-message log entry
  13203. # macro %T has overloaded semantics, ugly
  13204. $mybuiltins{'T'} = $mybuiltins{'TESTSSCORES'};
  13205. my($y,$n,$f) = delivery_short_report($msginfo);
  13206. @mybuiltins{'D','O','N'} = ($y,$n,$f);
  13207. if (ll(0)) {
  13208. my $strr = expand(cr('log_templ'), \%mybuiltins);
  13209. for my $logline (split(/[ \t]*\n/, $$strr)) {
  13210. do_log(0, '%s', $logline) if $logline ne '';
  13211. }
  13212. }
  13213. }
  13214. if (c('log_recip_templ') ne '') { # do per-recipient log entries
  13215. # redefine some macros with a by-recipient semantics
  13216. my $j = 0;
  13217. for my $r (@{$msginfo->per_recip_data}) {
  13218. # recipient counter in macro %. may indicate to the template
  13219. # that a per-recipient expansion semantics is expected
  13220. $j++; $mybuiltins{'.'} = sprintf("%d",$j);
  13221. my $recip = $r->recip_addr;
  13222. my $qrecip_addr = scalar(qquote_rfc2821_local($recip));
  13223. my $remote_mta = $r->recip_remote_mta;
  13224. my $smtp_resp = $r->recip_smtp_response;
  13225. $mybuiltins{'remote_mta'} = $remote_mta;
  13226. $mybuiltins{'smtp_response'} = $smtp_resp;
  13227. $mybuiltins{'remote_mta_smtp_response'} =
  13228. $r->recip_remote_mta_smtp_response;
  13229. $mybuiltins{'D'} = $mybuiltins{'O'} = $mybuiltins{'N'} = undef;
  13230. if ($r->recip_destiny==D_PASS &&($smtp_resp=~/^2/ || !$r->recip_done)){
  13231. $mybuiltins{'D'} = $qrecip_addr;
  13232. } else {
  13233. $mybuiltins{'O'} = $qrecip_addr;
  13234. $mybuiltins{'N'} = sprintf("%s:%s\n %s", $qrecip_addr,
  13235. ($remote_mta eq '' ?'' :" [$remote_mta] said:"), $smtp_resp);
  13236. }
  13237. my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
  13238. my $b_chopped = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
  13239. s/[ \t]{6,}/ ... /g for @b;
  13240. $mybuiltins{'banned_parts'} = \@b; # list of banned parts
  13241. $mybuiltins{'F'} = $r->banning_reason_short; # just one name & comment
  13242. $mybuiltins{'banning_rule_comment'} =
  13243. !defined($r->banning_rule_comment) ? undef
  13244. : unique_ref($r->banning_rule_comment);
  13245. $mybuiltins{'banning_rule_rhs'} =
  13246. !defined($r->banning_rule_rhs) ? undef
  13247. : unique_ref($r->banning_rule_rhs);
  13248. my $dn = $r->dsn_notify;
  13249. $mybuiltins{'dsn_notify'} =
  13250. uc(join(',', $sender eq '' ? 'NEVER' : !$dn ? 'FAILURE' : @$dn));
  13251. my($tag_level,$tag2_level,$kill_level);
  13252. if (!$r->bypass_spam_checks) {
  13253. $tag_level = lookup2(0,$recip, ca('spam_tag_level_maps'));
  13254. $tag2_level = lookup2(0,$recip, ca('spam_tag2_level_maps'));
  13255. $kill_level = lookup2(0,$recip, ca('spam_kill_level_maps'));
  13256. }
  13257. my $is_local = $r->recip_is_local;
  13258. my $do_tag = $r->is_in_contents_category(CC_CLEAN,1);
  13259. my $do_tag2 = $r->is_in_contents_category(CC_SPAMMY);
  13260. my $do_kill = $r->is_in_contents_category(CC_SPAM);
  13261. for ($do_tag,$do_tag2,$do_kill) { $_ = $_ ? 'Y' : '0' } # normalize
  13262. for ($is_local) { $_ = $_ ? 'L' : '0' } # normalize
  13263. for ($tag_level,$tag2_level,$kill_level) { $_ = 'x' if !defined($_) }
  13264. $mybuiltins{'R'} = $recip;
  13265. $mybuiltins{'c'} = $mybuiltins{'SCORE'} = $mybuiltins{'STARS'} =
  13266. sub { macro_score($msginfo, $j-1, @_) }; # info on one recipient
  13267. $mybuiltins{'T'} = $mybuiltins{'TESTSSCORES'} = $mybuiltins{'TESTS'} =
  13268. sub { macro_tests($msginfo, $j-1, @_)}; # info on one recipient
  13269. $mybuiltins{'tag_level'} = # replacement for deprecated %3
  13270. !defined($tag_level) ? '-' : 0+sprintf("%.3f",$tag_level);
  13271. $mybuiltins{'tag2_level'} = $mybuiltins{'REQD'} = # replacement for %4
  13272. !defined($tag2_level) ? '-' : 0+sprintf("%.3f",$tag2_level);
  13273. $mybuiltins{'kill_level'} = # replacement for deprecated %5
  13274. !defined($kill_level) ? '-' : 0+sprintf("%.3f",$kill_level);
  13275. @mybuiltins{('0','1','2','k')} = ($is_local,$do_tag,$do_tag2,$do_kill);
  13276. # macros %3, %4, %5 are deprecated, replaced by tag/tag2/kill_level
  13277. @mybuiltins{('3','4','5')} = ($tag_level,$tag2_level,$kill_level);
  13278. $mybuiltins{'ccat'} =
  13279. sub {
  13280. my($name,$attr,$which) = @_;
  13281. $attr = lc($attr); # name | major | minor | <empty>
  13282. # | is_blocking | is_nonblocking
  13283. # | is_blocked_by_nonmain
  13284. $which = lc($which); # main | blocking | auto
  13285. my $result = ''; my $blocking_ccat = $r->blocking_ccat;
  13286. if ($attr eq 'is_blocking') {
  13287. $result = defined($blocking_ccat) ? 1 : '';
  13288. } elsif ($attr eq 'is_nonblocking') {
  13289. $result = !defined($blocking_ccat) ? 1 : '';
  13290. } elsif ($attr eq 'is_blocked_by_nonmain') {
  13291. if (defined($blocking_ccat)) {
  13292. my $aref = $r->contents_category;
  13293. $result = 1 if ref($aref) && @$aref > 0
  13294. && $blocking_ccat ne $aref->[0];
  13295. }
  13296. } elsif ($attr eq 'name') {
  13297. $result =
  13298. $which eq 'main' ?
  13299. $r->setting_by_main_contents_category(\%ccat_display_names)
  13300. : $which eq 'blocking' ?
  13301. $r->setting_by_blocking_contents_category(
  13302. \%ccat_display_names)
  13303. : $r->setting_by_contents_category( \%ccat_display_names);
  13304. } else { # attr = major, minor, or anything else returns a pair
  13305. my($maj,$min) = ccat_split(
  13306. ($which eq 'blocking' ||
  13307. $which ne 'main' && defined $blocking_ccat)
  13308. ? $blocking_ccat : $r->contents_category);
  13309. $result = $attr eq 'major' ? $maj
  13310. : $attr eq 'minor' ? sprintf("%d",$min)
  13311. : sprintf("(%d,%d)",$maj,$min);
  13312. }
  13313. $result;
  13314. };
  13315. my $strr = expand(cr('log_recip_templ'), \%mybuiltins);
  13316. for my $logline (split(/[ \t]*\n/, $$strr)) {
  13317. do_log(0, "%s", $logline) if $logline ne '';
  13318. }
  13319. }
  13320. }
  13321. section_time($which_section);
  13322. prolong_timer($which_section);
  13323. if (defined $os_fingerprint && $os_fingerprint ne '') {
  13324. # log and collect statistics on contents type vs. OS
  13325. my $spam_ham_thd = 2.0; # reasonable threshold guesstimate
  13326. local($1); my $os_short; # extract operating system name when avail.
  13327. $os_short = $1 if $os_fingerprint =~ /^([^,([]*)/;
  13328. $os_short = $1 if $os_short =~ /^[ \t,-]*(.*?)[ \t,-]*\z/;
  13329. my $snmp_counter_name;
  13330. if ($os_short ne '') {
  13331. $os_short = $1 if $os_short =~ /^(Windows [^ ]+|[^ ]+)/; # drop vers.
  13332. $os_short =~ s{[^0-9A-Za-z:./_+-]}{-}g; $os_short =~ s{\.}{,}g;
  13333. $snmp_counter_name = $msginfo->setting_by_contents_category(
  13334. { CC_VIRUS,'virus', CC_BANNED,'banned',
  13335. CC_SPAM,'spam', CC_SPAMMY,'spammy', CC_CATCHALL,'clean' });
  13336. if ($snmp_counter_name eq 'clean') {
  13337. $snmp_counter_name = $max_spam_level <= $spam_ham_thd ?'ham' : undef;
  13338. }
  13339. if (defined $snmp_counter_name) {
  13340. snmp_count("$snmp_counter_name.byOS.$os_short");
  13341. if ($snmp_counter_name eq 'ham' &&
  13342. $os_fingerprint =~ /^Windows XP(?![^(]*\b2000 SP)/) {
  13343. do_log(3, 'Ham from Windows XP? Most weird! %s [%s] score=%.3f',
  13344. $mail_id||'', $cl_ip, $max_spam_level);
  13345. }
  13346. }
  13347. }
  13348. do_log(2, "OS_fingerprint: %s %s %s.%s - %s",
  13349. $msginfo->client_addr, $max_spam_level,
  13350. defined $snmp_counter_name ? $snmp_counter_name : 'x',
  13351. $os_short, $os_fingerprint);
  13352. }
  13353. if ($sql_storage && defined $msginfo->mail_id) {
  13354. # save final information to SQL (if enabled)
  13355. $which_section = 'sql-update';
  13356. my $ds = $msginfo->dsn_sent;
  13357. $ds = !$ds ? 'N' : $ds==1 ? 'Y' : $ds==2 ? 'q' : '?';
  13358. for (my $attempt=5; $attempt>0; ) { # sanity limit on retries
  13359. if ($sql_storage->save_info_final($msginfo,$ds)) {
  13360. last;
  13361. } elsif (--$attempt <= 0) {
  13362. do_log(-2,"ERROR sql_storage: too many retries ".
  13363. "on storing final, info not saved");
  13364. } else {
  13365. do_log(2,"sql_storage: retrying on final, %d attempts remain",
  13366. $attempt);
  13367. sleep(int(1+rand(3))); # can't mix Time::HiRes::sleep with alarm
  13368. }
  13369. };
  13370. section_time($which_section);
  13371. }
  13372. if (ll(2)) { # log SpamAssassin timing report if available
  13373. my $sa_tim = $msginfo->supplementary_info('TIMING');
  13374. do_log(2, "TIMING-SA %s", $sa_tim) if defined($sa_tim) && $sa_tim ne '';
  13375. }
  13376. if ($snmp_db || $zmq_obj) {
  13377. $which_section = 'update_snmp';
  13378. my($log_lines, $log_entries_by_level_ref,
  13379. $log_retries, $log_status_counts_ref) = collect_log_stats();
  13380. snmp_count( ['LogLines', $log_lines, 'C64'] );
  13381. my $log_entries_all_cnt = 0;
  13382. for my $level_str (keys %$log_entries_by_level_ref) {
  13383. my $level = 0+$level_str;
  13384. my $cnt = $log_entries_by_level_ref->{$level_str};
  13385. $log_entries_all_cnt += $cnt;
  13386. # snmp_count( ['LogEntriesEmerg', $cnt, 'C64'] ); # not in use
  13387. # snmp_count( ['LogEntriesAlert', $cnt, 'C64'] ); # not in use
  13388. snmp_count( ['LogEntriesCrit', $cnt, 'C64'] ) if $level <= -3;
  13389. snmp_count( ['LogEntriesErr', $cnt, 'C64'] ) if $level <= -2;
  13390. snmp_count( ['LogEntriesWarning', $cnt, 'C64'] ) if $level <= -1;
  13391. snmp_count( ['LogEntriesNotice', $cnt, 'C64'] ) if $level <= 0;
  13392. snmp_count( ['LogEntriesInfo', $cnt, 'C64'] ) if $level <= 1;
  13393. snmp_count( ['LogEntriesDebug', $cnt, 'C64'] );
  13394. if ($level < 0) { $level_str = "0" }
  13395. elsif ($level > 5) { $level_str = "5" }
  13396. snmp_count( ['LogEntriesLevel'.$level_str, $cnt, 'C64'] );
  13397. }
  13398. snmp_count( ['LogEntries', $log_entries_all_cnt, 'C64'] );
  13399. if ($log_retries > 0) {
  13400. snmp_count( ['LogRetries', $log_retries, 'C64'] );
  13401. do_log(3,"Syslog retries: %d x %s", $log_status_counts_ref->{$_}, $_)
  13402. for (keys %$log_status_counts_ref);
  13403. }
  13404. $elapsed{'TimeElapsedSending'} += # merge similar timing entries
  13405. delete $elapsed{$_} for ('TimeElapsedQuarantineAndNotify',
  13406. 'TimeElapsedForwarding', 'TimeElapsedDSN');
  13407. snmp_count( ['entropy',0,'STR'] );
  13408. $elapsed{'TimeElapsedTotal'} = Time::HiRes::time - $msginfo->rx_time;
  13409. # Will end up as SNMPv2-TC TimeInterval (INTEGER), units of 0.01 seconds,
  13410. # but we keep it in milliseconds in the bdb database!
  13411. # Note also the use of C32 instead of INT, we want cumulative time.
  13412. snmp_count([$_, int(1000*$elapsed{$_}+0.5), 'C32']) for (keys %elapsed);
  13413. $snmp_db->update_snmp_variables if $snmp_db;
  13414. $zmq_obj->update_snmp_variables if $zmq_obj;
  13415. section_time($which_section);
  13416. }
  13417. if (ref $custom_object) {
  13418. $which_section = "custom-mail_done";
  13419. eval {
  13420. $custom_object->mail_done($conn,$msginfo);
  13421. update_current_log_level(); 1;
  13422. } or do {
  13423. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  13424. do_log(-1,"custom mail_done error: %s", $eval_stat);
  13425. };
  13426. section_time($which_section);
  13427. }
  13428. $which_section = 'finishing';
  13429. 1;
  13430. } or do {
  13431. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  13432. $preserve_evidence = 1 if $allow_preserving_evidence;
  13433. my $msg = "$which_section FAILED: $eval_stat";
  13434. if ($point_of_no_return) {
  13435. do_log(-2, "TROUBLE in check_mail, but must continue (%s): %s",
  13436. $point_of_no_return,$msg);
  13437. } else {
  13438. do_log(-2, "TROUBLE in check_mail: %s", $msg);
  13439. $smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg";
  13440. $exit_code = EX_TEMPFAIL;
  13441. for my $r (@{$msginfo->per_recip_data})
  13442. { $r->recip_smtp_response($smtp_resp); $r->recip_done(1) }
  13443. }
  13444. };
  13445. # if ($hold ne '') {
  13446. # do_log(-1, "NOTICE: Evidence is to be preserved: %s", $hold);
  13447. # $preserve_evidence = 1 if $allow_preserving_evidence;
  13448. # }
  13449. if (!$preserve_evidence && debug_oneshot()) {
  13450. do_log(0, "DEBUG_ONESHOT CAUSES EVIDENCE TO BE PRESERVED");
  13451. $preserve_evidence = 1; # regardless of $allow_preserving_evidence
  13452. }
  13453. $zmq_obj->register_proc(1,0,'.') if $zmq_obj; # content checking done
  13454. $snmp_db->register_proc(1,0,'.') if $snmp_db;
  13455. do_log(-1, "signal: %s", join(', ',keys %got_signals)) if %got_signals;
  13456. undef $MSGINFO; # release global reference
  13457. ($smtp_resp, $exit_code, $preserve_evidence);
  13458. }
  13459. # Ensure we have $msginfo->$entity defined when we expect we'll need it,
  13460. #
  13461. sub ensure_mime_entity($) {
  13462. my($msginfo) = @_;
  13463. my($ent,$mime_err);
  13464. if (!defined($msginfo->mime_entity)) {
  13465. my $msg = $msginfo->mail_text;
  13466. if (IO::File->VERSION >= 1.10) { # see mime_decode() for explanation
  13467. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  13468. $msg = $msg_str_ref if ref $msg_str_ref;
  13469. }
  13470. ($ent,$mime_err) = mime_decode($msg, $msginfo->mail_tempdir,
  13471. $msginfo->parts_root);
  13472. $msginfo->mime_entity($ent);
  13473. prolong_timer('mime_decode');
  13474. }
  13475. $mime_err;
  13476. }
  13477. # Check if a message is a bounce, and if it is, try to obtain essential
  13478. # information from a header section of an attached original message,
  13479. # primarily the Message-ID.
  13480. #
  13481. sub inspect_a_bounce_message($) {
  13482. my($msginfo) = @_;
  13483. my(%header_field,$bounce_type); my $is_true_bounce = 0;
  13484. my $parts_root = $msginfo->parts_root;
  13485. if (!defined($parts_root)) {
  13486. do_log(5, 'inspect_dsn: no parts root');
  13487. } else {
  13488. my $sender = $msginfo->sender;
  13489. my $structure_type = '?';
  13490. my $top_main; my $top = $parts_root->children;
  13491. for my $e (!$top ? () : @$top) {
  13492. # take a main message component, ignoring preamble/epilogue MIME parts
  13493. # and pseudo components such as a fabricated 'MAIL' (i.e. a copy of
  13494. # entire message for the benefit of some virus scanners)
  13495. my $name = $e->name_declared;
  13496. next if !defined($e->type_declared) && defined($name) &&
  13497. ($name eq 'preamble' || $name eq 'epilogue');
  13498. next if $e->type_short eq 'MAIL' &&
  13499. lc($e->type_declared) eq 'message/rfc822';
  13500. $top_main = $e; last;
  13501. }
  13502. my(@parts); my $fname_ind; my $plaintext = 0;
  13503. if (defined $top_main) { # one level only
  13504. my $ch = $top_main->children;
  13505. @parts = ($top_main, !$ch ? () : @$ch);
  13506. }
  13507. my(@t) =
  13508. map { my $t = $_->type_declared; lc(ref $t ? $t->[0] : $t) } @parts;
  13509. ll(5) && do_log(5, "inspect_dsn: parts: %s", join(", ",@t));
  13510. my $fm = $msginfo->rfc2822_from;
  13511. my(@rfc2822_from) = !defined $fm ? () : ref $fm ? @$fm : $fm;
  13512. my $p0_report_type;
  13513. $p0_report_type = $parts[0]->report_type if @parts;
  13514. $p0_report_type = lc $p0_report_type if defined $p0_report_type;
  13515. if ( @parts >= 2 && @parts <= 4 &&
  13516. $t[0] eq 'multipart/report' &&
  13517. ( $t[2] eq 'message/delivery-status' || # RFC 3464
  13518. $t[2] eq 'message/global-delivery-status' || # RFC 5337
  13519. $t[2] eq 'message/disposition-notification' || # RFC 3798
  13520. $t[2] eq 'message/global-disposition-notification' || # RFC 5337
  13521. $t[2] eq 'message/feedback-report' # RFC 5965
  13522. ) &&
  13523. defined $p0_report_type && $t[2] eq 'message/'.$p0_report_type &&
  13524. ( $t[3] eq 'text/rfc822-headers' || $t[3] eq 'message/rfc822' ||
  13525. $t[3] eq 'message/rfc822-headers' || # nonstandard
  13526. $t[3] eq 'message/partial' ) # nonstandard
  13527. )
  13528. { # standard DSN or MDN or feedback-report
  13529. $bounce_type = $t[2] eq 'message/disposition-notification' ? 'MDN'
  13530. : $t[2] eq 'message/global-disposition-notification' ? 'MDN'
  13531. : $t[2] eq 'message/feedback-report' ? 'ARF' : 'DSN';
  13532. $structure_type = 'standard ' . $bounce_type;
  13533. $fname_ind = $#parts; $is_true_bounce = 1;
  13534. } elsif ( @parts == 5 &&
  13535. $t[0] eq 'multipart/report' &&
  13536. $t[-2] eq 'message/delivery-status' &&
  13537. defined $p0_report_type && $t[-2] eq 'message/'.$p0_report_type &&
  13538. ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' )
  13539. ) { # almost standard DSN, has two leading plain text parts
  13540. $bounce_type = 'DSN'; # BorderWare Security Platform
  13541. $structure_type = 'standard ' . $bounce_type;
  13542. $fname_ind = $#parts; $is_true_bounce = 1;
  13543. } elsif ( @parts >= 2 && @parts <= 4 &&
  13544. $t[0] eq 'multipart/report' &&
  13545. $t[2] eq 'message/delivery-status' &&
  13546. defined $p0_report_type && $t[2] eq 'message/'.$p0_report_type &&
  13547. $t[3] eq 'text/plain' ) {
  13548. # nonstandard DSN, missing header, unless it is stashed in text/plain
  13549. $fname_ind = 3; $structure_type = 'nostandard DSN-plain';
  13550. $plaintext = 1; $bounce_type = 'DSN';
  13551. } elsif (@parts >= 3 && @parts <= 4 && # a root with 2 or 3 leaves
  13552. $t[0] eq 'multipart/report' &&
  13553. defined $p0_report_type && $p0_report_type eq 'delivery-status' &&
  13554. ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' )) {
  13555. # not quite std. DSN (missing message/delivery-status), but recognizable
  13556. $fname_ind = -1; $is_true_bounce = 1; $bounce_type = 'DSN';
  13557. $structure_type = 'DSN, missing delivery-status part';
  13558. } elsif (@parts >= 3 && @parts <= 5 &&
  13559. $t[0] eq 'multipart/mixed' &&
  13560. ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' ||
  13561. $t[-1] eq 'message/rfc822-headers') && # nonstandard - Gordano M.S.
  13562. ( $rfc2822_from[0] =~ /^MAILER-DAEMON(?:\@|\z)/si ||
  13563. $msginfo->get_header_field_body('subject') =~
  13564. /\b(?:Delivery Failure Notification|failure notice)\b/
  13565. ) ) {
  13566. # qmail, msn?, mailman, C/R
  13567. $fname_ind = -1;
  13568. $structure_type = 'multipart/mixed(' . $msginfo->is_bulk . ')';
  13569. } elsif ( $msginfo->is_auto && $sender eq '' &&
  13570. # notify@yahoogroups.com notify@yahoogroupes.fr
  13571. $rfc2822_from[0] =~ /^notify\@yahoo/si &&
  13572. @parts >= 3 && @parts <= 5 &&
  13573. $t[0] eq 'multipart/mixed' &&
  13574. ( $t[-1] eq 'text/rfc822-headers' || $t[-1] eq 'message/rfc822' )
  13575. ) {
  13576. $fname_ind = -1;
  13577. $structure_type = 'multipart/mixed(yahoogroups)';
  13578. } elsif ( $msginfo->is_auto && $sender eq '' &&
  13579. @parts == 1 && $t[0] ne 'multipart/report' &&
  13580. $rfc2822_from[0] =~ /^(?:MAILER-DAEMON|postmaster)(?:\@|\z)/si
  13581. ) {
  13582. # nonstructured, possibly a non-standard bounce (qmail, gmail.com, ...)
  13583. $fname_ind = 0; $plaintext = 1;
  13584. $structure_type = 'nonstructured(' . $msginfo->is_auto . ')';
  13585. # } elsif ( $msginfo->is_auto && $sender eq '' &&
  13586. # ( grep($_->recip_addr eq 'xxx@example.com', # victim
  13587. # @{$msginfo->per_recip_data}) ) ) {
  13588. # # nonstructured, possibly a non-standard bounce
  13589. # $fname_ind = 0; $plaintext = 1; $is_true_bounce = 1;
  13590. # $structure_type = 'nonstructured, unknown';
  13591. # $bounce_type = 'INFO';
  13592. # } elsif (@parts == 3 &&
  13593. # $t[0] eq 'multipart/mixed' &&
  13594. # $t[-1] eq 'application/octet-stream' &&
  13595. # $parts[-1]->name_declared =~ /\.eml\z/) {
  13596. # # MDaemon; too permissive! test for postmaster or mailer-daemon ?
  13597. # $fname_ind = -1;
  13598. # $structure_type = 'multipart/mixed with binary .eml';
  13599. # } elsif ( $msginfo->is_auto && @parts == 2 &&
  13600. # $t[0] eq 'multipart/mixed' && $t[1] eq 'text/plain' ) {
  13601. # # nonstructured, possibly a broken bounce
  13602. # $fname_ind = 1; $plaintext = 1;
  13603. # $structure_type = $t[0] .' with '. $t[1] .'(' . $msginfo->is_auto .')';
  13604. # } elsif ( $msginfo->is_auto && @parts == 3 &&
  13605. # $t[0] eq 'multipart/alternative' &&
  13606. # $t[1] eq 'text/plain' && $t[2] eq 'text/html' ) {
  13607. # # text/plain+text/html, possibly a challenge CR message
  13608. # $fname_ind = 1; $plaintext = 1;
  13609. # $structure_type = $t[0] .' with '. $t[1] .'(' . $msginfo->is_auto .')';
  13610. }
  13611. if (defined $fname_ind && defined $parts[$fname_ind]) {
  13612. # we probably have a header section from original mail, scan it
  13613. $fname_ind = $#parts if $fname_ind == -1;
  13614. my $fname = $parts[$fname_ind]->full_name;
  13615. ll(5) && do_log(5,'inspect_dsn: struct: "%s", basenm(%s): %s, fname: %s',
  13616. $structure_type, $fname_ind, $parts[$fname_ind]->base_name, $fname);
  13617. if (defined $fname) {
  13618. my(%collectable_header_fields);
  13619. $collectable_header_fields{lc($_)} = 1
  13620. for qw(From To Return-Path Message-ID Date Received Subject
  13621. MIME-Version Content-Type);
  13622. my $fh = IO::File->new;
  13623. $fh->open($fname,'<') or die "Can't open file $fname: $!";
  13624. binmode($fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  13625. my $have_header_fields_cnt = 0; my $nonheader_cnt = 0;
  13626. my($curr_head,$ln); my $nr = 0; my $eof = 0; local($1,$2);
  13627. my $line_limit = $plaintext ? 200 : 1000;
  13628. for (;;) {
  13629. if ($eof) {
  13630. $ln = "\n"; # fake a missing header/body separator line
  13631. } else {
  13632. $! = 0; $ln = $fh->getline;
  13633. if (!defined($ln)) {
  13634. $eof = 1; $ln = "\n";
  13635. $! == 0 or # returning EBADF at EOF is a perl bug
  13636. $! == EBADF ? do_log(1,"Error reading mail header section: $!")
  13637. : die "Error reading mail header section: $!";
  13638. }
  13639. }
  13640. last if ++$nr > $line_limit; # safety measure
  13641. if ($ln =~ /^[ \t]/) { # folded
  13642. $curr_head .= $ln if length($curr_head) < 2000; # safety measure
  13643. } else { # a new header field, process previous if any
  13644. if (defined $curr_head) {
  13645. $curr_head =~ s/^[> ]+// if $plaintext;
  13646. # be more conservative on accepted h.f.name than RFC 5322 allows
  13647. # the '_' and '.' are quite rare, digits even rarer;
  13648. # the longest non-X h.f.name is content-transfer-encoding (25)
  13649. # the longest h.f.names in the wild are 59 chars, largest ever 77
  13650. if ($curr_head !~ /^([a-zA-Z0-9._-]{1,60})[ \t]*:(.*)\z/s) {
  13651. $nonheader_cnt++;
  13652. } else {
  13653. my $hfname = lc($1);
  13654. if ($collectable_header_fields{$hfname}) {
  13655. $have_header_fields_cnt++ if !exists $header_field{$hfname};
  13656. $header_field{$hfname} = $2;
  13657. }
  13658. }
  13659. }
  13660. $curr_head = $ln;
  13661. if (!$plaintext) {
  13662. last if $ln eq "\n" || substr($ln,0,2) eq '--';
  13663. } elsif ($ln =~ /^\s*$/ || substr($ln,0,2) eq '--') {
  13664. if (exists $header_field{'from'} &&
  13665. $have_header_fields_cnt >= 4 && $nonheader_cnt <= 1) {
  13666. last;
  13667. } else { # reset, hope for the next paragraph to be a header
  13668. $have_header_fields_cnt = 0; $nonheader_cnt = 0;
  13669. %header_field = (); $curr_head = undef;
  13670. }
  13671. }
  13672. }
  13673. }
  13674. defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug
  13675. $! == EBADF ? do_log(1,"Error reading from %s: %s", $fname,$!)
  13676. : die "Error reading from $fname: $!";
  13677. $fh->close or die "Error closing $fname: $!";
  13678. my $thd = exists $header_field{'message-id'} ? 3 : 5;
  13679. $is_true_bounce = 1 if exists $header_field{'from'} &&
  13680. $have_header_fields_cnt >= $thd;
  13681. if ($is_true_bounce) {
  13682. ll(5) && do_log(5, "inspect_dsn: plain=%s, got %d: %s",
  13683. $plaintext?"Y":"N", scalar(keys %header_field),
  13684. join(", ", sort keys %header_field));
  13685. for (@header_field{keys %header_field})
  13686. { s/\n(?=[ \t])//gs; s/^[ \t]+//; s/[ \t\n]+\z// }
  13687. if (!defined($header_field{'message-id'}) &&
  13688. $have_header_fields_cnt >= 5 && $nonheader_cnt <= 1) {
  13689. $header_field{'message-id'} = ''; # fake: defined but empty
  13690. do_log(5, "inspect_dsn: a header section with no Message-ID");
  13691. } elsif (defined($header_field{'message-id'})) {
  13692. $header_field{'message-id'} =
  13693. (parse_message_id($header_field{'message-id'}))[0]
  13694. if defined $header_field{'message-id'};
  13695. }
  13696. }
  13697. section_time("inspect_dsn");
  13698. }
  13699. }
  13700. $bounce_type = 'bounce' if !defined $bounce_type;
  13701. if ($is_true_bounce) {
  13702. do_log(3, 'inspect_dsn: is a %s, struct: "%s", part(%s/%d), <%s>',
  13703. $bounce_type, $structure_type,
  13704. !defined($fname_ind) ? '-' : $fname_ind, scalar(@parts),
  13705. $sender) if ll(3);
  13706. } elsif ($msginfo->is_auto) { # bounce likely, but contents unrecognizable
  13707. do_log(3, 'inspect_dsn: possibly a %s, unrecognizable, '.
  13708. 'struct: "%s", parts(%s/%d): %s',
  13709. $bounce_type, $structure_type,
  13710. !defined($fname_ind) ? '-' : $fname_ind, scalar(@parts),
  13711. join(", ",@t)) if ll(3);
  13712. } else { # not a bounce
  13713. do_log(3, 'inspect_dsn: not a bounce');
  13714. }
  13715. }
  13716. $bounce_type = undef if !$is_true_bounce;
  13717. !$is_true_bounce ? () : (\%header_field,$bounce_type);
  13718. }
  13719. # obtain authserv-id from an Authentication-Results header field
  13720. #
  13721. sub parse_authentication_results($) {
  13722. local($_) = $_[0];
  13723. tr/\n//d; local($1); my $comm_lvl = 0; my $authservid;
  13724. while (!/\G \z/gcsx) {
  13725. if ( /\G \( /gcsx) { $comm_lvl++ }
  13726. elsif ($comm_lvl > 0 && /\G \) /gcsx) { $comm_lvl-- }
  13727. elsif ($comm_lvl > 0 && /\G(?: \\. | [^()\\]+ )/gcsx) {}
  13728. elsif (!$comm_lvl && /\G [ \t]+ /gcsx) {}
  13729. elsif (!$comm_lvl && /\G ([^\000-\040\177-\377:;,"()<>\[\]\@\\]+)/gcsx)
  13730. { $authservid = $1; last }
  13731. else { last }; # syntax error
  13732. }
  13733. $authservid;
  13734. }
  13735. sub add_forwarding_header_edits_common($$$$$$) {
  13736. my($msginfo, $hdr_edits, $hold, $any_undecipherable,
  13737. $virus_presence_checked, $spam_presence_checked) = @_;
  13738. my $use_our_hdrs = cr('prefer_our_added_header_fields');
  13739. my $allowed_hdrs = cr('allowed_added_header_fields');
  13740. if ($allowed_hdrs && $allowed_hdrs->{lc('X-Amavis-Hold')}) {
  13741. # discard existing X-Amavis-Hold header field, only allow our own
  13742. $hdr_edits->delete_header('X-Amavis-Hold');
  13743. if ($hold ne '') {
  13744. $hdr_edits->add_header('X-Amavis-Hold', $hold);
  13745. do_log(-1, "Inserting header field: X-Amavis-Hold: %s", $hold);
  13746. }
  13747. }
  13748. if (c('enable_dkim_verification') &&
  13749. $allowed_hdrs && $allowed_hdrs->{lc('Authentication-Results')}) {
  13750. # RFC 5451: For security reasons, any MTA conforming to this specification
  13751. # MUST delete any discovered instance of this header field that claims to
  13752. # have been added within its trust boundary and that did not come from
  13753. # another trusted MTA. [...] For simplicity and maximum security, a border
  13754. # MTA MAY remove all instances of this header field on mail crossing into
  13755. # its trust boundary. [...] (Hmmm...!?) However, an MTA MUST remove such
  13756. # a header if the [SMTP] connection relaying the message is not from a
  13757. # trusted internal MTA.
  13758. my $authservid = c('myauthservid');
  13759. $authservid = c('myhostname') if !defined $authservid || $authservid eq '';
  13760. # delete header field if its authserv-id matches ours or is unparseable
  13761. $hdr_edits->edit_header('Authentication-Results',
  13762. sub { my($h,$b) = @_;
  13763. my $aid = parse_authentication_results($b);
  13764. if (defined $aid) { $aid =~ s{/.*}{}; $authservid =~ s{/.*}{} };
  13765. !defined $aid || lc($aid) eq lc($authservid) ? (undef,0) : ($b,1);
  13766. } );
  13767. # [...] Border MTA MAY elect simply to remove all instances of this
  13768. # header field on mail crossing into its trust boundary
  13769. # $hdr_edits->delete_header('Authentication-Results');
  13770. }
  13771. # example on how to remove subject tag inserted by some other MTA:
  13772. # $hdr_edits->edit_header('Subject',
  13773. # sub { my($h,$s)=@_; $s=~s/^\s*\*\*\* Spam \*\*\*(.*)/$1/si; $s });
  13774. if ($extra_code_antivirus) {
  13775. # $hdr_edits->delete_header('X-Amavis-Alert'); # it does not hurt to keep it
  13776. my $am_hdr_fld_head = c('X_HEADER_TAG');
  13777. my $am_hdr_fld_body = c('X_HEADER_LINE');
  13778. $hdr_edits->delete_header($am_hdr_fld_head)
  13779. if c('remove_existing_x_scanned_headers') &&
  13780. defined $am_hdr_fld_body && $am_hdr_fld_body ne '' &&
  13781. defined $am_hdr_fld_head && $am_hdr_fld_head =~ /^[!-9;-\176]+\z/;
  13782. }
  13783. for ('X-Spam-Checker-Version') {
  13784. if ($extra_code_antispam_sa &&
  13785. $allowed_hdrs && $allowed_hdrs->{lc $_} &&
  13786. $use_our_hdrs && $use_our_hdrs->{lc $_}) {
  13787. no warnings 'once';
  13788. $hdr_edits->add_header($_,
  13789. sprintf("SpamAssassin %s (%s) on %s", Mail::SpamAssassin::Version(),
  13790. $Mail::SpamAssassin::SUB_VERSION, c('myhostname')));
  13791. }
  13792. }
  13793. $hdr_edits;
  13794. }
  13795. # Prepare header edits for the first not-yet-done recipient.
  13796. # Inspect remaining recipients, returning the list of recipient objects
  13797. # that are receiving the same set of header edits (so the message may be
  13798. # delivered to them in one SMTP transaction).
  13799. #
  13800. sub add_forwarding_header_edits_per_recip($$$$$$$) {
  13801. my($msginfo, $hdr_edits, $hold, $any_undecipherable,
  13802. $virus_presence_checked, $spam_presence_checked, $filter) = @_;
  13803. my(@recip_cluster);
  13804. my(@per_recip_data) = grep(!$_->recip_done && (!$filter || &$filter($_)),
  13805. @{$msginfo->per_recip_data});
  13806. my $per_recip_data_len = scalar(@per_recip_data);
  13807. my $first = 1; my $cluster_key; my $cluster_full_spam_status;
  13808. my $use_our_hdrs = cr('prefer_our_added_header_fields');
  13809. my $allowed_hdrs = cr('allowed_added_header_fields');
  13810. my $x_header_tag = c('X_HEADER_TAG');
  13811. my $adding_x_header_tag =
  13812. $x_header_tag =~ /^[!-9;-\176]+\z/ && c('X_HEADER_LINE') ne '' &&
  13813. $allowed_hdrs && $allowed_hdrs->{lc($x_header_tag)};
  13814. my $mail_id = $msginfo->mail_id;
  13815. my $os_fp = $msginfo->client_os_fingerprint;
  13816. if (defined($os_fp) && $os_fp ne '' && $msginfo->client_addr ne '')
  13817. { $os_fp .= ', ['. $msginfo->client_addr . ']:' . $msginfo->client_port }
  13818. my(@headers_to_be_removed); # header fields that may need to be removed
  13819. if ($extra_code_antispam) {
  13820. @headers_to_be_removed = qw(
  13821. X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score
  13822. X-Spam-Report X-Spam-Checker-Version X-Spam-Tests);
  13823. @headers_to_be_removed =
  13824. grep(defined $msginfo->get_header_field2($_), @headers_to_be_removed);
  13825. }
  13826. my $header_tagged = 0;
  13827. for my $r (@per_recip_data) {
  13828. my $spam_level = $r->spam_level;
  13829. my $recip = $r->recip_addr;
  13830. my $is_local = $r->recip_is_local;
  13831. my $blacklisted = $r->recip_blacklisted_sender;
  13832. my $whitelisted = $r->recip_whitelisted_sender;
  13833. my $bypassed = $r->bypass_spam_checks;
  13834. my $do_tag = $r->is_in_contents_category(CC_CLEAN,1);
  13835. my $do_tag2 = $r->is_in_contents_category(CC_SPAMMY);
  13836. my $do_kill = $r->is_in_contents_category(CC_SPAM);
  13837. my $do_tag_badh = $r->is_in_contents_category(CC_BADH);
  13838. my $do_tag_banned = $r->is_in_contents_category(CC_BANNED);
  13839. my $do_tag_virus = $r->is_in_contents_category(CC_VIRUS);
  13840. my $mail_mangle = $r->mail_body_mangle;
  13841. my $do_tag_virus_checked =
  13842. $adding_x_header_tag && !$r->bypass_virus_checks;
  13843. my $do_rem_hdr = @headers_to_be_removed &&
  13844. lookup2(0,$recip,ca('remove_existing_spam_headers_maps'));
  13845. my $do_p0f = $is_local && defined($os_fp) && $os_fp ne '' &&
  13846. $allowed_hdrs && $allowed_hdrs->{lc('X-Amavis-OS-Fingerprint')};
  13847. my $pp_age;
  13848. if ($allowed_hdrs && $allowed_hdrs->{lc('X-Amavis-PenPals')}) {
  13849. $pp_age = $r->recip_penpals_age;
  13850. $pp_age = format_time_interval($pp_age) if defined $pp_age;
  13851. }
  13852. my($tag_level,$tag2_level,$subject_tag);
  13853. if ($extra_code_antispam && !$bypassed) {
  13854. $tag_level = lookup2(0,$recip, ca('spam_tag_level_maps'));
  13855. $tag2_level = lookup2(0,$recip, ca('spam_tag2_level_maps'));
  13856. }
  13857. if ($is_local) { # || c('warn_offsite')
  13858. my(@subj_maps_pairs) = $r->setting_by_main_contents_category_all(
  13859. cr('subject_tag_maps_by_ccat'));
  13860. for my $pair (@subj_maps_pairs) {
  13861. my($cc,$map_ref) = @$pair;
  13862. next if !ref($map_ref);
  13863. $subject_tag = lookup2(0,$recip,$map_ref);
  13864. # take the first nonempty string
  13865. last if defined $subject_tag && $subject_tag ne '';
  13866. }
  13867. }
  13868. $subject_tag = '' if !defined $subject_tag;
  13869. if ($subject_tag ne '') { # expand subject template
  13870. # just implement a small subset of macro-lookalikes, not true macro calls
  13871. $subject_tag =~
  13872. s{_(SCORE|REQD|YESNO|YESNOCAPS|HOSTNAME|DATE|U|LOGID|MAILID)_}
  13873. { $1 eq 'SCORE' ? (0+sprintf("%.3f",$spam_level))
  13874. : $1 eq 'REQD' ? (!defined($tag2_level) ? '-' :
  13875. 0+sprintf("%.3f",$tag2_level))
  13876. : $1 eq 'YESNO' ? ($do_tag2 ? 'Yes' : 'No')
  13877. : $1 eq 'YESNOCAPS' ? ($do_tag2 ? 'YES' : 'NO')
  13878. : $1 eq 'HOSTNAME' ? c('myhostname')
  13879. : $1 eq 'DATE' ? rfc2822_timestamp($msginfo->rx_time)
  13880. : $1 eq 'U' ? iso8601_utc_timestamp($msginfo->rx_time)
  13881. : $1 eq 'LOGID' ? $msginfo->log_id
  13882. : $1 eq 'MAILID' ? $mail_id||''
  13883. : '_'.$1.'_' }egsx;
  13884. }
  13885. # normalize
  13886. $_ = $_?1:0 for ($do_tag_virus_checked, $do_tag_virus, $do_tag_banned,
  13887. $do_tag_badh, $do_tag, $do_tag2, $do_p0f, $do_rem_hdr,
  13888. $is_local);
  13889. my($spam_level_bar, $full_spam_status);
  13890. if ($is_local && ($do_tag || $do_tag2)) { # prepare status and level bar
  13891. # spam-related header fields should _not_ be inserted for:
  13892. # - nonlocal recipients (outgoing mail), as a matter of courtesy
  13893. # to our users;
  13894. # - recipients matching bypass_spam_checks: even though spam checking
  13895. # may have been done for other reasons, these recipients do not expect
  13896. # such header fields, so let's pretend the check has not been done
  13897. # and not insert spam-related header fields for them;
  13898. # - everyone when the spam level is below the tag level
  13899. # or the sender was whitelisted and tag level is below -10
  13900. # (undefined tag level is treated as lower than any spam score).
  13901. my $autolearn_status = $msginfo->supplementary_info('AUTOLEARN');
  13902. my $slc = c('sa_spam_level_char');
  13903. $spam_level_bar = $slc x min(64, $bypassed || $whitelisted ? 0
  13904. : $blacklisted ? 64
  13905. : 0+$spam_level) if $slc ne '';
  13906. my $spam_tests = $r->spam_tests;
  13907. $spam_tests = !defined $spam_tests ?'' : join(',',map($$_,@$spam_tests));
  13908. # allow header field wrapping at any comma
  13909. my $s = $spam_tests; $s =~ s/,/,\n /g;
  13910. $full_spam_status = sprintf(
  13911. "%s,\n score=%s\n %s%s%stests=[%s]\n autolearn=%s",
  13912. $do_tag2 ? 'Yes' : 'No',
  13913. !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level),
  13914. !defined $tag_level || $tag_level eq '' ? ''
  13915. : sprintf("tagged_above=%s\n ",$tag_level),
  13916. !defined $tag2_level ? '' : sprintf("required=%s\n ", $tag2_level),
  13917. join('', $blacklisted ? "BLACKLISTED\n " : (),
  13918. $whitelisted ? "WHITELISTED\n " : ()),
  13919. $s, $autolearn_status||'unavailable');
  13920. }
  13921. my $key = join("\000", map {defined $_ ? $_ : ''} (
  13922. $do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
  13923. $do_tag && $is_local, $do_tag2 && $is_local, $subject_tag, $do_rem_hdr,
  13924. $spam_level_bar, $full_spam_status, $mail_mangle, $do_p0f, $pp_age) );
  13925. if ($first) {
  13926. if (ll(4)) {
  13927. my $sl = !defined($spam_level) ? 'x'
  13928. : 0+sprintf("%.3f",$spam_level); # trim fraction
  13929. do_log(4, "headers CLUSTERING: NEW CLUSTER <%s>: score=%s, ".
  13930. "tag=%s, tag2=%s, local=%s, bl=%s, s=%s, mangle=%s", $recip,
  13931. $sl, $do_tag, $do_tag2, $is_local, $blacklisted, $subject_tag,
  13932. $mail_mangle);
  13933. }
  13934. $cluster_key = $key; $cluster_full_spam_status = $full_spam_status;
  13935. } elsif ($key eq $cluster_key) {
  13936. do_log(5,"headers CLUSTERING: <%s> joining cluster", $recip);
  13937. } else {
  13938. do_log(5,"headers CLUSTERING: skipping <%s> (t=%s, t2=%s, r=%s, l=%s)",
  13939. $recip,$do_tag,$do_tag2,$do_rem_hdr,$is_local);
  13940. next; # this recipient will be handled in some later pass
  13941. }
  13942. if ($first) { # insert header fields required for the new cluster
  13943. my(%header_field_provided); # mainly applies to spam header fields
  13944. if ($do_rem_hdr) {
  13945. $hdr_edits->delete_header($_) for @headers_to_be_removed;
  13946. }
  13947. if ($is_local && defined $msginfo->quarantined_to && defined $mail_id) {
  13948. $hdr_edits->add_header('X-Quarantine-ID', '<'.$mail_id.'>')
  13949. if $allowed_hdrs && $allowed_hdrs->{lc('X-Quarantine-ID')};
  13950. }
  13951. if ($mail_mangle) { # mail body modified, invalidates DKIM signatures
  13952. if ($allowed_hdrs && $allowed_hdrs->{lc('X-Amavis-Modified')}) {
  13953. $hdr_edits->add_header('X-Amavis-Modified',
  13954. sprintf("Mail body modified (%s) - %s",
  13955. length($mail_mangle) > 1 ? "using $mail_mangle" : "defanged",
  13956. c('myhostname') ));
  13957. }
  13958. }
  13959. if ($do_tag_virus_checked) {
  13960. $hdr_edits->add_header(c('X_HEADER_TAG'), c('X_HEADER_LINE'));
  13961. }
  13962. if ($allowed_hdrs && $allowed_hdrs->{lc('X-Amavis-Alert')}) {
  13963. if ($do_tag_virus) {
  13964. my $virusname_list = $msginfo->virusnames;
  13965. $hdr_edits->add_header('X-Amavis-Alert',
  13966. "INFECTED, message contains virus: " .
  13967. (!$virusname_list ? '' : join(", ",@$virusname_list)) );
  13968. $header_tagged = 1;
  13969. }
  13970. if ($do_tag_banned) {
  13971. $hdr_edits->add_header('X-Amavis-Alert',
  13972. 'BANNED, message contains ' . $r->banning_reason_short);
  13973. $header_tagged = 1;
  13974. }
  13975. if ($do_tag_badh) {
  13976. $hdr_edits->add_header('X-Amavis-Alert',
  13977. 'BAD HEADER SECTION, ' . $bad_headers[0]);
  13978. # $header_tagged = 1; # not this one, it is mostly harmless
  13979. }
  13980. }
  13981. if ($is_local && $allowed_hdrs && $use_our_hdrs) {
  13982. for ('X-Spam-Checker-Version') {
  13983. if ($extra_code_antispam_sa &&
  13984. $allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  13985. # a hack instead of making %header_field_provided global:
  13986. # just mark it as already provided, this header field was
  13987. # already inserted by add_forwarding_header_edits_common()
  13988. $header_field_provided{lc $_} = 1;
  13989. }
  13990. }
  13991. for ('X-Spam-Flag') {
  13992. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  13993. $hdr_edits->add_header($_, $do_tag2 ? 'YES' : 'NO') if $do_tag;
  13994. $header_field_provided{lc $_} = 1;
  13995. $header_tagged = 1 if $do_tag2; # SPAMMY
  13996. }
  13997. }
  13998. for ('X-Spam-Score') {
  13999. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14000. if ($do_tag) {
  14001. my $score = 0+$spam_level;
  14002. $score = max(64,$score) if $blacklisted; # not below 64 if bl
  14003. $score = min( 0,$score) if $whitelisted; # not above 0 if wl
  14004. $hdr_edits->add_header($_, 0+sprintf("%.3f",$score));
  14005. }
  14006. $header_field_provided{lc $_} = 1;
  14007. }
  14008. }
  14009. for ('X-Spam-Level') {
  14010. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14011. if ($do_tag && defined $spam_level_bar) {
  14012. $hdr_edits->add_header($_, $spam_level_bar);
  14013. }
  14014. $header_field_provided{lc $_} = 1;
  14015. }
  14016. }
  14017. for ('X-Spam-Status') {
  14018. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14019. $hdr_edits->add_header($_, $full_spam_status, 1) if $do_tag;
  14020. $header_field_provided{lc $_} = 1;
  14021. }
  14022. }
  14023. for ('X-Spam-Report') {
  14024. # SA reports may contain any octet, i.e. 8-bit data from a mail
  14025. # that is reported by a matching rule; no charset is associated, so
  14026. # it doesn't make sense to RFC 2047 -encode it, so just sanitize it
  14027. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14028. if ($do_tag2) {
  14029. my $report = $r->spam_report;
  14030. $report = $msginfo->spam_report if !defined $report;
  14031. if (defined $report && $report ne '') {
  14032. $hdr_edits->add_header($_, "\n".sanitize_str($report,1), 2);
  14033. }
  14034. }
  14035. $header_field_provided{lc $_} = 1;
  14036. }
  14037. }
  14038. }
  14039. if ($is_local && $allowed_hdrs) {
  14040. # add remaining header fields as provided by spam scanners
  14041. my $sa_header = $msginfo->supplementary_info(
  14042. $do_tag2 ? 'ADDEDHEADERSPAM' : 'ADDEDHEADERHAM');
  14043. if (defined $sa_header && $sa_header ne '') {
  14044. for my $hf (split(/^(?![ \t])/m, $sa_header, -1)) {
  14045. local($1,$2);
  14046. if ($hf =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) {
  14047. my($hf_name,$hf_body) = ($1,$2);
  14048. my $hf_name_lc = lc($hf_name); chomp($hf_body);
  14049. if ($header_field_provided{$hf_name_lc}) {
  14050. do_log(5,'fwd: scanner provided %s, but we preferred our own',
  14051. $hf_name);
  14052. } elsif (!$allowed_hdrs->{$hf_name_lc}) {
  14053. do_log(5,'fwd: scanner provided %s, inhibited '.
  14054. 'by %%allowed_added_header_fields', $hf_name);
  14055. } else {
  14056. do_log(5,'fwd: scanner provided %s, inserting', $hf_name);
  14057. $hdr_edits->add_header($hf_name, $hf_body, 2);
  14058. }
  14059. }
  14060. }
  14061. }
  14062. for my $pair ( ['DSPAMRESULT', 'X-DSPAM-Result'],
  14063. ['DSPAMSIGNATURE', 'X-DSPAM-Signature'],
  14064. ['CRM114STATUS', 'X-CRM114-Status'],
  14065. ['CRM114CACHEID', 'X-CRM114-CacheID'] ) {
  14066. my($suppl_attr_name, $hf_name) = @$pair;
  14067. my $suppl_attr_val = $msginfo->supplementary_info($suppl_attr_name);
  14068. if (defined $suppl_attr_val && $suppl_attr_val ne '') {
  14069. if (!$allowed_hdrs->{lc $hf_name}) {
  14070. do_log(5,'fwd: scanner provided %s, '.
  14071. 'inhibited by %%allowed_added_header_fields', $hf_name);
  14072. } else {
  14073. do_log(5,'fwd: scanner provided %s, inserting', $hf_name);
  14074. $hdr_edits->add_header($hf_name,
  14075. sanitize_str($suppl_attr_val), 2);
  14076. }
  14077. }
  14078. }
  14079. }
  14080. $hdr_edits->add_header('X-Amavis-OS-Fingerprint',
  14081. sanitize_str($os_fp)) if $do_p0f;
  14082. $hdr_edits->add_header('X-Amavis-PenPals',
  14083. 'age '.$pp_age) if defined $pp_age;
  14084. if ($is_local && c('enable_dkim_verification') &&
  14085. $allowed_hdrs && $allowed_hdrs->{lc('Authentication-Results')}) {
  14086. for my $h (Amavis::DKIM::generate_authentication_results($msginfo,0)) {
  14087. $hdr_edits->add_header('Authentication-Results', $h, 1);
  14088. }
  14089. }
  14090. if ($subject_tag ne '') {
  14091. if (defined $msginfo->get_header_field2('subject')) {
  14092. $hdr_edits->edit_header('Subject',
  14093. sub { local($1,$2);
  14094. $_[1] =~ /^([ \t]?)(.*)\z/s; my $subj = $2;
  14095. if (length($subject_tag) >= 3) # precaution
  14096. { $subj =~ s/\Q$subject_tag\E//sg }
  14097. ' ' . $subject_tag . $subj });
  14098. } else { # no Subject header field present, insert one
  14099. $subject_tag =~ s/[ \t]+\z//; # trim
  14100. $hdr_edits->add_header('Subject', $subject_tag);
  14101. do_log(0,"INFO: no existing header field 'Subject', inserting it");
  14102. }
  14103. $header_tagged = 1;
  14104. }
  14105. if ($allowed_hdrs && $allowed_hdrs->{lc('Received')} &&
  14106. grep($_->delivery_method ne '', @{$msginfo->per_recip_data})) {
  14107. $hdr_edits->add_header('Received',
  14108. make_received_header_field($msginfo,1), 1);
  14109. }
  14110. } # if $first
  14111. push(@recip_cluster,$r); $first = 0;
  14112. $r->recip_tagged(1) if $header_tagged;
  14113. my $delim = c('recipient_delimiter');
  14114. if ($is_local) {
  14115. # rewrite/replace recipient addresses, possibly with multiple recipients
  14116. my $rewrite_map = $r->setting_by_contents_category(
  14117. cr('addr_rewrite_maps_by_ccat'));
  14118. my $rewrite = !ref $rewrite_map ? undef : lookup2(0,$recip,$rewrite_map);
  14119. if ($rewrite ne '') {
  14120. my(@replacements) = grep($_ ne '',
  14121. map { /^ [ \t]* (.*?) [ \t]* \z/sx; $1 } split(/,/, $rewrite, -1));
  14122. if (@replacements) {
  14123. my $repl_addr = shift @replacements;
  14124. my $modif_addr = replace_addr_fields($recip,$repl_addr,$delim);
  14125. ll(5) && do_log(5,"addr_rewrite_maps: replacing <%s> by <%s>",
  14126. $recip,$modif_addr);
  14127. $r->recip_addr_modified($modif_addr);
  14128. for my $bcc (@replacements) { # remaining addresses are extra Bcc
  14129. my $new_addr = replace_addr_fields($recip,$bcc,$delim);
  14130. ll(5) && do_log(5,"addr_rewrite_maps: recip <%s>, adding <%s>",
  14131. $recip,$new_addr);
  14132. # my $clone = $r->clone;
  14133. # $clone->recip_addr_modified($new_addr);
  14134. }
  14135. }
  14136. $r->dsn_orcpt(orcpt_encode($r->recip_addr_smtp))
  14137. if !defined($r->dsn_orcpt);
  14138. }
  14139. }
  14140. if ($is_local && defined $delim && $delim ne '') {
  14141. # append address extensions to mailbox names if desired
  14142. my $ext_map = $r->setting_by_contents_category(
  14143. cr('addr_extension_maps_by_ccat'));
  14144. my $ext = !ref($ext_map) ? undef : lookup2(0,$recip,$ext_map);
  14145. if ($ext ne '') {
  14146. $ext = $delim . $ext;
  14147. my $orig_extension; my($localpart,$domain) = split_address($recip);
  14148. ($localpart,$orig_extension) = split_localpart($localpart,$delim)
  14149. if c('replace_existing_extension'); # strip existing extension
  14150. my $new_addr = $localpart.$ext.$domain;
  14151. if (ll(5)) {
  14152. if (!defined($orig_extension)) {
  14153. do_log(5, "appending addr ext '%s', giving '%s'", $ext,$new_addr);
  14154. } else {
  14155. do_log(5, "replacing addr ext '%s' by '%s', giving '%s'",
  14156. $orig_extension,$ext,$new_addr);
  14157. }
  14158. }
  14159. # RFC 3461: If no ORCPT parameter was present in the RCPT command when
  14160. # the message was received, an ORCPT parameter MAY be added to the
  14161. # RCPT command when the message is relayed. If an ORCPT parameter is
  14162. # added by the relaying MTA, it MUST contain the recipient address
  14163. # from the RCPT command used when the message was received by that MTA.
  14164. $r->dsn_orcpt(orcpt_encode($r->recip_addr_smtp))
  14165. if !defined($r->dsn_orcpt);
  14166. $r->recip_addr_modified($new_addr);
  14167. $r->recip_tagged(1);
  14168. }
  14169. }
  14170. }
  14171. my $done_all;
  14172. if (@recip_cluster == $per_recip_data_len) {
  14173. do_log(5,"headers CLUSTERING: done all %d recips in one go",
  14174. $per_recip_data_len);
  14175. $done_all = 1;
  14176. } else {
  14177. ll(4) && do_log(4, "headers CLUSTERING: got %d recips out of %d: %s",
  14178. scalar(@recip_cluster), $per_recip_data_len,
  14179. join(', ', map($_->recip_addr_smtp, @recip_cluster)));
  14180. }
  14181. if (ll(2) && defined($cluster_full_spam_status) && @recip_cluster) {
  14182. my $s = $cluster_full_spam_status; $s =~ s/\n[ \t]/ /g;
  14183. do_log(2, "spam-tag, %s -> %s, %s", $msginfo->sender_smtp,
  14184. join(',', map($_->recip_addr_smtp, @recip_cluster)), $s);
  14185. }
  14186. ($hdr_edits, \@recip_cluster, $done_all);
  14187. }
  14188. # Mail body mangling (defanging, sanitizing or adding disclaimers);
  14189. # Prepare mail body replacement for the first recipient
  14190. # in the @$per_recip_data list (which contains a subset of recipients
  14191. # with the same mail edits, to be dispatched next as one message)
  14192. #
  14193. sub prepare_modified_mail($$$$) {
  14194. my($msginfo, $hold, $any_undecipherable, $per_recip_data) = @_;
  14195. my $body_modified = 0;
  14196. for my $r (@$per_recip_data) { # a subset of recipients!
  14197. my $recip = $r->recip_addr;
  14198. my $mail_mangle = $r->mail_body_mangle;
  14199. my $actual_mail_mangle;
  14200. if (!$mail_mangle) {
  14201. # skip
  14202. } elsif ($mail_mangle =~ /^(?:null|nulldisclaimer)\z/i) { # for testing
  14203. $body_modified = 1; # pretend mail was modified while actually it was not
  14204. $msginfo->mail_text_str(undef);
  14205. section_time('mangle-'.$mail_mangle);
  14206. } elsif (( lc $mail_mangle ne 'attach' &&
  14207. ($enable_anomy_sanitizer || $altermime ne '') )
  14208. || $mail_mangle =~ /^(?:anomy|altermime|disclaimer)\z/i) {
  14209. do_log(2,"mangling by: %s, <%s>", $mail_mangle,$recip);
  14210. my $orig_fn = $msginfo->mail_text_fn;
  14211. my $repl_fn = $msginfo->mail_tempdir . '/email-repl.txt';
  14212. my $file_position = $msginfo->skip_bytes;
  14213. my $out_fh; my $repl_size; my $eval_stat;
  14214. eval {
  14215. $out_fh = IO::File->new;
  14216. $out_fh->open($repl_fn, O_CREAT|O_EXCL|O_WRONLY, 0640)
  14217. or die "Can't create file $repl_fn: $!";
  14218. binmode($out_fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  14219. if (lc $mail_mangle eq 'anomy' && !$enable_anomy_sanitizer) {
  14220. die 'Anomy requested, but $enable_anomy_sanitizer is false';
  14221. } elsif ($enable_anomy_sanitizer &&
  14222. $mail_mangle !~ /^(?:altermime|disclaimer)\z/i) {
  14223. $actual_mail_mangle = 'anomy';
  14224. my $inp_fh = $msginfo->mail_text;
  14225. $inp_fh->seek($file_position, 0) or die "Can't rewind mail file: $!";
  14226. $enable_anomy_sanitizer or die "Anomy disabled: $mail_mangle";
  14227. my(@scanner_conf); my $e; my $engine = Anomy::Sanitizer->new;
  14228. if ($e = $engine->error) { die $e }
  14229. $engine->configure(@scanner_conf, @{ca('anomy_sanitizer_args')});
  14230. if ($e = $engine->error) { die $e }
  14231. my $ret = $engine->sanitize($inp_fh, $out_fh);
  14232. if ($e = $engine->error) { die $e }
  14233. # close flushes buffers, makes it possible to check file size below
  14234. $out_fh->close or die "Can't close file $repl_fn: $!";
  14235. # re-open as read-only
  14236. $out_fh = IO::File->new;
  14237. $out_fh->open($repl_fn,'<') or die "Can't open file $repl_fn: $!";
  14238. binmode($out_fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  14239. } else { # use altermime for adding disclaimers or defanging
  14240. $actual_mail_mangle = 'altermime';
  14241. $altermime ne '' or die "altermime not available: $mail_mangle";
  14242. # prepare arguments to altermime
  14243. my(@altermime_args); my $disclaimer_options;
  14244. if (lc($mail_mangle) ne 'disclaimer') { # defang: no by-sender opts.
  14245. @altermime_args = @{ca('altermime_args_defang')};
  14246. } else { # disclaimer
  14247. @altermime_args = @{ca('altermime_args_disclaimer')};
  14248. my $opt_maps = ca('disclaimer_options_bysender_maps');
  14249. if ($opt_maps && @$opt_maps && # by sender options?
  14250. grep(/_OPTION_/,@altermime_args))
  14251. { # determine whose by-sender options to use
  14252. my $fm = $msginfo->rfc2822_from;
  14253. my $rf = $msginfo->rfc2822_resent_from;
  14254. my $rs = $msginfo->rfc2822_resent_sender;
  14255. my(@rfc2822_from) = !defined($fm) ? () : ref $fm ? @$fm : $fm;
  14256. my(@rfc2822_resent_from, @rfc2822_resent_sender);
  14257. @rfc2822_resent_from = @$rf if defined $rf;
  14258. @rfc2822_resent_sender = @$rs if defined $rs;
  14259. # see comments in dkim_make_signatures
  14260. my(@search_list); # collects candidate originator addresses
  14261. # author addresses go first
  14262. push(@search_list, map([$_,'2822.From'], @rfc2822_from));
  14263. # merge Resent-From and Resent-Sender addresses by resent blocks
  14264. while (@rfc2822_resent_from || @rfc2822_resent_sender) {
  14265. while (@rfc2822_resent_from) {
  14266. my $addr = shift(@rfc2822_resent_from);
  14267. last if !defined $addr; # undef delimits resent blocks
  14268. push(@search_list, [$addr, '2822.Resent-From']);
  14269. }
  14270. while (@rfc2822_resent_sender) {
  14271. my $addr = shift(@rfc2822_resent_sender);
  14272. last if !defined $addr; # undef delimits resent blocks
  14273. push(@search_list, [$addr, '2822.Resent-Sender']);
  14274. }
  14275. }
  14276. push(@search_list, [$msginfo->rfc2822_sender, '2822.Sender']);
  14277. push(@search_list, [$msginfo->sender, '2821.mail_from']);
  14278. #
  14279. # find disclaimer options pertaining to the
  14280. # most appropriate originator address
  14281. my(%addr_seen);
  14282. for my $pair (@search_list) {
  14283. my($addr,$addr_src) = @$pair;
  14284. next if !defined($addr) || $addr eq '';
  14285. next if $addr_seen{$addr}++;
  14286. do_log(5,"disclaimer options lookup (%s) %s", $addr_src,$addr);
  14287. next if !lookup2(0,$addr, ca('local_domains_maps'));
  14288. my($opt,$matchingkey) = lookup2(0,$addr,$opt_maps);
  14289. if (defined $opt) {
  14290. $disclaimer_options = $opt;
  14291. do_log(3,"disclaimer options pertaining to (%s) %s: %s",
  14292. $addr_src, $addr, $disclaimer_options);
  14293. last;
  14294. }
  14295. }
  14296. $disclaimer_options = '' if !defined $disclaimer_options;
  14297. s/_OPTION_/$disclaimer_options/gs for @altermime_args;
  14298. }
  14299. }
  14300. my $msg = $msginfo->mail_text;
  14301. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  14302. $msg = $msg_str_ref if ref $msg_str_ref;
  14303. # copy original mail to $repl_fn, altermime can't handle stdin well
  14304. if (!defined $msg) {
  14305. # empty mail
  14306. } elsif (ref $msg eq 'SCALAR') {
  14307. # do it in chunks, saves memory, cache friendly
  14308. while ($file_position < length($$msg)) {
  14309. $out_fh->print(substr($$msg,$file_position,16384))
  14310. or die "Error writing to $repl_fn: $!";
  14311. $file_position += 16384; # may overshoot, no problem
  14312. }
  14313. } elsif ($msg->isa('MIME::Entity')) {
  14314. die "sanitizing a MIME::Entity object is not implemented";
  14315. } else {
  14316. $msg->seek($file_position,0) or die "Can't rewind mail file: $!";
  14317. my($nbytes,$buff);
  14318. while (($nbytes = $msg->read($buff,16384)) > 0) {
  14319. $out_fh->print($buff) or die "Error writing to $repl_fn: $!";
  14320. }
  14321. defined $nbytes or die "Error reading mail file: $!";
  14322. undef $buff; # release storage
  14323. }
  14324. $out_fh->close or die "Can't close file $repl_fn: $!";
  14325. undef $out_fh;
  14326. my($proc_fh,$pid) = run_command(undef, '&1', $altermime,
  14327. "--input=$repl_fn", @altermime_args);
  14328. my($r,$status) = collect_results($proc_fh,$pid,$altermime,16384,[0]);
  14329. undef $proc_fh; undef $pid;
  14330. do_log(2,"program %s said: %s",
  14331. $altermime, $$r) if ref $r && $$r ne '';
  14332. $status == 0 or die "Program $altermime failed: $status, $$r";
  14333. $out_fh = IO::File->new;
  14334. $out_fh->open($repl_fn,'<') or die "Can't open file $repl_fn: $!";
  14335. binmode($out_fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  14336. }
  14337. my $errn = lstat($repl_fn) ? 0 : 0+$!;
  14338. if ($errn) { die "Replacement $repl_fn inaccessible: $!" }
  14339. else { $repl_size = 0 + (-s _) }
  14340. 1;
  14341. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat };
  14342. if (defined $eval_stat || !defined $repl_size || $repl_size <= 0) {
  14343. # handle failure
  14344. my $msg = defined $eval_stat ? $eval_stat
  14345. : sprintf("replacement size %d", $repl_size);
  14346. do_log(-1,"mangling by %s failed: %s, mail will pass unmodified",
  14347. $actual_mail_mangle, $msg);
  14348. if (defined $out_fh) {
  14349. $out_fh->close or do_log(-1,"Can't close %s: %s", $repl_fn,$!);
  14350. undef $out_fh;
  14351. }
  14352. unlink($repl_fn) or do_log(-1,"Can't remove %s: %s", $repl_fn,$!);
  14353. if ($actual_mail_mangle eq 'altermime') { # check for leftover files
  14354. my $repl_tmp_fn = $repl_fn . '.tmp'; # altermime's temporary file
  14355. my $errn = lstat($repl_tmp_fn) ? 0 : 0+$!;
  14356. if ($errn == ENOENT) {} # fine, does not exist
  14357. elsif ($errn) {
  14358. do_log(-1,"Temporary file %s is inaccessible: %s",$repl_tmp_fn,$!);
  14359. } else { # cleanup after failing altermime
  14360. unlink($repl_tmp_fn)
  14361. or do_log(-1,"Can't remove %s: %s",$repl_tmp_fn,$!);
  14362. }
  14363. }
  14364. } else {
  14365. do_log(1,"mangling by %s (%s) done, new size: %d, orig %d bytes",
  14366. $actual_mail_mangle, $mail_mangle,
  14367. $repl_size, $msginfo->msg_size);
  14368. # don't close or delete the original file, we'll still need it
  14369. $msginfo->mail_text($out_fh); $msginfo->mail_text_fn($repl_fn);
  14370. $msginfo->mail_text_str(undef); $msginfo->body_start_pos(undef);
  14371. $msginfo->skip_bytes(0);
  14372. $body_modified = 1;
  14373. }
  14374. section_time('mangle-'.$actual_mail_mangle);
  14375. } else { # 'attach' (default) - poor-man's defanging of dangerous contents
  14376. do_log(2,"mangling by built-in defanger: %s, <%s>", $mail_mangle,$recip);
  14377. $actual_mail_mangle = 'attach';
  14378. my(@explanation); my $spam_summary_inserted = 0;
  14379. my(@df_pairs) =
  14380. $r->setting_by_main_contents_category_all(cr('defang_maps_by_ccat'));
  14381. for my $pair (@df_pairs) { # collect all defanging reasons that apply
  14382. my($cc,$mangle_map_ref) = @$pair;
  14383. my $df = !defined($mangle_map_ref) ? undef
  14384. : !ref($mangle_map_ref) ? $mangle_map_ref # compatibility
  14385. : lookup2(0,$recip,$mangle_map_ref, Label=>'Mangling2');
  14386. # the $r->mail_body_mangle happens to be the first noteworthy $df
  14387. do_log(4,'defang? ccat "%s": %s', $cc,$df);
  14388. next if !$df;
  14389. my $ccm = ccat_maj($cc);
  14390. if ($ccm==CC_VIRUS) {
  14391. my $virusname_list = $msginfo->virusnames;
  14392. push(@explanation, 'WARNING: contains virus ' .
  14393. (!$virusname_list ? '' : join(", ",@$virusname_list)));
  14394. }
  14395. if ($ccm==CC_BANNED) {
  14396. push(@explanation,
  14397. "WARNING: banning rules detected suspect part(s),\n".
  14398. "do not open unless you know what you are doing");
  14399. }
  14400. if ($ccm==CC_UNCHECKED) {
  14401. if ($hold ne '') {
  14402. push(@explanation,
  14403. "WARNING: NOT CHECKED FOR VIRUSES (mail bomb?):\n $hold");
  14404. } elsif ($any_undecipherable) {
  14405. push(@explanation, "WARNING: contains undecipherable part");
  14406. }
  14407. }
  14408. if ($ccm==CC_BADH) {
  14409. my $bad = join(' ',@bad_headers);
  14410. if (length($bad) > 1000) { $bad = substr($bad,0,1000) . "..." }
  14411. push(@explanation, split(/\n/,
  14412. wrap_string('WARNING: bad headers - '.$bad, 78,'',' ') ));
  14413. }
  14414. push(@explanation, 'WARNING: oversized') if $ccm==CC_OVERSIZED;
  14415. if (!$spam_summary_inserted && # can be both CC_SPAMMY and CC_SPAM
  14416. ($ccm==CC_SPAM || $ccm==CC_SPAMMY)) {
  14417. push(@explanation, split(/\n/, $msginfo->spam_summary));
  14418. $spam_summary_inserted = 1;
  14419. }
  14420. }
  14421. my $s = join(' ',@explanation);
  14422. do_log(1, "DEFANGING MAIL: %s",
  14423. length($s) <= 150 ? $s : substr($s,0,150-3).'[...]');
  14424. for (@explanation)
  14425. { if (length($_) > 100) { $_ = substr($_,0,100-3) . '...' } }
  14426. $_ .= "\n" for (@explanation); # append newlines
  14427. my $d = defanged_mime_entity($msginfo,\@explanation);
  14428. $msginfo->mail_text($d); # substitute mail with a rewritten version
  14429. $msginfo->mail_text_fn(undef); # remove filename information
  14430. $msginfo->mail_text_str(undef); $msginfo->body_start_pos(undef);
  14431. $msginfo->skip_bytes(0);
  14432. $body_modified = 1; section_time('defang');
  14433. }
  14434. # actually the 'for' loop is bogus and runs only once, all recipients
  14435. # listed in the argument are known to be using the same setting for
  14436. # $r->mail_body_mangle, ensured by add_forwarding_header_edits_per_recip;
  14437. # just exit the loop
  14438. last;
  14439. }
  14440. $body_modified;
  14441. }
  14442. sub do_quarantine($$$$;@) {
  14443. shift(@_) if $_[0]->isa('Amavis::In::Connection'); # for compatibility
  14444. my($msginfo, $hdr_edits_inherited, $recips_ref,
  14445. $quarantine_method, @snmp_id) = @_;
  14446. if ($quarantine_method eq '') {
  14447. do_log(5, 'quarantine disabled');
  14448. } else {
  14449. local($1);
  14450. my $quar_m_protocol = !ref $quarantine_method ? $quarantine_method
  14451. : $quarantine_method->[0];
  14452. $quar_m_protocol = lc $1 if $quar_m_protocol =~ /^([a-z][a-z0-9.+-]*):/si;
  14453. my $quar_msg = Amavis::In::Message->new;
  14454. $quar_msg->rx_time($msginfo->rx_time); # copy the reception time
  14455. $quar_msg->log_id($msginfo->log_id); # use the same log_id
  14456. $quar_msg->partition_tag($msginfo->partition_tag); # same partition_tag
  14457. $quar_msg->conn_obj($msginfo->conn_obj);
  14458. $quar_msg->mail_id($msginfo->mail_id); # use the same mail_id
  14459. $quar_msg->body_type($msginfo->body_type); # use the same BODY= type
  14460. $quar_msg->header_8bit($msginfo->header_8bit);
  14461. $quar_msg->body_8bit($msginfo->body_8bit);
  14462. $quar_msg->msg_size($msginfo->msg_size);
  14463. $quar_msg->body_digest($msginfo->body_digest); # copy original digest
  14464. $quar_msg->dsn_ret($msginfo->dsn_ret);
  14465. $quar_msg->dsn_envid($msginfo->dsn_envid);
  14466. $quar_msg->auth_submitter($msginfo->sender_smtp);
  14467. $quar_msg->auth_user(c('amavis_auth_user'));
  14468. $quar_msg->auth_pass(c('amavis_auth_pass'));
  14469. $quar_msg->originating(0); # disables DKIM signing
  14470. my($orig_env_sender_retained, $orig_env_recips_retained);
  14471. my $mftq = c('mailfrom_to_quarantine');
  14472. if (!defined $mftq || $quar_m_protocol =~ /^(?:bsmtp|sql)\z/) {
  14473. # we keep the original envelope sender address if replacement sender
  14474. # is not provided, or with quarantine methods which store to fixed
  14475. # locations which do not depend on envelope
  14476. $quar_msg->sender($msginfo->sender); # original sender
  14477. $quar_msg->sender_smtp($msginfo->sender_smtp);
  14478. $orig_env_sender_retained = 1;
  14479. } elsif (defined $mftq) { # have a replacement and smtp, lmtp, pipe, local
  14480. $quar_msg->sender($mftq);
  14481. $mftq = qquote_rfc2821_local($mftq);
  14482. $quar_msg->sender_smtp($mftq);
  14483. $quar_msg->auth_submitter($mftq);
  14484. }
  14485. my(@recips);
  14486. if (!$recips_ref || $quar_m_protocol =~ /^(?:bsmtp|sql)\z/) {
  14487. # we keep the original envelope recipients if replacement recipients
  14488. # are not provided, or with quarantine methods which store to fixed
  14489. # locations which do not depend on envelope information
  14490. for my $r (@{$msginfo->per_recip_data}) {
  14491. my $recip_obj = Amavis::In::Message::PerRecip->new;
  14492. # copy original recipient addresses and DSN info
  14493. $recip_obj->recip_addr($r->recip_addr);
  14494. $recip_obj->recip_addr_smtp($r->recip_addr_smtp);
  14495. $recip_obj->dsn_orcpt($r->dsn_orcpt);
  14496. $recip_obj->recip_destiny(D_PASS);
  14497. $recip_obj->dsn_notify(['NEVER']) if $orig_env_sender_retained;
  14498. $recip_obj->delivery_method($quarantine_method);
  14499. push(@recips,$recip_obj);
  14500. }
  14501. $orig_env_recips_retained = 1;
  14502. } else { # have a replacement and smtp, lmtp, pipe, local
  14503. # with these quarantine methods the envelope information is used to
  14504. # determine where and how to store a quarantined message, and may not
  14505. # reflect original envelope sender and recipients addresses
  14506. for my $rec (@$recips_ref) { # use recipients provided by a caller
  14507. my $recip_obj = Amavis::In::Message::PerRecip->new;
  14508. $recip_obj->recip_addr($rec);
  14509. $recip_obj->recip_addr_smtp(qquote_rfc2821_local($rec));
  14510. $recip_obj->recip_destiny(D_PASS);
  14511. $recip_obj->dsn_notify(['NEVER']) if $orig_env_sender_retained;
  14512. $recip_obj->delivery_method($quarantine_method);
  14513. push(@recips,$recip_obj);
  14514. }
  14515. }
  14516. $quar_msg->per_recip_data(\@recips);
  14517. my $hdr_edits = Amavis::Out::EditHeader->new;
  14518. $hdr_edits->inherit_header_edits($hdr_edits_inherited);
  14519. if (defined $msginfo->mail_id) {
  14520. $hdr_edits->prepend_header('X-Quarantine-ID', '<'.$msginfo->mail_id.'>');
  14521. }
  14522. if ($quar_m_protocol ne 'bsmtp') {
  14523. # NOTE: RFC 2821 mentions possible header flds X-SMTP-MAIL & X-SMTP-RCPT
  14524. # Exim uses: Envelope-To, Sendmail uses X-Envelope-To;
  14525. # No need with bsmtp, which preserves the envelope.
  14526. my(@blocked_recips) = map($_->recip_addr_smtp,
  14527. grep($_->recip_done, @{$msginfo->per_recip_data}));
  14528. $hdr_edits->prepend_header('X-Envelope-To-Blocked',
  14529. join(",\n ", @blocked_recips), 1);
  14530. $hdr_edits->prepend_header('X-Envelope-To',
  14531. join(",\n ", map($_->recip_addr_smtp, @{$msginfo->per_recip_data})),1);
  14532. }
  14533. if (!$orig_env_sender_retained) { # unless X-Envelope-* would be redundant
  14534. $hdr_edits->prepend_header('X-Envelope-From', $msginfo->sender_smtp);
  14535. }
  14536. $hdr_edits->add_header('Received',
  14537. make_received_header_field($msginfo,1), 1);
  14538. $quar_msg->header_edits($hdr_edits);
  14539. $quar_msg->mail_text($msginfo->mail_text); # use the same mail contents
  14540. $quar_msg->mail_text_str($msginfo->mail_text_str);
  14541. $quar_msg->body_start_pos($msginfo->body_start_pos);
  14542. $quar_msg->skip_bytes($msginfo->skip_bytes);
  14543. if (ll(5)) {
  14544. my $quar_m_displ = !ref $quarantine_method ? $quarantine_method
  14545. : '(' . join(', ',@$quarantine_method) . ')';
  14546. do_log(5,"DO_QUARANTINE, %s, %s -> %s",
  14547. $quar_m_displ, $quar_msg->sender_smtp,
  14548. join(', ', map($_->recip_addr_smtp,
  14549. @{$quar_msg->per_recip_data})) );
  14550. }
  14551. snmp_count('QuarMsgs');
  14552. snmp_count( ['QuarMsgsSize', $quar_msg->msg_size, 'C64'] );
  14553. mail_dispatch($quar_msg, 'Quar', 0);
  14554. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  14555. one_response_for_all($quar_msg, 0); # check status
  14556. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
  14557. @snmp_id = ('Other') if !@snmp_id;
  14558. for (unique_list(@snmp_id)) {
  14559. snmp_count('QuarMsgs'.$_);
  14560. snmp_count( ['QuarMsgsSize'.$_, $quar_msg->msg_size, 'C64'] );
  14561. }
  14562. my $any_arch = grep($_ eq 'Arch', @snmp_id);
  14563. my $any_nonarch = grep($_ ne 'Arch', @snmp_id);
  14564. my $act_perf = $msginfo->actions_performed;
  14565. $msginfo->actions_performed($act_perf=[]) if !$act_perf;
  14566. if ($any_nonarch && !grep($_ eq 'Quarantined', @$act_perf)) {
  14567. push(@$act_perf, 'Quarantined');
  14568. }
  14569. if ($any_arch && !grep($_ eq 'Archived', @$act_perf)) {
  14570. push(@$act_perf, 'Archived');
  14571. }
  14572. } elsif ($n_smtp_resp =~ /^4/) {
  14573. snmp_count('QuarAttemptTempFails');
  14574. die "temporarily unable to quarantine: $n_smtp_resp";
  14575. } else { # abort if quarantining not successful
  14576. snmp_count('QuarAttemptFails');
  14577. die "Can't quarantine: $n_smtp_resp";
  14578. }
  14579. my($q_ty, $q_to, @quar_type, @quar_to);
  14580. $q_ty = $msginfo->quar_type;
  14581. $q_to = $msginfo->quarantined_to;
  14582. @quar_type = ref $q_ty ? @$q_ty : ( $q_ty ) if defined $q_ty;
  14583. @quar_to = ref $q_to ? @$q_to : ( $q_to ) if defined $q_to;
  14584. my(%seen_q_ty); $seen_q_ty{$_}=1 for @quar_type;
  14585. my(%seen_q_to); $seen_q_to{$_}=1 for @quar_to;
  14586. for my $r (@{$quar_msg->per_recip_data}) {
  14587. my $mbxname = $r->recip_mbxname;
  14588. next if !defined $mbxname || $mbxname eq '';
  14589. my $p = $quar_m_protocol;
  14590. $p = $p eq 'smtp' ? 'M' : $p eq 'lmtp' ? 'L' :
  14591. $p eq 'bsmtp' ? 'B' : $p eq 'sql' ? 'Q' :
  14592. $p eq 'local' ? ($mbxname =~ /\@/ ? 'M' :
  14593. $mbxname =~ /\.gz\z/ ? 'Z' : 'F')
  14594. : '?';
  14595. push(@quar_type,$p) if !$seen_q_ty{$p}++;
  14596. push(@quar_to,$mbxname) if !$seen_q_to{$mbxname}++;
  14597. }
  14598. # remember quarantine methods/protocols and locations (quarantined_to)
  14599. $msginfo->quar_type(\@quar_type) if @quar_type;
  14600. $msginfo->quarantined_to(\@quar_to) if @quar_to;
  14601. ll(5) && do_log(5, 'quar_types: %s, quar_to: %s',
  14602. join(',', @quar_type), join(', ', @quar_to));
  14603. do_log(4, 'DO_QUARANTINE done');
  14604. }
  14605. }
  14606. # prepare header edits for the quarantined message
  14607. #
  14608. sub prepare_header_edits_for_quarantine($) {
  14609. my($msginfo) = @_;
  14610. my($blacklisted_any,$whitelisted_any) = (0,0);
  14611. my($do_tag_any,$do_tag2_any,$do_kill_any) = (0,0,0);
  14612. my($tag_level_min,$tag2_level_min,$kill_level_min);
  14613. my(%all_spam_tests);
  14614. my($min_spam_level, $max_spam_level) =
  14615. minmax(map($_->spam_level, @{$msginfo->per_recip_data}));
  14616. for my $r (@{$msginfo->per_recip_data}) {
  14617. my $rec = $r->recip_addr;
  14618. my $spam_level = $r->spam_level;
  14619. if (ll(2)) {
  14620. my $blocking_ccat = $r->blocking_ccat;
  14621. my($rec_ccat_maj,$rec_ccat_min) = ccat_split(
  14622. defined $blocking_ccat ? $blocking_ccat : $r->contents_category);
  14623. my($ccat,$ccat_min) = ccat_split($msginfo->contents_category);
  14624. do_log(2,"header_edits_for_quar: rec_bl_ccat=(%d,%d), ccat=(%d,%d) %s",
  14625. $rec_ccat_maj, $rec_ccat_min, $ccat, $ccat_min, $rec)
  14626. if $rec_ccat_maj != $ccat || $rec_ccat_min != $ccat_min;
  14627. }
  14628. my($tag_level,$tag2_level,$kill_level,$do_tag,$do_tag2,$do_kill);
  14629. $do_tag = $r->is_in_contents_category(CC_CLEAN,1);
  14630. $do_tag2 = $r->is_in_contents_category(CC_SPAMMY);
  14631. $do_kill = $r->is_in_contents_category(CC_SPAM);
  14632. if (!$r->bypass_spam_checks && ($do_tag || $do_tag2 || $do_kill)) {
  14633. # do the more expensive lookups only when needed
  14634. $tag_level = lookup2(0,$rec, ca('spam_tag_level_maps'));
  14635. $tag2_level = lookup2(0,$rec, ca('spam_tag2_level_maps'));
  14636. $kill_level = lookup2(0,$rec, ca('spam_kill_level_maps'));
  14637. }
  14638. # summarize
  14639. $blacklisted_any = 1 if $r->recip_blacklisted_sender;
  14640. $whitelisted_any = 1 if $r->recip_whitelisted_sender;
  14641. $tag_level_min = $tag_level if defined($tag_level) && $tag_level ne '' &&
  14642. (!defined($tag_level_min) || $tag_level < $tag_level_min);
  14643. $tag2_level_min = $tag2_level if defined($tag2_level) &&
  14644. (!defined($tag2_level_min) || $tag2_level < $tag2_level_min);
  14645. $kill_level_min = $kill_level if defined($kill_level) &&
  14646. (!defined($kill_level_min) || $kill_level < $kill_level_min);
  14647. $do_tag_any = 1 if $do_tag;
  14648. $do_tag2_any = 1 if $do_tag2;
  14649. $do_kill_any = 1 if $do_kill;
  14650. my $spam_tests = $r->spam_tests;
  14651. if (defined $spam_tests) {
  14652. $all_spam_tests{$_} = 1 for split(/,/, join(',',map($$_,@$spam_tests)));
  14653. }
  14654. }
  14655. my(%header_field_provided); # mainly applies to spam header fields
  14656. my $use_our_hdrs = cr('prefer_our_added_header_fields');
  14657. my $allowed_hdrs = cr('allowed_added_header_fields');
  14658. my $hdr_edits = Amavis::Out::EditHeader->new;
  14659. if ($allowed_hdrs && $allowed_hdrs->{lc('X-Amavis-Alert')}) {
  14660. if ($msginfo->is_in_contents_category(CC_VIRUS)) {
  14661. my $virusname_list = $msginfo->virusnames;
  14662. $hdr_edits->add_header('X-Amavis-Alert',
  14663. "INFECTED, message contains virus: " .
  14664. (!$virusname_list ? '' : join(", ",@$virusname_list)) );
  14665. }
  14666. if ($msginfo->is_in_contents_category(CC_BANNED)) {
  14667. for my $r (@{$msginfo->per_recip_data}) {
  14668. if (defined($r->banning_reason_short)) {
  14669. $hdr_edits->add_header('X-Amavis-Alert',
  14670. 'BANNED, message contains ' . $r->banning_reason_short);
  14671. last; # fudge: only the first recipient's banned hit will be shown
  14672. }
  14673. }
  14674. }
  14675. if ($msginfo->is_in_contents_category(CC_BADH)) {
  14676. $hdr_edits->add_header('X-Amavis-Alert',
  14677. 'BAD HEADER SECTION '.$bad_headers[0]);
  14678. }
  14679. }
  14680. if ($allowed_hdrs) {
  14681. for ('X-Amavis-OS-Fingerprint') {
  14682. my $p0f = $msginfo->client_os_fingerprint;
  14683. if (defined($p0f) && $p0f ne '' && $allowed_hdrs->{lc $_}) {
  14684. $hdr_edits->add_header($_, sanitize_str($p0f));
  14685. }
  14686. }
  14687. }
  14688. if ($allowed_hdrs && $use_our_hdrs) {
  14689. my $spam_level_bar; my $slc = c('sa_spam_level_char');
  14690. $spam_level_bar = $slc x min(64, $whitelisted_any ? 0
  14691. : $blacklisted_any ? 64
  14692. : 0+$max_spam_level) if $slc ne '';
  14693. # allow header field wrapping at any comma
  14694. my $s = join(",\n ", sort keys %all_spam_tests);
  14695. my $sl = 'x';
  14696. if (defined $min_spam_level) {
  14697. my $minsl = 0+sprintf("%.3f",$min_spam_level);
  14698. my $maxsl = 0+sprintf("%.3f",$max_spam_level);
  14699. $sl = $minsl eq $maxsl ? $minsl : "$minsl..$maxsl";
  14700. }
  14701. my $autolearn_status = $msginfo->supplementary_info('AUTOLEARN');
  14702. my $full_spam_status = sprintf(
  14703. "%s,\n score=%s\n tag=%s\n tag2=%s\n kill=%s\n ".
  14704. "%stests=[%s]\n autolearn=%s",
  14705. $do_tag2_any||$do_kill_any ? 'Yes' : 'No', $sl,
  14706. (map { !defined $_ ? 'x' : 0+sprintf("%.3f",$_) }
  14707. ($tag_level_min, $tag2_level_min, $kill_level_min)),
  14708. join('', $blacklisted_any ? "BLACKLISTED\n " : (),
  14709. $whitelisted_any ? "WHITELISTED\n " : ()),
  14710. $s, $autolearn_status||'unavailable');
  14711. if (ll(2)) {
  14712. # log entry semi-compatible with older log parsers
  14713. my $s = $full_spam_status; $s =~ s/\n[ \t]/ /g;
  14714. do_log(2,"header_edits_for_quar: %s -> %s, %s", $msginfo->sender_smtp,
  14715. join(',', qquote_rfc2821_local(@{$msginfo->recips})), $s);
  14716. }
  14717. for ('X-Spam-Flag') {
  14718. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14719. $hdr_edits->add_header($_, $do_tag2_any ? 'YES' : 'NO');
  14720. $header_field_provided{lc $_} = 1;
  14721. }
  14722. }
  14723. for ('X-Spam-Score') {
  14724. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14725. my $score = 0+$max_spam_level;
  14726. $score = max(64,$score) if $blacklisted_any; # not below 64 if bl
  14727. $score = min( 0,$score) if $whitelisted_any; # not above 0 if wl
  14728. $hdr_edits->add_header($_, 0+sprintf("%.3f",$score));
  14729. $header_field_provided{lc $_} = 1;
  14730. }
  14731. }
  14732. for ('X-Spam-Level') {
  14733. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14734. $hdr_edits->add_header($_, $spam_level_bar) if defined $spam_level_bar;
  14735. $header_field_provided{lc $_} = 1;
  14736. }
  14737. }
  14738. for ('X-Spam-Status') {
  14739. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14740. $hdr_edits->add_header($_, $full_spam_status, 1);
  14741. $header_field_provided{lc $_} = 1;
  14742. }
  14743. }
  14744. for ('X-Spam-Report') {
  14745. if ($allowed_hdrs->{lc $_} && $use_our_hdrs->{lc $_}) {
  14746. my $report = $msginfo->spam_report;
  14747. if (defined $report && $report ne '') {
  14748. $hdr_edits->add_header($_, "\n".sanitize_str($report,1), 2);
  14749. }
  14750. $header_field_provided{lc $_} = 1;
  14751. }
  14752. }
  14753. }
  14754. if ($allowed_hdrs) {
  14755. # add remaining header fields as provided by spam scanners
  14756. my $sa_header = $msginfo->supplementary_info(
  14757. $do_tag2_any ? 'ADDEDHEADERSPAM' : 'ADDEDHEADERHAM');
  14758. if (defined $sa_header && $sa_header ne '') {
  14759. for my $hf (split(/^(?![ \t])/m, $sa_header, -1)) {
  14760. local($1,$2);
  14761. if ($hf =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) {
  14762. my($hf_name,$hf_body) = ($1,$2);
  14763. my $hf_name_lc = lc($hf_name); chomp($hf_body);
  14764. if ($header_field_provided{$hf_name_lc}) {
  14765. do_log(5,'quar: scanner provided %s, but we preferred our own',
  14766. $hf_name);
  14767. } elsif (!$allowed_hdrs->{$hf_name_lc}) {
  14768. do_log(5,'quar: scanner provided %s, '.
  14769. 'inhibited by %%allowed_added_header_fields', $hf_name);
  14770. } else {
  14771. do_log(5,'quar: scanner provided %s, inserting', $hf_name);
  14772. $hdr_edits->add_header($hf_name, $hf_body, 2);
  14773. }
  14774. }
  14775. }
  14776. }
  14777. for my $pair ( ['DSPAMRESULT', 'X-DSPAM-Result'],
  14778. ['DSPAMSIGNATURE', 'X-DSPAM-Signature'],
  14779. ['CRM114STATUS', 'X-CRM114-Status'],
  14780. ['CRM114CACHEID', 'X-CRM114-CacheID'] ) {
  14781. my($suppl_attr_name, $hf_name) = @$pair;
  14782. my $suppl_attr_val = $msginfo->supplementary_info($suppl_attr_name);
  14783. if (defined $suppl_attr_val && $suppl_attr_val ne '') {
  14784. if (!$allowed_hdrs->{lc $hf_name}) {
  14785. do_log(5,'quar: scanner provided %s, '.
  14786. 'inhibited by %%allowed_added_header_fields', $hf_name);
  14787. } else {
  14788. do_log(5,'quar: scanner provided %s, inserting', $hf_name);
  14789. $hdr_edits->add_header($hf_name,
  14790. sanitize_str($suppl_attr_val), 2);
  14791. }
  14792. }
  14793. }
  14794. }
  14795. if (c('enable_dkim_verification') &&
  14796. $allowed_hdrs && $allowed_hdrs->{lc('Authentication-Results')}) {
  14797. for my $h (Amavis::DKIM::generate_authentication_results($msginfo,0)) {
  14798. $hdr_edits->add_header('Authentication-Results', $h, 1);
  14799. }
  14800. }
  14801. section_time('quar-hdrs');
  14802. $hdr_edits;
  14803. }
  14804. # Quarantine according to contents and send admin & recip notif. as needed
  14805. # (this subroutine replaces the former subroutines do_virus and do_spam)
  14806. #
  14807. sub do_notify_and_quarantine($$) {
  14808. my($msginfo, $virus_dejavu) = @_;
  14809. my($mailfrom_admin, $hdrfrom_admin, $notify_admin_templ_ref) =
  14810. map { scalar($msginfo->setting_by_contents_category(cr($_))) }
  14811. qw(mailfrom_notify_admin_by_ccat hdrfrom_notify_admin_by_ccat
  14812. notify_admin_templ_by_ccat);
  14813. my $qar_method = c('archive_quarantine_method');
  14814. my(@ccat_names_pairs) =
  14815. $msginfo->setting_by_main_contents_category_all(\%ccat_display_names);
  14816. my($ccat,$ccat_min) = ccat_split($msginfo->contents_category);
  14817. if (ll(3)) {
  14818. my $ccat_name = ref $ccat_names_pairs[0] ? $ccat_names_pairs[0][1] :undef;
  14819. do_log(3,"do_notify_and_quar: ccat=%s (%d,%d) (%s) ccat_block=(%s)".
  14820. ", qar_mth=%s", $ccat_name, $ccat, $ccat_min,
  14821. join(', ', map(sprintf('"%s":%s', $_->[0], $_->[1]),
  14822. @ccat_names_pairs)),
  14823. $msginfo->blocking_ccat, $qar_method);
  14824. }
  14825. my $virusname_list = $msginfo->virusnames;
  14826. my $newvirus_admin_maps_ref =
  14827. $virusname_list && @$virusname_list && !$virus_dejavu ?
  14828. ca('newvirus_admin_maps') : undef;
  14829. my $archive_any = 0; my $archive_transparent = 1;
  14830. if (defined $qar_method && $qar_method ne '') { # archiving quarantine
  14831. # test if @archive_quarantine_to_maps for all recipients yields
  14832. # a magic placeholder '%a', indicating we want transparent archiving
  14833. # which retains unmodified envelope recipient addresses
  14834. my $aqtm = ca('archive_quarantine_to_maps');
  14835. for my $r (@{$msginfo->per_recip_data}) {
  14836. my $q = lookup2(0, $r->recip_addr, $aqtm);
  14837. $archive_any = 1 if defined $q && $q ne '';
  14838. $archive_transparent = 0 if !defined $q || $q ne '%a';
  14839. last if $archive_any && !$archive_transparent;
  14840. }
  14841. }
  14842. my(@q_tuples,@a_addr); # per-recip quarantine address(es) and admins
  14843. for my $r (@{$msginfo->per_recip_data}) {
  14844. my $rec = $r->recip_addr;
  14845. my $blacklisted = $r->recip_blacklisted_sender;
  14846. my $whitelisted = $r->recip_whitelisted_sender;
  14847. my $spam_level = $r->spam_level;
  14848. # an alternative approach to determining which quarantine and notif. to take
  14849. # my(@qmqta_tuples) = $r->setting_by_main_contents_category_all(
  14850. # cr('quarantine_method_by_ccat'), cr('quarantine_to_maps_by_ccat'),
  14851. # cr('admin_maps_by_ccat') );
  14852. # my $qq; # quarantine (pseudo) address associated with the recipient
  14853. # my $quarantining_reason_ccat;
  14854. # for my $tuple (@qmqta_tuples) {
  14855. # my($cc, $q_method, $quarantine_to_maps_ref, $admin_maps_ref) = @$tuple;
  14856. # if (defined($q_method) && $q_method ne '' && $quarantine_to_maps_ref) {
  14857. # my $q = lookup2(0,$rec,$quarantine_to_maps_ref);
  14858. # if (defined $q && $q ne '')
  14859. # { $qq = $q; $quarantining_reason_ccat = $cc; last }
  14860. # }
  14861. # }
  14862. # my $aa; # administrator's e-mail address
  14863. # my $admin_notif_reason_ccat;
  14864. # for my $tuple (@qmqta_tuples) {
  14865. # my($cc, $q_method, $quarantine_to_maps_ref, $admin_maps_ref) = @$tuple;
  14866. # if ($admin_maps_ref) {
  14867. # my $a = lookup2(0,$rec,$admin_maps_ref);
  14868. # if (defined $a && $a ne '')
  14869. # { $aa = $a; $admin_notif_reason_ccat = $cc; last }
  14870. # }
  14871. # }
  14872. # ($rec_ccat_maj,$rec_ccat_min) = ccat_split($quarantining_reason_ccat);
  14873. my $blocking_ccat = $r->blocking_ccat;
  14874. my($rec_ccat_maj,$rec_ccat_min) = ccat_split(
  14875. defined $blocking_ccat ? $blocking_ccat : $r->contents_category);
  14876. my $q_method =
  14877. $r->setting_by_contents_category(cr('quarantine_method_by_ccat'));
  14878. my $quarantine_to_maps_ref =
  14879. $r->setting_by_contents_category(cr('quarantine_to_maps_by_ccat'));
  14880. # get per-recipient quarantine address(es) and admins
  14881. if (!defined($q_method) || $q_method eq '') {
  14882. do_log(5,"do_notify_and_quarantine: not quarantining, q_method off");
  14883. } elsif (!$quarantine_to_maps_ref) {
  14884. do_log(5,"do_notify_and_quarantine: not quarantining, null q_to maps");
  14885. } else {
  14886. my $q; # quarantine (pseudo) address associated with the recipient
  14887. $q = lookup2(0,$rec,$quarantine_to_maps_ref);
  14888. if (defined $q && $q ne '' &&
  14889. ($rec_ccat_maj==CC_SPAM || $rec_ccat_maj==CC_SPAMMY)) {
  14890. # consider suppressing spam quarantine
  14891. my $cutoff = lookup2(0,$rec, ca('spam_quarantine_cutoff_level_maps'));
  14892. if (!defined $cutoff || $cutoff eq '') {
  14893. # no cutoff, quarantining all
  14894. } elsif ($blacklisted && !$whitelisted) {
  14895. do_log(2,"do_notify_and_quarantine: cutoff, blacklisted");
  14896. $q = ''; # disable quarantine on behalf of this recipient
  14897. } elsif (($spam_level||0) >= $cutoff) {
  14898. do_log(2,"do_notify_and_quarantine: spam level exceeds ".
  14899. "quarantine cutoff level %s", $cutoff);
  14900. $q = ''; # disable quarantine on behalf of this recipient
  14901. }
  14902. }
  14903. # keep original recipient when q_to is '%a' or with BSMTP; some day
  14904. # we may end up doing %k, %a, %l, %u, %e, %d placeholder replacements
  14905. $q = $rec if defined $q && $q ne '' &&
  14906. ($q eq '%a' || $q_method =~ /^bsmtp:/i);
  14907. if (!defined($q) || $q eq '') {
  14908. do_log(5,"do_notify_and_quarantine: not quarantining, q_to off");
  14909. } else {
  14910. my $ccat_name_major =
  14911. $r->setting_by_contents_category(\%ccat_display_names_major);
  14912. push(@q_tuples, [$q_method, $q, $ccat_name_major]);
  14913. }
  14914. }
  14915. my $admin_maps_ref =
  14916. $r->setting_by_contents_category(cr('admin_maps_by_ccat'));
  14917. my $a; # administrator's e-mail address
  14918. $a = lookup2(0,$rec,$admin_maps_ref) if $admin_maps_ref;
  14919. if (defined $a && $a ne '' &&
  14920. ($rec_ccat_maj==CC_SPAM || $rec_ccat_maj==CC_SPAMMY)) {
  14921. # consider suppressing spam admin notifications
  14922. my $cutoff = lookup2(0,$rec, ca('spam_notifyadmin_cutoff_level_maps'));
  14923. if (!defined $cutoff || $cutoff eq '') {
  14924. # no cutoff, sending administrator notifications
  14925. } elsif ($blacklisted && !$whitelisted) {
  14926. do_log(2,"do_notify_and_quarantine: spam admin cutoff, blacklisted");
  14927. $a = ''; # disable admin notification on behalf of this recipient
  14928. } elsif (($spam_level||0) >= $cutoff) {
  14929. do_log(2,"do_notify_and_quarantine: spam level exceeds ".
  14930. "spam admin cutoff level %s", $cutoff);
  14931. $a = ''; # disable admin notification on behalf of this recipient
  14932. }
  14933. }
  14934. push(@a_addr, $a) if defined $a && $a ne '' && !grep($_ eq $a, @a_addr);
  14935. if (ccat_maj($r->contents_category)==CC_VIRUS && $newvirus_admin_maps_ref){
  14936. $a = lookup2(0,$rec,$newvirus_admin_maps_ref);
  14937. push(@a_addr, $a) if defined $a && $a ne '' && !grep($_ eq $a, @a_addr);
  14938. }
  14939. if ($archive_any && !$archive_transparent) { # archiving quarantine
  14940. my $q = lookup2(0,$rec, ca('archive_quarantine_to_maps'));
  14941. # keep original recipient when q_to is '%a' or with BSMTP
  14942. $q = $rec if defined $q && $q ne '' &&
  14943. ($q eq '%a' || $qar_method =~ /^bsmtp:/i);
  14944. push(@q_tuples, [$qar_method, $q, 'Arch']) if defined $q && $q ne '';
  14945. }
  14946. } # endfor per_recip_data
  14947. if ($ccat == CC_SPAM) {
  14948. my $sqbsm = ca('spam_quarantine_bysender_to_maps');
  14949. if (@$sqbsm) { # by-sender spam quarantine (hardly useful, rarely used)
  14950. my $q = lookup2(0,$msginfo->sender, $sqbsm);
  14951. if (defined $q && $q ne '') {
  14952. my $msg_q_method = $msginfo->setting_by_contents_category(
  14953. cr('quarantine_method_by_ccat'));
  14954. push(@q_tuples, [$msg_q_method, $q, 'Spam'])
  14955. if defined $msg_q_method && $msg_q_method ne '';
  14956. }
  14957. }
  14958. }
  14959. section_time('notif-quar');
  14960. if (@q_tuples || $archive_any) {
  14961. if (!defined($msginfo->mail_id) && grep($_->[2] ne 'Arch', @q_tuples)) {
  14962. # delayed mail_id generation - now we really need it
  14963. $zmq_obj->register_proc(2,0,'G',$msginfo->log_id) if $zmq_obj;
  14964. $snmp_db->register_proc(2,0,'G',$msginfo->log_id) if $snmp_db;
  14965. # create a mail_id unique to a database and save preliminary info to SQL
  14966. generate_unique_mail_id($msginfo);
  14967. section_time('gen_mail_id') if $sql_storage;
  14968. }
  14969. # compatibility: replace quarantine method 'local:xxx'
  14970. # with $notify_method when quarantine_to looks like an e-mail address
  14971. my $notif_m = c('notify_method');
  14972. for my $tuple (@q_tuples) {
  14973. my($q_method,$q_to,$ccat_name) = @$tuple;
  14974. $tuple->[0] = $notif_m if $q_method =~ /^local:/i && $q_to =~ /\@/;
  14975. }
  14976. my $hdr_edits = prepare_header_edits_for_quarantine($msginfo);
  14977. if (@q_tuples) {
  14978. do_log(4,"do_notify_and_quarantine: quarantine %s",
  14979. join(',', map($_->[1], @q_tuples)));
  14980. my(@q_tuples_tmp) = @q_tuples;
  14981. while (@q_tuples_tmp) {
  14982. my($q_method,$q_to,$ccat_name) = @{$q_tuples_tmp[0]};
  14983. my(@same_method_tuples) = grep($_->[0] eq $q_method, @q_tuples_tmp);
  14984. @q_tuples_tmp = grep($_->[0] ne $q_method, @q_tuples_tmp);
  14985. my(@q_to) = unique_list(map($_->[1], @same_method_tuples));
  14986. # per-recipient blocking ccat names select snmp counter names
  14987. my(@snmp_id) = unique_list(map($_->[2], @same_method_tuples));
  14988. do_quarantine($msginfo, $hdr_edits, \@q_to, $q_method, @snmp_id);
  14989. }
  14990. }
  14991. if ($archive_any && $archive_transparent) {
  14992. # transparent archiving retains envelope recipient addresses
  14993. do_log(4,"do_notify_and_quarantine: transparent archiving");
  14994. do_quarantine($msginfo, $hdr_edits, undef, $qar_method, 'Arch');
  14995. }
  14996. }
  14997. if (!@a_addr) {
  14998. do_log(4,"skip admin notification, no administrators");
  14999. } elsif (!ref($notify_admin_templ_ref) ||
  15000. (ref($notify_admin_templ_ref) eq 'ARRAY' ?
  15001. !@$notify_admin_templ_ref : $$notify_admin_templ_ref eq '')) {
  15002. do_log(5,"skip admin notifications - empty template");
  15003. } else { # notify per-recipient administrators
  15004. ll(5) && do_log(5, "Admin notifications to %s; sender: %s",
  15005. join(',',qquote_rfc2821_local(@a_addr)),
  15006. $msginfo->sender_smtp);
  15007. $hdrfrom_admin = expand_variables($hdrfrom_admin);
  15008. my $mailfrom_admin_q;
  15009. if (!defined($mailfrom_admin)) {
  15010. # defaults to email address in hdrfrom_notify_admin
  15011. $mailfrom_admin_q = (parse_address_list($hdrfrom_admin))[0];
  15012. $mailfrom_admin = unquote_rfc2821_local($mailfrom_admin_q);
  15013. }
  15014. $mailfrom_admin_q = qquote_rfc2821_local($mailfrom_admin);
  15015. my $notification = Amavis::In::Message->new;
  15016. $notification->rx_time($msginfo->rx_time); # copy the reception time
  15017. $notification->log_id($msginfo->log_id); # copy log id
  15018. $notification->partition_tag($msginfo->partition_tag); # same partition_tag
  15019. $notification->conn_obj($msginfo->conn_obj);
  15020. $notification->originating(1);
  15021. $notification->sender($mailfrom_admin);
  15022. $notification->sender_smtp($mailfrom_admin_q);
  15023. $notification->auth_submitter($mailfrom_admin_q);
  15024. $notification->auth_user(c('amavis_auth_user'));
  15025. $notification->auth_pass(c('amavis_auth_pass'));
  15026. $notification->recips([@a_addr]);
  15027. my $notif_m = c('notify_method');
  15028. $_->delivery_method($notif_m) for @{$notification->per_recip_data};
  15029. my(@rfc2822_from_admin) = map(unquote_rfc2821_local($_),
  15030. parse_address_list($hdrfrom_admin));
  15031. $notification->rfc2822_from($rfc2822_from_admin[0]);
  15032. # if ($mailfrom_admin ne '')
  15033. # { $_->dsn_notify(['NEVER']) for @{$notification->per_recip_data} }
  15034. my(%mybuiltins) = %builtins; # make a local copy
  15035. $mybuiltins{'T'} = [qquote_rfc2821_local(@a_addr)]; # used in To:
  15036. $mybuiltins{'f'} = $hdrfrom_admin; # From:
  15037. $notification->mail_text(
  15038. build_mime_entity(expand($notify_admin_templ_ref,\%mybuiltins),
  15039. $msginfo, undef,undef,0, 1,0) );
  15040. # $notification->body_type('7BIT');
  15041. my $hdr_edits = Amavis::Out::EditHeader->new;
  15042. $notification->header_edits($hdr_edits);
  15043. mail_dispatch($notification, 'Notif', 0);
  15044. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  15045. one_response_for_all($notification, 0); # check status
  15046. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
  15047. } elsif ($n_smtp_resp =~ /^4/) {
  15048. die "temporarily unable to notify admin: $n_smtp_resp";
  15049. } else {
  15050. do_log(-1, "FAILED to notify admin: %s", $n_smtp_resp);
  15051. }
  15052. # $notification->purge;
  15053. }
  15054. # recipient notifications
  15055. my $wrmbc = cr('warnrecip_maps_by_ccat');
  15056. for my $r (@{$msginfo->per_recip_data}) {
  15057. my $rec = $r->recip_addr;
  15058. # if ($r->is_in_contents_category(CC_SPAM)) {
  15059. # if ($wrmbc->{&CC_VIRUS}) {
  15060. # $wrmbc = { %$wrmbc }; # copy
  15061. # delete $wrmbc->{&CC_VIRUS};
  15062. # do_log(5,"disabling virus recipient notifications for infected spam");
  15063. # }
  15064. # }
  15065. my $warnrecip_maps_ref = $r->setting_by_contents_category($wrmbc);
  15066. my $wr; my $notify_recips_templ_ref;
  15067. $wr = lookup2(0,$rec,$warnrecip_maps_ref) if $warnrecip_maps_ref;
  15068. if ($wr) {
  15069. $notify_recips_templ_ref =
  15070. $r->setting_by_contents_category(cr('notify_recips_templ_by_ccat'));
  15071. if (!ref($notify_recips_templ_ref) ||
  15072. (ref($notify_recips_templ_ref) eq 'ARRAY' ?
  15073. !@$notify_recips_templ_ref : $$notify_recips_templ_ref eq '')){
  15074. do_log(5,"skip recipient notifications - empty template");
  15075. $wr = 0; # do not send empty notifications
  15076. } elsif (!c('warn_offsite') && !$r->recip_is_local) {
  15077. do_log(5,"skip recipient notifications - nonlocal recipient");
  15078. $wr = 0; # do not notify foreign recipients
  15079. # } elsif ($r->recip_destiny == D_PASS) {
  15080. # do_log(5,"skip recipient notifications - mail will be delivered");
  15081. # $wr = 0; # do not notify recips which will be getting a message anyway
  15082. # } elsif ($msginfo->sender eq '') { # (not general enough)
  15083. # do_log(5,"skip recipient notifications for null sender");
  15084. # $wr = 0;
  15085. }
  15086. }
  15087. if ($wr) { # warn recipient
  15088. my $mailfrom_recip =
  15089. $r->setting_by_contents_category(cr('mailfrom_notify_recip_by_ccat'));
  15090. my $hdrfrom_recip =
  15091. $r->setting_by_contents_category(cr('hdrfrom_notify_recip_by_ccat'));
  15092. $hdrfrom_recip = expand_variables($hdrfrom_recip);
  15093. my $mailfrom_recip_q;
  15094. if (!defined($mailfrom_recip)) {
  15095. # defaults to email address in hdrfrom_notify_recip
  15096. $mailfrom_recip_q = (parse_address_list($hdrfrom_recip))[0];
  15097. $mailfrom_recip = unquote_rfc2821_local($mailfrom_recip_q);
  15098. }
  15099. $mailfrom_recip_q = qquote_rfc2821_local($mailfrom_recip);
  15100. my $notification = Amavis::In::Message->new;
  15101. $notification->rx_time($msginfo->rx_time); # copy the reception time
  15102. $notification->log_id($msginfo->log_id); # copy log id
  15103. $notification->partition_tag($msginfo->partition_tag); # same partition
  15104. $notification->conn_obj($msginfo->conn_obj);
  15105. $notification->originating(1);
  15106. $notification->sender($mailfrom_recip);
  15107. $notification->sender_smtp($mailfrom_recip_q);
  15108. $notification->auth_submitter($mailfrom_recip_q);
  15109. $notification->auth_user(c('amavis_auth_user'));
  15110. $notification->auth_pass(c('amavis_auth_pass'));
  15111. $notification->recips([$rec]);
  15112. my $notif_m = c('notify_method');
  15113. $_->delivery_method($notif_m) for @{$notification->per_recip_data};
  15114. my(@rfc2822_from_recip) = map(unquote_rfc2821_local($_),
  15115. parse_address_list($hdrfrom_recip));
  15116. $notification->rfc2822_from($rfc2822_from_recip[0]);
  15117. # if ($mailfrom_recip ne '')
  15118. # { $_->dsn_notify(['NEVER']) for @{$notification->per_recip_data} }
  15119. my(@b); @b = @{$r->banned_parts} if defined $r->banned_parts;
  15120. my $b_chopped = @b > 2; @b = (@b[0,1],'...') if $b_chopped;
  15121. s/[ \t]{6,}/ ... /g for @b;
  15122. my(%mybuiltins) = %builtins; # make a local copy
  15123. $mybuiltins{'banned_parts'} = \@b; # list of banned parts
  15124. $mybuiltins{'F'} = $r->banning_reason_short; # just one name & comment
  15125. $mybuiltins{'banning_rule_comment'} =
  15126. !defined($r->banning_rule_comment) ? undef
  15127. : unique_ref($r->banning_rule_comment);
  15128. $mybuiltins{'banning_rule_rhs'} =
  15129. !defined($r->banning_rule_rhs) ? undef
  15130. : unique_ref($r->banning_rule_rhs);
  15131. $mybuiltins{'f'} = $hdrfrom_recip; # From:
  15132. $mybuiltins{'T'} = qquote_rfc2821_local($rec); # To:
  15133. $notification->mail_text(
  15134. build_mime_entity(expand($notify_recips_templ_ref,\%mybuiltins),
  15135. $msginfo, undef,undef,0, 0,0) );
  15136. # $notification->body_type('7BIT');
  15137. my $hdr_edits = Amavis::Out::EditHeader->new;
  15138. $notification->header_edits($hdr_edits);
  15139. mail_dispatch($notification, 'Notif', 0);
  15140. my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
  15141. one_response_for_all($notification, 0); # check status
  15142. if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok
  15143. } elsif ($n_smtp_resp =~ /^4/) {
  15144. die "temporarily unable to notify recipient rec: $n_smtp_resp";
  15145. } else {
  15146. do_log(-1, "FAILED to notify recipient %s: %s", $rec,$n_smtp_resp);
  15147. }
  15148. # $notification->purge;
  15149. }
  15150. }
  15151. do_log(5, "do_notify_and_quarantine - done");
  15152. }
  15153. # Calculate a message body digest;
  15154. # While at it, also get message size, verify DKIM signatures, check for 8-bit
  15155. # data, collect entropy, and store original header section since we need it
  15156. # for the %H macro, and MIME::Tools may modify its copy.
  15157. #
  15158. sub get_body_digest($$) {
  15159. my($msginfo, $alg) = @_;
  15160. my($remaining_time, $dkim_deadline) = # sanity limit for DKIM verification
  15161. get_deadline('get_body_digest', 0.5, 8, 30);
  15162. prolong_timer('digest_pre'); # restart the timer
  15163. my($hctx,$bctx);
  15164. # choose a message digest: MD5: 128 bits (32 hex), SHA family: 160..512 bits
  15165. if (uc $alg eq 'MD5') { $hctx = Digest::MD5->new; $bctx = Digest::MD5->new }
  15166. else { $hctx = Digest::SHA->new($alg); $bctx = Digest::SHA->new($alg) }
  15167. my $dkim_verifier;
  15168. $dkim_verifier = Mail::DKIM::Verifier->new if c('enable_dkim_verification');
  15169. # section_time('digest_init');
  15170. my($header_size, $body_size, $h_8bit, $b_8bit) = (0) x 4;
  15171. my $orig_header = []; # array of header fields, with folding and trailing NL
  15172. my $orig_header_fields = {};
  15173. my $sanity_limit = 4*1024*1024; # 4 MiB header size sanity limit
  15174. my $dkim_sanity_limit = 256*1024; # 256 KiB header size sanity limit
  15175. my $msg = $msginfo->mail_text;
  15176. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  15177. $msg = $msg_str_ref if ref $msg_str_ref;
  15178. my $pos = 0;
  15179. if (!defined $msg) {
  15180. # empty mail
  15181. $msginfo->body_start_pos(0);
  15182. } elsif (ref $msg eq 'SCALAR') {
  15183. do_log(5, "get_body_digest: reading header section from memory");
  15184. my $header;
  15185. $pos = min($msginfo->skip_bytes, length($$msg));
  15186. if ($pos >= length($$msg)) { # empty message
  15187. $header = ''; $pos = length($$msg);
  15188. } elsif (substr($$msg,$pos,1) eq "\n") { # empty header section
  15189. $header = ''; $pos++;
  15190. } else {
  15191. my $ind = index($$msg, "\n\n", $pos); # find header/body separator
  15192. $header = $ind < 0 ? substr($$msg, $pos)
  15193. : substr($$msg, $pos, $ind+1-$pos);
  15194. $h_8bit = 1 if $header =~ tr/\000-\177//c;
  15195. $hctx->add($header);
  15196. $pos = $ind < 0 ? length($$msg) : $ind+2;
  15197. }
  15198. # $pos now points to the first byte of a body
  15199. $msginfo->body_start_pos($pos);
  15200. local($1); my($j,$k,$ln);
  15201. for ($j = 0; $j < length($header); $j = $k+1) {
  15202. $k = index($header, "\n", $j);
  15203. $ln = $k < 0 ? substr($header, $j) : substr($header, $j, $k-$j+1);
  15204. if ($ln =~ /^[ \t]/) { # header field continuation
  15205. $$orig_header[-1] .= $ln; # includes NL
  15206. } else { # starts a new header field
  15207. push(@$orig_header, $ln); # includes NL
  15208. if ($ln =~ /^([^: \t]+)[ \t]*:/) {
  15209. # remember array index of each occurrence of a header field, top down
  15210. my $curr_entry = $orig_header_fields->{lc($1)};
  15211. if (!defined $curr_entry) {
  15212. # optimized: if there is only one element, it is stored as itself
  15213. $orig_header_fields->{lc($1)} = $#$orig_header;
  15214. } elsif (ref $curr_entry) { # already an arrayref, append
  15215. push(@{$orig_header_fields->{lc($1)}}, $#$orig_header);
  15216. } else { # was a single element as a scalar, now there are two
  15217. $orig_header_fields->{lc($1)} = [ $curr_entry, $#$orig_header ];
  15218. }
  15219. }
  15220. }
  15221. last if $k < 0;
  15222. }
  15223. $header =~ s{\n}{\015\012}gs; # needed for DKIM and for size
  15224. $header_size = length($header); # size includes CRLF (RFC 1870)
  15225. if (defined $dkim_verifier) {
  15226. eval {
  15227. $dkim_verifier->PRINT($header)
  15228. or die "Error writing mail header to DKIM: $!";
  15229. 1;
  15230. } or do {
  15231. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  15232. do_log(-1,"Error feeding header to DKIM verifier: %s",$eval_stat);
  15233. undef $dkim_verifier;
  15234. };
  15235. }
  15236. } elsif ($msg->isa('MIME::Entity')) {
  15237. die "get_body_digest: reading from a MIME::Entity object not implemented";
  15238. } else { # a file handle assumed
  15239. do_log(5, "get_body_digest: reading header section from a file");
  15240. $pos = $msginfo->skip_bytes; # should be 0, but anyway...
  15241. $msg->seek($pos,0) or die "Can't rewind mail file: $!";
  15242. # read mail header section
  15243. local($1); my $ln;
  15244. for ($! = 0; defined($ln=$msg->getline); $! = 0) {
  15245. $pos += length($ln);
  15246. last if $ln eq "\n";
  15247. $hctx->add($ln);
  15248. $h_8bit = 1 if !$h_8bit && $ln =~ tr/\000-\177//c;
  15249. if ($ln =~ /^[ \t]/) { # header field continuation
  15250. $$orig_header[-1] .= $ln; # including NL
  15251. } else { # starts a new header field
  15252. push(@$orig_header,$ln); # including NL
  15253. if ($ln =~ /^([^: \t]+)[ \t]*:/) {
  15254. # remember array index of each occurrence of a header field, top down
  15255. my $curr_entry = $orig_header_fields->{lc($1)};
  15256. if (!defined $curr_entry) {
  15257. # optimized: if there is only one element, it is stored as itself
  15258. $orig_header_fields->{lc($1)} = $#$orig_header;
  15259. } elsif (ref $curr_entry) { # already an arrayref, append
  15260. push(@{$orig_header_fields->{lc($1)}}, $#$orig_header);
  15261. } else { # was a single element as a scalar, now there are two
  15262. $orig_header_fields->{lc($1)} = [ $curr_entry, $#$orig_header ];
  15263. }
  15264. }
  15265. }
  15266. chomp($ln);
  15267. if (!defined $dkim_verifier) {
  15268. # don't bother
  15269. } elsif ($header_size > $dkim_sanity_limit) {
  15270. do_log(-1,"Stopped feeding header to DKIM verifier: ".
  15271. "%.0f KiB sanity limit exceeded", $dkim_sanity_limit/1024);
  15272. undef $dkim_verifier;
  15273. } elsif (Time::HiRes::time > $dkim_deadline) {
  15274. do_log(-1,"Stopped feeding header to DKIM verifier: deadline exceeded");
  15275. undef $dkim_verifier;
  15276. } else {
  15277. eval {
  15278. $dkim_verifier->PRINT($ln."\015\012")
  15279. or die "Error writing mail header to DKIM: $!";
  15280. 1;
  15281. } or do {
  15282. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  15283. do_log(-1,"Error feeding header line to DKIM verifier: %s",
  15284. $eval_stat);
  15285. undef $dkim_verifier;
  15286. };
  15287. }
  15288. $header_size += length($ln)+2; # size includes CRLF (RFC 1870)
  15289. # exceeded $sanity_limit will break DKIM signatures, too bad...
  15290. last if $header_size > $sanity_limit;
  15291. }
  15292. defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug
  15293. $! == EBADF ? do_log(0,"Error reading mail header section: $!")
  15294. : die "Error reading mail header section: $!";
  15295. $msginfo->body_start_pos($pos);
  15296. }
  15297. add_entropy($hctx->digest);
  15298. if (defined $dkim_verifier) {
  15299. do_log(5, "get_body_digest: sending h/b separator to DKIM");
  15300. eval {
  15301. # h/b separator will trigger signature pre-processing in DKIM module
  15302. $dkim_verifier->PRINT("\015\012")
  15303. or die "Error writing h/b separator to DKIM: $!";
  15304. 1;
  15305. } or do {
  15306. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  15307. do_log(-1,"Error feeding h/b separ to DKIM verifier: %s", $eval_stat);
  15308. undef $dkim_verifier;
  15309. };
  15310. }
  15311. $header_size += 2; # include a separator CRLF line in a header section size
  15312. untaint_inplace($header_size); # length(tainted) stays tainted too
  15313. section_time('digest_hdr');
  15314. # a DNS lookup in Mail::DKIM older than 0.30 stops the timer!
  15315. # The lookup is performed at a header/body separator line or at CLOSE, at
  15316. # which point signatures become available through the $dkim_verifier object.
  15317. prolong_timer('digest_hdr'); # restart timer if stopped
  15318. my(@dkim_signatures);
  15319. @dkim_signatures = $dkim_verifier->signatures if defined $dkim_verifier;
  15320. # don't bother feeding body to DKIM if there are no signature header fields
  15321. my $feed_dkim = @dkim_signatures > 0;
  15322. if ($feed_dkim) {
  15323. $msginfo->checks_performed({}) if !$msginfo->checks_performed;
  15324. $msginfo->checks_performed->{D} = 1;
  15325. }
  15326. if (!defined $msg) {
  15327. # empty mail
  15328. } elsif (ref $msg eq 'SCALAR') {
  15329. do_log(5, "get_body_digest: reading mail body from memory");
  15330. my($buff, $buff_l);
  15331. while ($pos < length($$msg)) {
  15332. # do it in chunks to avoid unnecessarily large memory use
  15333. # for temporary variables; also helps keeping it all in a CPU cache
  15334. $buff = substr($$msg,$pos,16384); $buff_l = length($buff);
  15335. $pos += $buff_l;
  15336. $bctx->add($buff);
  15337. $b_8bit = 1 if !$b_8bit && ($buff =~ tr/\000-\177//c);
  15338. if (!$feed_dkim) {
  15339. # count \n, compensating for CRLF (RFC 1870)
  15340. $body_size += $buff_l + ($buff =~ tr/\n//);
  15341. } else {
  15342. $buff =~ s{\n}{\015\012}gs;
  15343. $body_size += length($buff);
  15344. eval {
  15345. $dkim_verifier->PRINT($buff)
  15346. or die "Error writing mail body to DKIM: $!";
  15347. 1;
  15348. } or do {
  15349. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  15350. do_log(-1,"Error feeding body to DKIM verifier: %s",$eval_stat);
  15351. undef $dkim_verifier;
  15352. };
  15353. }
  15354. }
  15355. } elsif ($msg->isa('MIME::Entity')) {
  15356. die "get_body_digest: reading from MIME::Entity is not implemented";
  15357. } else {
  15358. #*** # only read further if not already at end-of-file
  15359. do_log(5, "get_body_digest: reading mail body from a file");
  15360. my($buff, $buff_l);
  15361. while (($buff_l = $msg->read($buff,65536)) > 0) {
  15362. $bctx->add($buff);
  15363. $b_8bit = 1 if !$b_8bit && ($buff =~ tr/\000-\177//c);
  15364. if (!$feed_dkim) {
  15365. # count \n, compensating for CRLF (RFC 1870)
  15366. $body_size += $buff_l + ($buff =~ tr/\n//);
  15367. } else {
  15368. $buff =~ s{\n}{\015\012}gs;
  15369. $body_size += length($buff);
  15370. eval {
  15371. $dkim_verifier->PRINT($buff)
  15372. or die "Error writing mail body to DKIM: $!";
  15373. 1;
  15374. } or do {
  15375. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  15376. do_log(-1,"Error feeding body to DKIM verifier: %s",$eval_stat);
  15377. undef $dkim_verifier;
  15378. };
  15379. }
  15380. }
  15381. defined $buff_l or die "Error reading mail body: $!";
  15382. }
  15383. if (defined $dkim_verifier) {
  15384. eval {
  15385. # this will trigger signature verification in the DKIM module
  15386. $dkim_verifier->CLOSE or die "Can't close dkim_verifier: $!";
  15387. 1;
  15388. } or do {
  15389. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  15390. do_log(-1,"Error closing DKIM verifier: %s",$eval_stat);
  15391. undef $dkim_verifier;
  15392. };
  15393. @dkim_signatures = $dkim_verifier->signatures if defined $dkim_verifier;
  15394. }
  15395. prolong_timer('digest_body'); # restart timer if stopped
  15396. my $body_digest = untaint($bctx->digest);
  15397. add_entropy($body_digest);
  15398. # store information obtained
  15399. if (@dkim_signatures) {
  15400. if (@dkim_signatures > 50) { # sanity
  15401. do_log(-1, "Too many DKIM or DK signatures (%d), truncating to 50",
  15402. scalar(@dkim_signatures));
  15403. $#dkim_signatures = 49;
  15404. }
  15405. $msginfo->dkim_signatures_all(\@dkim_signatures);
  15406. }
  15407. if (ll(5)) {
  15408. my $mail_size_old = $msginfo->msg_size;
  15409. my $mail_size_new = $header_size + $body_size;
  15410. if (defined($mail_size_old) && $mail_size_new != $mail_size_old) {
  15411. # copy_smtp_data() provides a message size which is not adjusted for
  15412. # dot-destuffing - for speed. We finely adjust the message size here,
  15413. # now that we have the necessary information available.
  15414. do_log(5, "get_body_digest: message size adjusted %d -> %d, ".
  15415. "header+sep %d, body %d",
  15416. $mail_size_old, $mail_size_new, $header_size, $body_size);
  15417. } else {
  15418. do_log(5, "get_body_digest: message size %d, header+sep %d, body %d",
  15419. $mail_size_new, $header_size, $body_size);
  15420. }
  15421. }
  15422. $msginfo->msg_size($header_size + $body_size);
  15423. $msginfo->orig_header_fields($orig_header_fields); # stores just indices
  15424. $msginfo->orig_header($orig_header); # header section, without separator line
  15425. $msginfo->orig_header_size($header_size); # size includes a separator line!
  15426. $msginfo->orig_body_size($body_size);
  15427. my $body_digest_hex = unpack('H*', $body_digest); # high nybble first
  15428. # store hex-encoded to retain backward compatibility with pre-2.8.0
  15429. $msginfo->body_digest($body_digest_hex);
  15430. $msginfo->header_8bit($h_8bit ? 1 : 0);
  15431. $msginfo->body_8bit($b_8bit ? 1 : 0);
  15432. # check for 8-bit characters and adjust body type if necessary (RFC 1652)
  15433. my $bt_orig = $msginfo->body_type;
  15434. $bt_orig = !defined($bt_orig) ? '' : uc($bt_orig);
  15435. if ($h_8bit || $b_8bit) {
  15436. # just keep original label whatever it is (garbage-in - garbage-out);
  15437. # keeping 8-bit mail unlabeled might avoid breaking DKIM in transport
  15438. # (labeling as 8-bit may invoke 8>7 downgrades in MTA, breaking signatures)
  15439. } elsif ($bt_orig eq '') { # unlabeled on reception
  15440. $msginfo->body_type('7BIT'); # safe to label
  15441. } elsif ($bt_orig eq '8BITMIME') { # redundant (quite common)
  15442. $msginfo->body_type('7BIT'); # turn a redundant 8BITMIME into 7BIT
  15443. }
  15444. if (ll(4)) {
  15445. my $msg_fmt =
  15446. ($bt_orig eq '' && $b_8bit) ? "%s, but 8-bit body"
  15447. : ($bt_orig eq '' && $h_8bit) ? "%s, but 8-bit header"
  15448. : ($bt_orig eq '7BIT' && ($h_8bit || $b_8bit)) ? "%s inappropriately"
  15449. : ($bt_orig eq '8BITMIME' && !($h_8bit || $b_8bit)) ? "%s unnecessarily"
  15450. : "%s, good";
  15451. do_log(4, "body type (ESMTP BODY): $msg_fmt (h=%s, b=%s)",
  15452. $bt_orig eq '' ? 'unlabeled' : "labeled $bt_orig", $h_8bit,$b_8bit);
  15453. }
  15454. do_log(3, "body hash: %s", $body_digest_hex);
  15455. section_time(defined $dkim_verifier ? 'digest_body_dkim' : 'digest_body');
  15456. $body_digest_hex;
  15457. }
  15458. sub find_program_path($$) {
  15459. my($fv_list, $path_list_ref) = @_;
  15460. $fv_list = [$fv_list] if !ref $fv_list;
  15461. my $found;
  15462. for my $fv (@$fv_list) { # search through alternatives
  15463. my(@fv_cmd) = split(' ',$fv);
  15464. my $cmd = $fv_cmd[0];
  15465. if (!@fv_cmd) {
  15466. # empty, not available
  15467. } elsif ($cmd =~ m{^/}s) { # absolute path
  15468. my $errn = stat($cmd) ? 0 : 0+$!;
  15469. if ($errn == ENOENT) {
  15470. # file does not exist
  15471. } elsif ($errn) {
  15472. do_log(-1, "find_program_path: %s inaccessible: %s", $cmd,$!);
  15473. } elsif (-x _ && !-d _) {
  15474. $found = join(' ', @fv_cmd);
  15475. }
  15476. } elsif ($cmd =~ m{/}s) { # relative path
  15477. die "find_program_path: relative paths not implemented: @fv_cmd\n";
  15478. } else { # walk through the specified PATH
  15479. for my $p (@$path_list_ref) {
  15480. my $errn = stat("$p/$cmd") ? 0 : 0+$!;
  15481. if ($errn == ENOENT) {
  15482. # file does not exist
  15483. } elsif ($errn) {
  15484. do_log(-1, "find_program_path: %s/%s inaccessible: %s", $p,$cmd,$!);
  15485. } elsif (-x _ && !-d _) {
  15486. $found = $p . '/' . join(' ', @fv_cmd);
  15487. last;
  15488. }
  15489. }
  15490. }
  15491. last if defined $found;
  15492. }
  15493. $found;
  15494. }
  15495. sub find_external_programs($) {
  15496. my($path_list_ref) = @_;
  15497. for my $f (qw($file $altermime)) {
  15498. my $g = $f; $g =~ s/\$/Amavis::Conf::/; my $fv_list = eval('$' . $g);
  15499. my $found = find_program_path($fv_list, $path_list_ref);
  15500. { no strict 'refs'; $$g = $found } # NOTE: a symbolic reference
  15501. if (!defined $found) { do_log(0,"No %-19s not using it", "$f,") }
  15502. else {
  15503. do_log(0,"Found %-16s at %s%s", $f,
  15504. $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
  15505. $found);
  15506. }
  15507. }
  15508. # map program name path hints to full paths for decoders
  15509. my(%any_st);
  15510. for my $f (@{ca('decoders')}) {
  15511. next if !defined $f || !ref $f; # empty, skip
  15512. my $short_types = $f->[0];
  15513. if (!defined $short_types || (ref $short_types && !@$short_types))
  15514. { undef $f; next }
  15515. my(@tried,@found); my $any = 0;
  15516. for my $d (@$f[2..$#$f]) { # all but the first two elements are programs
  15517. # find the program, allow one level of indirection
  15518. my $dd = (ref $d eq 'SCALAR' || ref $d eq 'REF') ? $$d : $d;
  15519. my $found = find_program_path($dd, $path_list_ref);
  15520. if (defined $found) {
  15521. $any = 1; $d = $dd = $found; push(@found,$dd);
  15522. } else {
  15523. push(@tried, !ref($dd) ? $dd : join(", ",@$dd)) if $dd ne '';
  15524. undef $d;
  15525. }
  15526. }
  15527. my $any_in_use;
  15528. for my $short_type (ref $short_types ? @$short_types : $short_types) {
  15529. my $is_a_backup = $any_st{$short_type};
  15530. my($ll,$tier) = !$is_a_backup ? (0,'') : (2,' (backup, not used)');
  15531. if (@$f <= 2) { # no external programs specified
  15532. if (!$is_a_backup) { $any_in_use = 1; $any_st{$short_type} = 1 }
  15533. do_log($ll, "Internal decoder for .%-4s%s", $short_type,$tier);
  15534. } elsif (!$any) { # external programs specified but none found
  15535. do_log($ll, "No ext program for .%s, tried: %s",
  15536. $short_type, join('; ',@tried)) if @tried && !$is_a_backup;
  15537. } else {
  15538. if (!$is_a_backup) { $any_in_use = 1; $any_st{$short_type} = 1 }
  15539. do_log($ll, "Found decoder for .%-4s at %s%s%s", $short_type,
  15540. $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
  15541. join('; ',@found), $tier);
  15542. }
  15543. # defined but false, collect a list of tried short types as hash keys
  15544. $any_st{$short_type} = 0 if !defined $any_st{$short_type};
  15545. }
  15546. if (!$any_in_use) {
  15547. undef $f; # discard a backup entry
  15548. } else {
  15549. # turn array (in the first element) into a hash
  15550. $f->[0] = { map(($_,1), @$short_types) } if ref $short_types;
  15551. }
  15552. }
  15553. for my $short_type (sort grep(!$any_st{$_}, keys %any_st)) {
  15554. do_log(0, "No decoder for .%-4s", $short_type);
  15555. }
  15556. # map program name hints to full paths - av scanners
  15557. my $tier = 'primary'; # primary, secondary, ... av scanners
  15558. for my $f (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
  15559. if ($f eq "\000") { # next tier
  15560. $tier = 'secondary';
  15561. } elsif (!defined $f || !ref $f) {
  15562. # empty, skip
  15563. } elsif (ref($f->[1]) eq 'CODE') {
  15564. do_log(0, "Using %s internal av scanner code for %s", $tier,$f->[0]);
  15565. } else {
  15566. my $found = $f->[1] = find_program_path($f->[1], $path_list_ref);
  15567. if (!defined $found) {
  15568. do_log(3, "No %s av scanner: %s", $tier, $f->[0]);
  15569. undef $f; # release its storage
  15570. } else {
  15571. do_log(0, "Found %s av scanner %-11s at %s%s", $tier, $f->[0],
  15572. $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
  15573. $found);
  15574. }
  15575. }
  15576. }
  15577. for my $f (@{ca('spam_scanners')}) {
  15578. if (!defined $f || !ref $f) {
  15579. # empty, skip
  15580. } elsif ($f->[1] ne 'Amavis::SpamControl::ExtProg') {
  15581. do_log(5, "Using internal spam scanner code for %s", $f->[0]);
  15582. } else { # using the Amavis::SpamControl::ExtProg interface module
  15583. my $found = $f->[2] = find_program_path($f->[2], $path_list_ref);
  15584. if (!defined $found) {
  15585. do_log(3, "No spam scanner: %s", $f->[0]);
  15586. undef $f; # release its storage
  15587. } else {
  15588. do_log(0, "Found spam scanner %-11s at %s%s", $f->[0],
  15589. $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
  15590. $found);
  15591. }
  15592. }
  15593. }
  15594. }
  15595. # Fetch remaining modules, all must be loaded before chroot and fork occurs
  15596. #
  15597. sub fetch_modules_extra() {
  15598. my(@modules,@optmodules);
  15599. if ($extra_code_sql_base) {
  15600. push(@modules, 'DBI');
  15601. for (@lookup_sql_dsn, @storage_sql_dsn) {
  15602. my(@dsn) = split(/:/,$_->[0],-1);
  15603. push(@modules, 'DBD::'.$dsn[1]) if uc($dsn[0]) eq 'DBI';
  15604. }
  15605. }
  15606. push(@modules, qw(Net::LDAP Net::LDAP::Util Net::LDAP::Search
  15607. Net::LDAP::Bind Net::LDAP::Extension)) if $extra_code_ldap;
  15608. if ($extra_code_dkim ||
  15609. c('tls_security_level_in') || c('tls_security_level_out')) {
  15610. push(@modules, qw(Crypt::OpenSSL::RSA));
  15611. }
  15612. if (c('tls_security_level_in') || c('tls_security_level_out')) {
  15613. push(@modules, qw(IO::Socket::SSL
  15614. Net::SSLeay auto::Net::SSLeay::ssl_write_all
  15615. auto::Net::SSLeay::ssl_read_until
  15616. auto::Net::SSLeay::dump_peer_certificate));
  15617. }
  15618. push(@modules, qw(Net::DNS::RR::TXT Text::ParseWords
  15619. auto::Crypt::OpenSSL::RSA::new_public_key)) if $extra_code_dkim;
  15620. push(@modules, 'Anomy::Sanitizer') if $enable_anomy_sanitizer;
  15621. Amavis::Boot::fetch_modules('REQUIRED ADDITIONAL MODULES', 1, @modules);
  15622. push(@optmodules, qw(
  15623. bytes bytes_heavy.pl utf8 utf8_heavy.pl
  15624. Encode Encode::Byte Encode::MIME::Header Encode::Unicode::UTF7
  15625. Encode::CN Encode::TW Encode::KR Encode::JP
  15626. unicore::To::Lower.pl unicore::To::Upper.pl
  15627. unicore::To::Fold.pl unicore::To::Title.pl unicore::To::Digit.pl
  15628. unicore::lib::Perl::Alnum.pl unicore::lib::Perl::SpacePer.pl
  15629. unicore::lib::Perl::Word.pl
  15630. unicore::lib::Alpha::Y.pl unicore::lib::Nt::De.pl
  15631. ));
  15632. if (@Amavis::Conf::decoders &&
  15633. grep { exists $policy_bank{$_}{'bypass_decode_parts'} &&
  15634. !do { my $v = $policy_bank{$_}{'bypass_decode_parts'};
  15635. !ref $v ? $v : $$v } } keys %policy_bank)
  15636. { # at least one bypass_decode_parts is explicitly false
  15637. push(@modules, qw(Convert::TNEF Convert::UUlib Archive::Zip));
  15638. # push(@modules, qw(Archive::Tar)); # terrible, don't use it!
  15639. }
  15640. push(@optmodules, $] >= 5.012000 ? qw(unicore::Heavy.pl)
  15641. : qw(unicore::Canonical.pl unicore::Exact.pl unicore::PVA.pl));
  15642. # unicore::lib::Perl::Word.pl unicore::lib::Perl::SpacePer.pl
  15643. # unicore::lib::Perl::Alnum.pl unicore::lib::Alpha::Y.pl
  15644. # unicore::lib::Nt::De.pl unicore::lib::Hex::Y.pl
  15645. push(@optmodules, qw(IO::Socket::IP IO::Socket::INET6 Unix::Getrusage));
  15646. push(@optmodules, 'Authen::SASL') if $extra_code_ldap &&
  15647. !grep($_ eq 'Authen::SASL', @modules);
  15648. push(@optmodules, defined($min_servers) ? 'Net::Server::PreFork'
  15649. : 'Net::Server::PreForkSimple');
  15650. push(@optmodules, @additional_perl_modules);
  15651. my $missing;
  15652. $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0,
  15653. @optmodules) if @optmodules;
  15654. do_log(2, 'INFO: no optional modules: %s', join(' ',@$missing))
  15655. if ref $missing && @$missing;
  15656. # require minimal version 0.32, Net::LDAP::Util::escape_filter_value() needed
  15657. Net::LDAP->VERSION(0.32) if $extra_code_ldap;
  15658. # needed a working last_insert_id in the past, no longer so but nevertheless:
  15659. DBI->VERSION(1.43) if $extra_code_sql_base;
  15660. MIME::Entity->VERSION != 5.419
  15661. or die "MIME::Entity 5.419 breaks quoted-printable encoding, ".
  15662. "please upgrade to 5.420 or later (or use 5.418)";
  15663. # load optional modules SAVI and Mail::ClamAV if available and requested
  15664. if ($extra_code_antivirus) {
  15665. my $clamav_module_ok;
  15666. for my $entry (@{ca('av_scanners')}, @{ca('av_scanners_backup')}) {
  15667. if (ref($entry) ne 'ARRAY') {
  15668. # none
  15669. } elsif ($entry->[0] eq 'Sophos SAVI') {
  15670. if (defined(eval { require SAVI }) && SAVI->VERSION(0.30) &&
  15671. Amavis::AV::sophos_savi_init(@$entry)) {} # ok, loaded
  15672. else { undef $entry->[1] } # disable entry
  15673. } elsif ($entry->[0] =~ /^Mail::ClamAV/) {
  15674. if (!defined($clamav_module_ok)) {
  15675. $clamav_module_ok = eval { require Mail::ClamAV };
  15676. $clamav_module_ok = 0 if !defined $clamav_module_ok;
  15677. }
  15678. undef $entry->[1] if !$clamav_module_ok; # disable entry
  15679. }
  15680. }
  15681. }
  15682. }
  15683. sub usage() {
  15684. my $myprogram_name = c('myprogram_name');
  15685. return <<"EOD";
  15686. Usage:
  15687. $myprogram_name
  15688. [-u user] [-g group]
  15689. [-i instance_name] {-c config_file}
  15690. [-d log_level,area,...]
  15691. [-m max_servers] {-p listen_port_or_socket}
  15692. [-L lock_file] [-P pid_file] [-H home_dir]
  15693. [-D db_home_dir | -D ''] [-Q quarantine_dir | -Q '']
  15694. [-R chroot_dir | -R ''] [-S helpers_home_dir] [-T tempbase_dir]
  15695. ( [start] | stop | reload | restart | debug | debug-sa | foreground |
  15696. showkeys {domains} | testkeys {domains} | genrsa file_name [nbits]
  15697. convert_keysfile file_name )
  15698. where area is a SpamAssassin debug area, e.g. all,util,rules,plugin,dkim,dcc
  15699. or:
  15700. $myprogram_name (-h | -V) ... show help or version, then exit
  15701. EOD
  15702. }
  15703. # drop privileges
  15704. #
  15705. sub drop_priv($$) {
  15706. my($desired_user,$desired_group) = @_;
  15707. local($1);
  15708. my($username,$passwd,$uid,$gid) =
  15709. $desired_user=~/^(\d+)$/ ? (undef,undef,$1,undef) :getpwnam($desired_user);
  15710. defined $uid or die "drop_priv: No such username: $desired_user\n";
  15711. if ($desired_group eq '') { $desired_group = $gid } # for logging purposes
  15712. else { $gid = $desired_group=~/^(\d+)$/ ? $1 : getgrnam($desired_group) }
  15713. defined $gid or die "drop_priv: No such group: $desired_group\n";
  15714. $( = $gid; $) = "$gid $gid"; # real and effective GID
  15715. POSIX::setgid($gid) or die "drop_priv: Can't setgid to $gid: $!";
  15716. POSIX::setuid($uid) or die "drop_priv: Can't setuid to $uid: $!";
  15717. $> = $uid; $< = $uid; # just in case
  15718. # print STDERR "desired user=$desired_user ($uid), current: EUID: $> ($<)\n";
  15719. # print STDERR "desired group=$desired_group ($gid), current: EGID: $) ($()\n";
  15720. $> != 0 or die "drop_priv: Still running as root, aborting\n";
  15721. $< != 0 or die "Effective UID changed, but Real UID is 0, aborting\n";
  15722. }
  15723. #
  15724. # Main program starts here
  15725. #
  15726. stir_random();
  15727. add_entropy($], @INC, %ENV);
  15728. delete @ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
  15729. # Read dynamic source code, and logging and notification message templates
  15730. # from the end of this file (pseudo file handle DATA)
  15731. #
  15732. $Amavis::Conf::notify_spam_admin_templ = ''; # not used
  15733. $Amavis::Conf::notify_spam_recips_templ = ''; # not used
  15734. do {
  15735. local($/) = "__DATA__\n"; # set line terminator to this string
  15736. for (
  15737. $extra_code_zmq, $extra_code_db,
  15738. $extra_code_sql_lookup, $extra_code_ldap,
  15739. $extra_code_in_ampdp, $extra_code_in_smtp, $extra_code_in_courier,
  15740. $extra_code_out_smtp, $extra_code_out_pipe,
  15741. $extra_code_out_bsmtp, $extra_code_out_local, $extra_code_p0f,
  15742. $extra_code_sql_base, $extra_code_sql_log, $extra_code_sql_quar,
  15743. $extra_code_antivirus, $extra_code_antispam,
  15744. $extra_code_antispam_extprog,
  15745. $extra_code_antispam_spamc, $extra_code_antispam_sa,
  15746. $extra_code_unpackers, $extra_code_dkim, $extra_code_tools)
  15747. { $_ = <Amavis::DATA>;
  15748. defined($_) or die "Error reading optional code from the source file: $!";
  15749. chomp($_);
  15750. }
  15751. binmode(\*Amavis::DATA, ':encoding(UTF-8)')
  15752. or die "Can't set \*DATA encoding to UTF-8: $!";
  15753. for (
  15754. $Amavis::Conf::log_short_templ,
  15755. $Amavis::Conf::log_verbose_templ,
  15756. $Amavis::Conf::log_recip_templ,
  15757. $Amavis::Conf::notify_sender_templ,
  15758. $Amavis::Conf::notify_virus_sender_templ,
  15759. $Amavis::Conf::notify_virus_admin_templ,
  15760. $Amavis::Conf::notify_virus_recips_templ,
  15761. $Amavis::Conf::notify_spam_sender_templ,
  15762. $Amavis::Conf::notify_spam_admin_templ,
  15763. $Amavis::Conf::notify_release_templ,
  15764. $Amavis::Conf::notify_report_templ,
  15765. $Amavis::Conf::notify_autoresp_templ)
  15766. { $_ = <Amavis::DATA>;
  15767. defined($_) or die "Error reading templates from the source file: $!";
  15768. chomp($_);
  15769. }
  15770. }; # restore line terminator
  15771. close(\*Amavis::DATA) or die "Error closing *Amavis::DATA: $!";
  15772. # close(STDIN) or die "Error closing STDIN: $!";
  15773. # note: don't close STDIN just yet to prevent some other file taking up fd 0
  15774. STDERR->autoflush(1);
  15775. { local($1);
  15776. s/^(.*?)[\r\n]+\z/$1/s # discard trailing NL
  15777. for ($Amavis::Conf::log_short_templ,
  15778. $Amavis::Conf::log_verbose_templ,
  15779. $Amavis::Conf::log_recip_templ);
  15780. };
  15781. $Amavis::Conf::log_templ = $Amavis::Conf::log_short_templ;
  15782. umask(0027); # set our preferred umask
  15783. POSIX::setlocale(LC_TIME,'C'); # English dates required in syslog and RFC 5322
  15784. # using Net::Server internal mechanism for a restart on HUP
  15785. $warm_restart = defined $ENV{BOUND_SOCKETS} && $ENV{BOUND_SOCKETS} ne '' ?1:0;
  15786. # Consider dropping privileges early, before reading a config file.
  15787. # This is only possible if running under chroot will not be needed.
  15788. #
  15789. my $desired_group; # defaults to $desired_user's group
  15790. my $desired_user; # username or UID
  15791. if ($> != 0) { $desired_user = $> } # use effective UID if not root
  15792. # collect and parse command line options
  15793. my($log_level_override, $max_servers_override);
  15794. my($myhome_override, $tempbase_override, $helpers_home_override);
  15795. my($quarantinedir_override, $db_home_override, $daemon_chroot_dir_override);
  15796. my($lock_file_override, $pid_file_override);
  15797. my(@listen_sockets_override, $listen_sockets_overridden);
  15798. my(@argv) = @ARGV; # preserve @ARGV, may modify @argv
  15799. while (@argv >= 2 && $argv[0] =~ /^-[ugdimcpDHLPQRST]\z/ ||
  15800. @argv >= 1 && $argv[0] =~ /^-/) {
  15801. my($opt,$val);
  15802. $opt = shift @argv;
  15803. $val = shift @argv if $opt !~ /^-[hV-]\z/; # these take no arguments
  15804. if ($opt eq '--') {
  15805. last;
  15806. } elsif ($opt eq '-h') { # -h (help)
  15807. die "$myversion\n\n" . usage();
  15808. } elsif ($opt eq '-V') { # -V (version)
  15809. die "$myversion\n";
  15810. } elsif ($opt eq '-u') { # -u username
  15811. if ($> == 0) { $desired_user = $val }
  15812. else { print STDERR "Ignoring option -u when not running as root\n" }
  15813. } elsif ($opt eq '-g') { # -g group
  15814. print STDERR "NOTICE: Option -g may not achieve desired result when ".
  15815. "running as non-root\n" if $> != 0 && $val ne $desired_group;
  15816. $desired_group = $val;
  15817. } elsif ($opt eq '-i') { # -i instance_name, may be of use to a .conf file
  15818. $val =~ /^[a-z0-9._+-]*\z/i or die "Special chars in option -i $val\n";
  15819. $instance_name = untaint($val); # not used by amavisd directly
  15820. } elsif ($opt eq '-d') { # -d log_level or -d SAdbg1,SAdbg2,..,SAdbg3
  15821. $log_level_override = untaint($val);
  15822. } elsif ($opt eq '-m') { # -m max_servers
  15823. $val =~ /^\+?\d+\z/ or die "Option -m requires a numeric argument\n";
  15824. $max_servers_override = untaint($val);
  15825. } elsif ($opt eq '-c') { # -c config_file
  15826. push(@config_files, untaint($val)) if $val ne '';
  15827. } elsif ($opt eq '-p') { # -p port_or_socket
  15828. $listen_sockets_overridden = 1; # may disable all sockets by -p ''
  15829. push(@listen_sockets_override, untaint($val)) if $val ne '';
  15830. } elsif ($opt eq '-D') { # -D db_home_dir, empty string turns off db use
  15831. $db_home_override = untaint($val);
  15832. } elsif ($opt eq '-H') { # -H home_dir
  15833. $myhome_override = untaint($val) if $val ne '';
  15834. } elsif ($opt eq '-L') { # -L lock_file
  15835. $lock_file_override = untaint($val) if $val ne '';
  15836. } elsif ($opt eq '-P') { # -P pid_file
  15837. $pid_file_override = untaint($val) if $val ne '';
  15838. } elsif ($opt eq '-Q') { # -Q quarantine_dir, empty string disables quarant.
  15839. $quarantinedir_override = untaint($val);
  15840. } elsif ($opt eq '-R') { # -R chroot_dir, empty string or '/' avoids chroot
  15841. $daemon_chroot_dir_override = $val eq '/' ? '' : untaint($val);
  15842. } elsif ($opt eq '-S') { # -S helpers_home_dir for SA
  15843. $helpers_home_override = untaint($val) if $val ne '';
  15844. } elsif ($opt eq '-T') { # -T tempbase_dir
  15845. $tempbase_override = untaint($val) if $val ne '';
  15846. } else {
  15847. die "Error in parsing command line options: $opt\n\n" . usage();
  15848. }
  15849. }
  15850. my $cmd = lc(shift @argv);
  15851. if ($cmd !~ /^(?:start|debug|debug-sa|foreground|reload|restart|stop|
  15852. showkeys?|testkeys?|genrsa|convert_keysfile)?\z/xs) {
  15853. die "$myversion:\n Unknown command line parameter: $cmd\n\n" . usage();
  15854. } elsif (@argv > 0 &&
  15855. $cmd !~ /^(:?showkeys?|testkeys?|genrsa|convert_keysfile)/xs) {
  15856. die sprintf("%s:\n Only one command line parameter allowed: %s\n\n%s\n",
  15857. $myversion, join(' ',@argv), usage());
  15858. }
  15859. # deal with debugging early, based on a command line arg
  15860. if ($cmd =~ /^(?:start|debug|debug-sa|foreground)?\z/) {
  15861. $daemonize=0 if $cmd eq 'foreground';
  15862. $daemonize=0, $DEBUG=1 if $cmd eq 'debug';
  15863. $daemonize=0, $sa_debug=1 if $cmd eq 'debug-sa';
  15864. }
  15865. if (!defined($desired_user)) {
  15866. # early dropping of privileges not requested
  15867. } elsif ($> != 0 && $< != 0) {
  15868. # early dropping of privileges not needed
  15869. } elsif (defined $daemon_chroot_dir_override &&
  15870. $daemon_chroot_dir_override ne '') {
  15871. # early dropping of privs would prevent later chroot and is to be skipped
  15872. } else {
  15873. # drop privileges early if an uid was specified on a command line, option -u
  15874. drop_priv($desired_user,$desired_group);
  15875. }
  15876. if ($cmd eq 'genrsa') {
  15877. eval $extra_code_tools or die "Problem in Amavis::Tools code: $@";
  15878. $extra_code_tools = 1; Amavis::Tools::generate_dkim_private_key(@argv);
  15879. exit(0);
  15880. }
  15881. if ($cmd eq 'convert_keysfile') {
  15882. eval $extra_code_tools or die "Problem in Amavis::Tools code: $@";
  15883. $extra_code_tools = 1; Amavis::Tools::convert_dkim_keys_file(@argv);
  15884. exit(0);
  15885. }
  15886. # these settings must be overridden before and after read_config
  15887. # because some other settings in a config file may be derived from them
  15888. $Amavis::Conf::MYHOME = $myhome_override if defined $myhome_override;
  15889. $Amavis::Conf::TEMPBASE = $tempbase_override if defined $tempbase_override;
  15890. $Amavis::Conf::QUARANTINEDIR = $quarantinedir_override
  15891. if defined $quarantinedir_override;
  15892. $Amavis::Conf::helpers_home = $helpers_home if defined $helpers_home;
  15893. $Amavis::Conf::daemon_chroot_dir = $daemon_chroot_dir_override
  15894. if defined $daemon_chroot_dir_override;
  15895. # some remaining initialization, possibly after dropping privileges by -u,
  15896. # but before reading configuration file
  15897. init_local_delivery_aliases();
  15898. init_builtin_macros();
  15899. $instance_name = '' if !defined $instance_name;
  15900. # convert arrayref to Amavis::Lookup::RE object, the Amavis::Lookup::RE module
  15901. # was not yet available during BEGIN phase
  15902. $Amavis::Conf::map_full_type_to_short_type_re =
  15903. Amavis::Lookup::RE->new(@$Amavis::Conf::map_full_type_to_short_type_re);
  15904. # default location of the config file if none specified
  15905. if (!@config_files) {
  15906. @config_files = ( '/etc/amavisd.conf' );
  15907. # # Debian/Ubuntu specific:
  15908. # @config_files = Amavis::Util::find_config_files('/usr/share/amavis/conf.d',
  15909. # '/etc/amavis/conf.d');
  15910. }
  15911. # Read and evaluate config files, which may override default settings
  15912. Amavis::Conf::include_config_files(@config_files);
  15913. Amavis::Conf::supply_after_defaults();
  15914. update_current_log_level();
  15915. add_entropy($Amavis::Conf::myhostname, $Amavis::Conf::myversion_date);
  15916. # not needed any longer, reclaim storage
  15917. undef $Amavis::Conf::log_short_templ;
  15918. undef $Amavis::Conf::log_verbose_templ;
  15919. if (defined $desired_user && $daemon_user ne '') {
  15920. local($1);
  15921. # compare the config file settings to current UID
  15922. my($username,$passwd,$uid,$gid) =
  15923. $daemon_user=~/^(\d+)$/ ? (undef,undef,$1,undef) : getpwnam($daemon_user);
  15924. ($desired_user eq $daemon_user || $desired_user eq $uid)
  15925. or warn sprintf("WARN: running under user '%s' (UID=%s), ".
  15926. "the config file specifies \$daemon_user='%s' (UID=%s)\n",
  15927. $desired_user, $>, $daemon_user, defined $uid ? $uid : '?');
  15928. }
  15929. if ($> != 0 && $< != 0) {
  15930. # dropping of privs is not needed
  15931. } elsif (defined $daemon_chroot_dir && $daemon_chroot_dir ne '') {
  15932. # dropping of privs now would prevent later chroot and is to be skipped
  15933. } else { # drop privileges, unless needed for chrooting
  15934. drop_priv($daemon_user,$daemon_group);
  15935. }
  15936. # override certain config file options by command line arguments
  15937. my(@sa_debug_fac); # list of SA debug facilities
  15938. if (defined $log_level_override) {
  15939. for my $item (split(/[ \t]*,[ \t]*/,$log_level_override,-1)) {
  15940. if ($item =~ /^[+-]?\d+\z/) { $Amavis::Conf::log_level = $item }
  15941. elsif ($item =~ /^[A-Za-z0-9_-]+\z/) { push(@sa_debug_fac,$item) }
  15942. }
  15943. update_current_log_level();
  15944. }
  15945. $Amavis::Conf::MYHOME = $myhome_override if defined $myhome_override;
  15946. $Amavis::Conf::TEMPBASE = $tempbase_override if defined $tempbase_override;
  15947. $Amavis::Conf::QUARANTINEDIR = $quarantinedir_override
  15948. if defined $quarantinedir_override;
  15949. $Amavis::Conf::helpers_home = $helpers_home if defined $helpers_home;
  15950. $Amavis::Conf::daemon_chroot_dir = $daemon_chroot_dir_override
  15951. if defined $daemon_chroot_dir_override;
  15952. if (defined $db_home_override) {
  15953. if ($db_home_override =~ /^\s*\z/) { $enable_db = 0 }
  15954. else { $Amavis::Conf::db_home = $db_home_override }
  15955. }
  15956. if (defined $max_servers_override && $max_servers_override ne '') {
  15957. $Amavis::Conf::max_servers = $max_servers_override;
  15958. }
  15959. if ($cmd =~ /^(?:showkeys?|testkeys?)\z/) {
  15960. # useful for preparing DNS zone files and testing public keys in DNS
  15961. eval $extra_code_dkim or die "Problem in Amavis::DKIM code: $@";
  15962. $extra_code_dkim = 1; Amavis::DKIM::dkim_key_postprocess();
  15963. eval $extra_code_tools or die "Problem in Amavis::Tools code: $@";
  15964. $extra_code_tools = 1; # release memory occupied by the source code
  15965. Amavis::Tools::show_or_test_dkim_public_keys($cmd,\@argv);
  15966. exit(0);
  15967. }
  15968. undef $extra_code_tools; # no longer needed
  15969. for ($unix_socketname, $inet_socket_port) {
  15970. push(@listen_sockets, ref $_ ? @$_ : $_) if defined $_ && $_ ne '';
  15971. }
  15972. @listen_sockets = @listen_sockets_override if $listen_sockets_overridden;
  15973. for my $s (@listen_sockets) {
  15974. # convert to a Net::Server::Proto syntax
  15975. local($1);
  15976. if ($s =~ m{^unix:(/\S+)\z}s) { $s = "$1|unix" }
  15977. elsif ($s =~ m{^inet:(.*)\z}s) { $s = "$1/tcp" }
  15978. elsif ($s =~ m{^inet6:(.*)\z}s) { $s = "$1/tcp" }
  15979. elsif ($s =~ m{^/\S+}s) { $s = "$s|unix" }
  15980. elsif ($s =~ m{^\d+\z}s) { $s = "$s/tcp" } # port number
  15981. elsif ($s =~ m{^[^/|]+\z}s) { $s = "$s/tcp" } # almost anything goes
  15982. elsif ($s =~ m{^.+\z}s) { $s = "$s" } # anything goes
  15983. else { die "Socket specification syntax error: $s\n" }
  15984. }
  15985. @listen_sockets > 0 or die "No listen sockets or ports specified\n";
  15986. # %modules_basic = %INC; # helps to track missing modules in chroot
  15987. # compile optional modules if needed
  15988. # NOTE: when releasing memory occupied by the source code, keep in mind:
  15989. # use undef(), see: http://www.perlmonks.org/?node_id=803515
  15990. if (!$enable_zmq) {
  15991. undef $extra_code_zmq;
  15992. } else {
  15993. eval $extra_code_zmq
  15994. or die "Problem in Amavis::ZMQ code: $@";
  15995. # release memory occupied by the source code
  15996. undef $extra_code_zmq; $extra_code_zmq = 1;
  15997. }
  15998. if (!$enable_db) {
  15999. undef $extra_code_db;
  16000. } else {
  16001. eval $extra_code_db
  16002. or die "Problem in Amavis::DB or Amavis::DB::SNMP code: $@";
  16003. # release memory occupied by the source code
  16004. undef $extra_code_db; $extra_code_db = 1;
  16005. }
  16006. { my $any_dkim_verification =
  16007. scalar(grep { my $v = $policy_bank{$_}{'enable_dkim_verification'};
  16008. !ref $v ? $v : $$v } keys %policy_bank);
  16009. my $any_dkim_signing =
  16010. scalar(grep { my $v = $policy_bank{$_}{'enable_dkim_signing'};
  16011. !ref $v ? $v : $$v } keys %policy_bank);
  16012. if (!$any_dkim_verification && !$any_dkim_signing) {
  16013. undef $extra_code_dkim;
  16014. } else {
  16015. eval $extra_code_dkim or die "Problem in Amavis::DKIM code: $@";
  16016. # release memory occupied by the source code
  16017. undef $extra_code_dkim; $extra_code_dkim = 1;
  16018. }
  16019. if ($any_dkim_signing) {
  16020. Amavis::DKIM::dkim_key_postprocess();
  16021. } else { # release storage
  16022. undef %dkim_signing_keys_by_domain;
  16023. undef @dkim_signing_keys_list; undef @dkim_signing_keys_storage;
  16024. }
  16025. }
  16026. { my(%needed_protocols_in);
  16027. for my $bank_name (keys %policy_bank) {
  16028. my $var = $policy_bank{$bank_name}{'protocol'};
  16029. $var = $$var if ref($var) eq 'SCALAR'; # allow one level of indirection
  16030. $needed_protocols_in{$var} = 1 if defined $var;
  16031. }
  16032. # compatibility with older config files unaware of $protocol config variable
  16033. # $needed_protocols_in{'AM.CL'} = 1 # AM.CL is no longer supported
  16034. # if grep(m{\|unix\z}i, @listen_sockets) &&
  16035. # !grep($needed_protocols_in{$_}, qw(AM.PDP COURIER));
  16036. $needed_protocols_in{'SMTP'} = 1
  16037. if grep(m{/(?:tcp|ssleay|ssl)\z}i, @listen_sockets) &&
  16038. !grep($needed_protocols_in{$_}, qw(SMTP LMTP QMQPqq));
  16039. if ($needed_protocols_in{'AM.PDP'} || $needed_protocols_in{'AM.CL'}) {
  16040. eval $extra_code_in_ampdp or die "Problem in the In::AMPDP code: $@";
  16041. # release memory occupied by the source code
  16042. undef $extra_code_in_ampdp; $extra_code_in_ampdp = 1;
  16043. } else {
  16044. undef $extra_code_in_ampdp;
  16045. }
  16046. if ($needed_protocols_in{'SMTP'} || $needed_protocols_in{'LMTP'}) {
  16047. eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@";
  16048. # release memory occupied by the source code
  16049. undef $extra_code_in_smtp; $extra_code_in_smtp = 1;
  16050. } else {
  16051. undef $extra_code_in_smtp;
  16052. }
  16053. if ($needed_protocols_in{'COURIER'}) {
  16054. eval $extra_code_in_courier or die "Problem in the In::Courier code: $@";
  16055. # release memory occupied by the source code
  16056. undef $extra_code_in_courier; $extra_code_in_courier = 1;
  16057. } else {
  16058. undef $extra_code_in_courier;
  16059. }
  16060. if ($needed_protocols_in{'QMQPqq'}) { die "In::QMQPqq code not available" }
  16061. }
  16062. if (!@lookup_sql_dsn) { undef $extra_code_sql_lookup }
  16063. if (!@storage_sql_dsn) { undef $extra_code_sql_log }
  16064. # sql quarantine depends on sql log
  16065. undef $extra_code_sql_quar if !defined $extra_code_sql_log;
  16066. { my(%needed_protocols_out); local($1);
  16067. for my $bank_name (keys %policy_bank) {
  16068. for my $method_name (qw(
  16069. forward_method notify_method resend_method
  16070. release_method requeue_method
  16071. os_fingerprint_method virus_quarantine_method
  16072. banned_files_quarantine_method unchecked_quarantine_method
  16073. spam_quarantine_method bad_header_quarantine_method
  16074. clean_quarantine_method archive_quarantine_method )) {
  16075. local($1); my $var = $policy_bank{$bank_name}{$method_name};
  16076. $var = $$var if ref($var) eq 'SCALAR'; # allow one level of indirection
  16077. $needed_protocols_out{uc($1)} = 1 if $var =~ /^([a-z][a-z0-9.+-]*):/si;
  16078. }
  16079. }
  16080. if (!$needed_protocols_out{'SMTP'} &&
  16081. !$needed_protocols_out{'LMTP'}) { undef $extra_code_out_smtp }
  16082. else {
  16083. eval $extra_code_out_smtp or die "Problem in Amavis::Out::SMTP code: $@";
  16084. # release memory occupied by the source code
  16085. undef $extra_code_out_smtp; $extra_code_out_smtp = 1;
  16086. }
  16087. if (!$needed_protocols_out{'PIPE'}) { undef $extra_code_out_pipe }
  16088. else {
  16089. eval $extra_code_out_pipe or die "Problem in Amavis::Out::Pipe code: $@";
  16090. # release memory occupied by the source code
  16091. undef $extra_code_out_pipe; $extra_code_out_pipe = 1;
  16092. }
  16093. if (!$needed_protocols_out{'BSMTP'}) { undef $extra_code_out_bsmtp }
  16094. else {
  16095. eval $extra_code_out_bsmtp or die "Problem in Amavis::Out::BSMTP code: $@";
  16096. # release memory occupied by the source code
  16097. undef $extra_code_out_bsmtp; $extra_code_out_bsmtp = 1;
  16098. }
  16099. if (!$needed_protocols_out{'LOCAL'}) { undef $extra_code_out_local }
  16100. else {
  16101. eval $extra_code_out_local or die "Problem in Amavis::Out::Local code: $@";
  16102. # release memory occupied by the source code
  16103. undef $extra_code_out_local; $extra_code_out_local = 1;
  16104. }
  16105. if (!$needed_protocols_out{'SQL'}) { undef $extra_code_sql_quar }
  16106. else {
  16107. # deal with it in the next section
  16108. }
  16109. if (!$needed_protocols_out{'P0F'}) { undef $extra_code_p0f }
  16110. else {
  16111. eval $extra_code_p0f or die "Problem in OS_Fingerprint code: $@";
  16112. # release memory occupied by the source code
  16113. undef $extra_code_p0f; $extra_code_p0f = 1;
  16114. }
  16115. }
  16116. if (!defined($extra_code_sql_log) && !defined($extra_code_sql_quar) &&
  16117. !defined($extra_code_sql_lookup)) {
  16118. undef $extra_code_sql_base;
  16119. } else {
  16120. eval $extra_code_sql_base or die "Problem in Amavis SQL base code: $@";
  16121. # release memory occupied by the source code
  16122. undef $extra_code_sql_base; $extra_code_sql_base = 1;
  16123. }
  16124. if (defined $extra_code_sql_log) {
  16125. eval $extra_code_sql_log or die "Problem in Amavis::SQL::Log code: $@";
  16126. # release memory occupied by the source code
  16127. undef $extra_code_sql_log; $extra_code_sql_log = 1;
  16128. }
  16129. if (defined $extra_code_sql_quar) {
  16130. eval $extra_code_sql_quar
  16131. or die "Problem in Amavis::SQL::Quarantine code: $@";
  16132. # release memory occupied by the source code
  16133. undef $extra_code_sql_quar; $extra_code_sql_quar = 1;
  16134. }
  16135. if (defined $extra_code_sql_lookup) {
  16136. eval $extra_code_sql_lookup or die "Problem in Amavis SQL lookup code: $@";
  16137. # release memory occupied by the source code
  16138. undef $extra_code_sql_lookup; $extra_code_sql_lookup = 1;
  16139. }
  16140. if (!grep { my $v = $policy_bank{$_}{'enable_ldap'};
  16141. !ref $v ? $v : $$v } keys %policy_bank) {
  16142. undef $extra_code_ldap;
  16143. } else { # at least one enable_ldap is true
  16144. eval $extra_code_ldap or die "Problem in Lookup::LDAP code: $@";
  16145. # release memory occupied by the source code
  16146. undef $extra_code_ldap; $extra_code_ldap = 1;
  16147. }
  16148. my $bpvcm = ca('bypass_virus_checks_maps');
  16149. if (!@{ca('av_scanners')} && !@{ca('av_scanners_backup')}) {
  16150. undef $extra_code_antivirus;
  16151. } elsif (@$bpvcm && !ref($bpvcm->[0]) && $bpvcm->[0]) {
  16152. # do a simple-minded test to make it easy to turn off virus checks
  16153. undef $extra_code_antivirus;
  16154. } else {
  16155. eval $extra_code_antivirus or die "Problem in antivirus code: $@";
  16156. # release memory occupied by the source code
  16157. undef $extra_code_antivirus; $extra_code_antivirus = 1;
  16158. }
  16159. if (!$extra_code_antivirus) { # release storage
  16160. undef @Amavis::Conf::av_scanners; undef @Amavis::Conf::av_scanners_backup;
  16161. }
  16162. my(%spam_scanners_used);
  16163. my $bpscm = ca('bypass_spam_checks_maps');
  16164. if (!@{ca('spam_scanners')}) {
  16165. undef $extra_code_antispam;
  16166. } elsif (@$bpscm && !ref($bpscm->[0]) && $bpscm->[0]) { # simple-minded
  16167. undef $extra_code_antispam;
  16168. } else {
  16169. eval $extra_code_antispam or die "Problem in antispam code: $@";
  16170. # release memory occupied by the source code
  16171. undef $extra_code_antispam; $extra_code_antispam = 1;
  16172. for my $as (@{ca('spam_scanners')}) {
  16173. next if !ref $as || !defined $as->[1];
  16174. my($scanner_name,$module) = @$as; $spam_scanners_used{$module} = 1;
  16175. }
  16176. }
  16177. if (!$extra_code_antispam) { undef @Amavis::Conf::spam_scanners }
  16178. # load required built-in spam scanning modules
  16179. if ($spam_scanners_used{'Amavis::SpamControl::ExtProg'}) {
  16180. eval $extra_code_antispam_extprog or die "Problem in ExtProg code: $@";
  16181. # release memory occupied by source code
  16182. undef $extra_code_antispam_extprog; $extra_code_antispam_extprog = 1;
  16183. } else {
  16184. undef $extra_code_antispam_extprog;
  16185. }
  16186. if ($spam_scanners_used{'Amavis::SpamControl::SpamdClient'}) {
  16187. eval $extra_code_antispam_spamc or die "Problem in spamd client code: $@";
  16188. # release memory occupied by source code
  16189. undef $extra_code_antispam_spamc; $extra_code_antispam_spamc = 1;
  16190. } else {
  16191. undef $extra_code_antispam_spamc;
  16192. }
  16193. if ($spam_scanners_used{'Amavis::SpamControl::SpamAssassin'}) {
  16194. eval $extra_code_antispam_sa or die "Problem in antispam SA code: $@";
  16195. # release memory occupied by the source code
  16196. undef $extra_code_antispam_sa; $extra_code_antispam_sa = 1;
  16197. } else {
  16198. undef $extra_code_antispam_sa;
  16199. }
  16200. if (!grep { exists $policy_bank{$_}{'bypass_decode_parts'} &&
  16201. !do { my $v = $policy_bank{$_}{'bypass_decode_parts'};
  16202. !ref $v ? $v : $$v } } keys %policy_bank) {
  16203. undef $extra_code_unpackers;
  16204. } else { # at least one bypass_decode_parts is explicitly false
  16205. eval $extra_code_unpackers or die "Problem in Amavis::Unpackers code: $@";
  16206. # release memory occupied by the source code
  16207. undef $extra_code_unpackers; $extra_code_unpackers = 1;
  16208. }
  16209. if ($enable_zmq && $extra_code_zmq && @zmq_sockets) {
  16210. # better to catch and report potential ZMQ problems early before forking
  16211. $zmq_obj = Amavis::ZMQ->new(@zmq_sockets);
  16212. if ($zmq_obj && !$warm_restart && $cmd !~ /^(?:reload|stop)\z/) {
  16213. sleep 1; # a crude way to avoid a "slow joiner" syndrome #***
  16214. $zmq_obj->put_initial_snmp_data('FLUSH');
  16215. $zmq_obj->register_proc(1,1,'FLUSH');
  16216. }
  16217. }
  16218. Amavis::Log::init($do_syslog, $logfile); # initialize logging
  16219. Amavis::Log::log_to_stderr($cmd eq 'debug' || $cmd eq 'debug-sa' ? 1 : 0);
  16220. do_log(2, 'logging initialized, log level %s, %s%s', c('log_level'),
  16221. $do_syslog ? sprintf("syslog: %s.%s",c('syslog_ident'),c('syslog_facility')):
  16222. $logfile ne '' ? "logfile: $logfile" : "STDERR",
  16223. !$enable_log_capture ? '' : ', log capture enabled');
  16224. do_log(2, 'ZMQ enabled: %s', Amavis::ZMQ::zmq_version()) if $zmq_obj;
  16225. # insist on a FQDN in $myhostname
  16226. my $myhn = c('myhostname');
  16227. $myhn =~ /[^.]\.[a-zA-Z0-9-]+\z/s || lc($myhn) eq 'localhost'
  16228. or die <<"EOD";
  16229. The value of variable \$myhostname is \"$myhn\", but should have been
  16230. a fully qualified domain name; perhaps uname(3) did not provide such.
  16231. You must explicitly assign a FQDN of this host to variable \$myhostname
  16232. in amavisd.conf, or fix what uname(3) provides as a host's network name!
  16233. EOD
  16234. $mail_id_size_bits > 0 &&
  16235. $mail_id_size_bits == int $mail_id_size_bits &&
  16236. $mail_id_size_bits % 24 == 0
  16237. or die "\$mail_id_size_bits ($mail_id_size_bits) must be a multiple of 24\n";
  16238. eval {
  16239. my $amavisd_pid; # PID of a currently running amavisd daemon (not our pid)
  16240. # is amavisd daemon already running?
  16241. my $pidf = defined $pid_file_override ? $pid_file_override : $pid_file;
  16242. $pidf ne '' or die "Config parameter \$pid_file not defined";
  16243. my(@stat_list) = lstat($pidf); my $errn = @stat_list ? 0 : 0+$!;
  16244. if ($warm_restart) {
  16245. # skip pid file checking, let Net::Server handle it
  16246. } elsif ($errn == ENOENT) {
  16247. die "The amavisd daemon is apparently not running, no PID file $pidf\n"
  16248. if $cmd =~ /^(?:reload|restart|stop)\z/;
  16249. } elsif ($errn != 0) {
  16250. die "PID file $pidf is inaccessible: $!\n";
  16251. } elsif (!-f _) {
  16252. die "PID file $pidf is not a regular file\n";
  16253. } else { # determine PID of the currently running amavisd daemon, validate it
  16254. my $ln; my $lcnt = 0; my $pidf_h = IO::File->new;
  16255. $pidf_h->open($pidf,'<') or die "Can't open PID file $pidf: $!";
  16256. for ($! = 0; defined($ln=$pidf_h->getline); $! = 0) {
  16257. chomp($ln); $lcnt++; last if $lcnt > 100;
  16258. $amavisd_pid = $ln if $lcnt == 1 && $ln =~ /^\d{1,10}\z/;
  16259. }
  16260. defined $ln || $! == 0 or die "Error reading from file $pidf: $!";
  16261. $pidf_h->close or die "Error closing file $pidf: $!";
  16262. if ($lcnt <= 1 && !defined $amavisd_pid) {
  16263. # treat empty or junk one-line pid file the same as nonexisting pid file
  16264. die "The amavisd daemon is apparently not running, empty PID file $pidf\n"
  16265. if $cmd =~ /^(?:reload|restart|stop)\z/;
  16266. # prevent Net::Server from seeing this crippled file
  16267. do_log(-1, "removing empty or crippled PID file %s", $pidf);
  16268. unlink($pidf) or die "Can't remove PID file $pidf: $!";
  16269. undef $amavisd_pid;
  16270. } else {
  16271. $lcnt <= 1 or die "More than one line in file $pidf";
  16272. defined $amavisd_pid or die "Missing process ID in file $pidf";
  16273. $amavisd_pid > 1 or die "Invalid PID in file $pidf: [$amavisd_pid]";
  16274. }
  16275. my $mtime = $stat_list[9];
  16276. if (defined $amavisd_pid && defined $mtime) { # got a PID from a file
  16277. # Is pid file older than system uptime? If so, it should be disregarded,
  16278. # it must not prevent starting up amavisd after unclean shutdown.
  16279. my $now = int(time); my($uptime,$uptime_fmt); # sys uptime in seconds
  16280. my(@prog_args); my(@progs) = ('/usr/bin/uptime','uptime');
  16281. if (lc($^O) eq 'freebsd')
  16282. { @progs = ('/sbin/sysctl','sysctl'); @prog_args = 'kern.boottime' }
  16283. my $prog = find_program_path(\@progs, [split(/:/,$path,-1)] );
  16284. if (!defined($prog)) {
  16285. do_log(1,'No programs: %s',join(", ",@progs));
  16286. } else { # obtain system uptime
  16287. my($proc_fh,$uppid);
  16288. eval {
  16289. ($proc_fh,$uppid) = run_command(undef,'/dev/null',$prog,@prog_args);
  16290. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  16291. local($1,$2,$3,$4); chomp($ln);
  16292. if (defined $uptime) {}
  16293. elsif ($ln =~ /{[^}]*\bsec\s*=\s*(\d+)[^}]*}/) { $uptime= $now-$1 }
  16294. # amazing how broken reports from uptime(1) soon after boot can be!
  16295. elsif ($ln =~ /\b up \s+ (?: (\d{1,4}) \s* days? )? [,\s]*
  16296. (\d{1,2}) : (\d{1,2}) (?: : (\d{1,2}))? (?! \d ) /ix
  16297. || $ln =~ /\b up (?: \s* \b (\d{1,4}) \s* days? )?
  16298. (?: [,\s]* \b (\d{1,2}) \s* hrs? )?
  16299. (?: [,\s]* \b (\d{1,2}) \s* mins? )?
  16300. (?: [,\s]* \b (\d{1,2}) \s* secs? )? /ix )
  16301. { $uptime = (($1*24 + $2)*60 + $3)*60 + $4 }
  16302. elsif ($ln =~ /\b (\d{1,2}) \s* secs?/ix) { $uptime = $1 } #OpenBSD
  16303. $uptime_fmt = format_time_interval($uptime);
  16304. do_log(5,"system uptime %s: %s", $uptime_fmt,$ln);
  16305. }
  16306. defined $ln || $! == 0 or die "Reading uptime: $!";
  16307. my $err=0; $proc_fh->close or $err = $!;
  16308. my $child_stat = defined $uppid && waitpid($uppid,0)>0 ? $? : undef;
  16309. undef $proc_fh; undef $uppid;
  16310. proc_status_ok($child_stat,$err) or die "Error running $prog: " .
  16311. exit_status_str($child_stat,$err) . "\n";
  16312. } or do {
  16313. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  16314. do_log(1,"uptime: %s", $eval_stat);
  16315. };
  16316. if (defined $proc_fh) { $proc_fh->close } # ignoring status
  16317. if (defined $uppid) { waitpid($uppid,0) } # ignoring status
  16318. }
  16319. if (!defined($uptime)) {
  16320. do_log(1,'Unable to determine system uptime, will trust PID file');
  16321. } elsif ($now-$mtime <= $uptime+70) {
  16322. do_log(1,'Valid PID file (younger than sys uptime %s)', $uptime_fmt);
  16323. } else { # must not kill an unrelated process which happens to have the
  16324. # same pid as amavisd had before a system shutdown or crash
  16325. undef $amavisd_pid;
  16326. do_log(1,'Ignoring stale PID file %s, older than system uptime %s',
  16327. $pidf,$uptime_fmt);
  16328. }
  16329. }
  16330. }
  16331. if (defined $amavisd_pid) {
  16332. untaint_inplace($amavisd_pid);
  16333. if (!kill(0,$amavisd_pid)) { # does a process exist?
  16334. $! == ESRCH or die "Can't send SIG 0 to process [$amavisd_pid]: $!";
  16335. undef $amavisd_pid; # process does not exist
  16336. };
  16337. }
  16338. if ($warm_restart) {
  16339. # a semi-documented Net::Server mechanism for a restart on HUP;
  16340. # assume we have just been reincarnated by exec as a result of a HUP,
  16341. # so just ignore the command parameter and let Net::Server do the rest
  16342. } elsif ($cmd =~ /^(?:start|debug|debug-sa|foreground)?\z/) {
  16343. !defined($amavisd_pid)
  16344. or die "The amavisd daemon is already running, PID: [$amavisd_pid]\n";
  16345. } elsif ($cmd eq 'reload') { # reload: send a HUP signal to a running daemon
  16346. defined $amavisd_pid or die "The amavisd daemon is not running\n";
  16347. kill('HUP',$amavisd_pid) or $! == ESRCH
  16348. or die "Can't SIGHUP amavisd[$amavisd_pid]: $!";
  16349. my $msg = "Signalling a SIGHUP to a running daemon [$amavisd_pid]";
  16350. do_log(2,"%s",$msg); print STDOUT "$msg\n";
  16351. exit(0);
  16352. } elsif ($cmd =~ /^(?:restart|stop)\z/) { # stop or restart
  16353. defined $amavisd_pid or die "The amavisd daemon is not running\n";
  16354. my($kill_sig_used, $killed_amavisd_pid);
  16355. eval { # first stop a running daemon
  16356. $kill_sig_used = 'TERM';
  16357. kill($kill_sig_used,$amavisd_pid) or $! == ESRCH
  16358. or die "Can't SIG$kill_sig_used amavisd[$amavisd_pid]: $!";
  16359. my $waited = 0; my $sigkill_sent = 0; my $delay = 1; # seconds
  16360. for (;;) { # wait for the old running daemon to go away
  16361. sleep($delay); $waited += $delay; $delay = 5;
  16362. if (!kill(0,$amavisd_pid)) { # is the old daemon still there?
  16363. $! == ESRCH or die "Can't send SIG 0 to amavisd[$amavisd_pid]: $!";
  16364. $killed_amavisd_pid = $amavisd_pid; # old process is gone, done
  16365. last;
  16366. }
  16367. if ($waited < 60 || $sigkill_sent) {
  16368. do_log(2,"Waiting for the process [%s] to terminate",$amavisd_pid);
  16369. print STDOUT
  16370. "Waiting for the process [$amavisd_pid] to terminate\n";
  16371. } else { # use stronger hammer
  16372. do_log(2,"Sending SIGKILL to amavisd[%s]",$amavisd_pid);
  16373. print STDERR "Sending SIGKILL to amavisd[$amavisd_pid]\n";
  16374. $kill_sig_used = 'KILL';
  16375. kill($kill_sig_used,$amavisd_pid) or $! == ESRCH
  16376. or warn "Can't SIGKILL amavisd[$amavisd_pid]: $!";
  16377. $sigkill_sent = 1;
  16378. }
  16379. }
  16380. 1;
  16381. } or do {
  16382. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  16383. die "$eval_stat, can't $cmd the process\n";
  16384. };
  16385. my $msg = !defined($killed_amavisd_pid) ? undef :
  16386. "Daemon [$killed_amavisd_pid] terminated by SIG$kill_sig_used";
  16387. if ($cmd eq 'stop') {
  16388. if (defined $msg) { do_log(2,"%s",$msg); print STDOUT "$msg\n" }
  16389. exit(0);
  16390. }
  16391. if (defined $killed_amavisd_pid) {
  16392. print STDOUT "$msg, waiting for dust to settle...\n";
  16393. sleep 5; # wait for the TCP socket to be released
  16394. }
  16395. print STDOUT "becoming a new daemon...\n";
  16396. } else {
  16397. die "$myversion: Unknown command line parameter: $cmd\n\n" . usage();
  16398. }
  16399. 1;
  16400. } or do {
  16401. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  16402. do_log(2,"%s", $eval_stat);
  16403. die "$eval_stat\n";
  16404. };
  16405. $daemonize = 0 if $DEBUG; # in case $DEBUG came from a config file
  16406. # Set path, home and term explicitly. Don't trust environment
  16407. $ENV{PATH} = $path if defined $path && $path ne '';
  16408. $ENV{HOME} = $helpers_home if defined $helpers_home && $helpers_home ne '';
  16409. $ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100';
  16410. { my $msg = '';
  16411. $msg .= ", instance=$instance_name" if $instance_name ne '';
  16412. $msg .= ", nl=".sprintf("\\%03o",ord("\n")) if "\n" ne "\012";
  16413. $msg .= ", Unicode aware" if $unicode_aware;
  16414. for (qw(PERLIO LC_ALL LC_TYPE LC_CTYPE LANG))
  16415. { $msg .= sprintf(', %s="%s"', $_,$ENV{$_}) if $ENV{$_} ne '' }
  16416. do_log(0,"starting.%s %s at %s %s%s",
  16417. !$warm_restart?'':' (warm)', $0, c('myhostname'), $myversion, $msg);
  16418. }
  16419. # report version of Perl and process UID/GID
  16420. do_log(1, "perl=%s, user=%s, EUID: %s (%s); group=%s, EGID: %s (%s)",
  16421. $], $desired_user, $>, $<, $desired_group, $), $();
  16422. if ($warm_restart) {
  16423. # a semi-documented Net::Server mechanism to let a restarted process
  16424. # re-acquire sockets from its predecessor on a HUP
  16425. my $str = $ENV{BOUND_SOCKETS}; $str =~ s/\n/, /gs;
  16426. do_log(1,"warm restart on HUP [%s]: '%s', sockets: %s",
  16427. $$, join(' ',$0,@ARGV), $str);
  16428. }
  16429. # $SIG{USR2} = sub {
  16430. # my $msg = Carp::longmess("SIG$_[0] received, backtrace:");
  16431. # print STDERR "\n",$msg,"\n"; do_log(-1,"%s",$msg);
  16432. # };
  16433. fetch_modules_extra(); # bring additional modules into memory and compile them
  16434. $spamcontrol_obj = Amavis::SpamControl->new if $extra_code_antispam;
  16435. $spamcontrol_obj->init_pre_chroot if $spamcontrol_obj;
  16436. if ($daemonize) { # log warnings and uncaught errors
  16437. $SIG{'__DIE__' } =
  16438. sub { if (!$^S) { my($m) = @_; chomp($m); do_log(-1,"_DIE: %s", $m) } };
  16439. $SIG{'__WARN__'} =
  16440. sub { my($m) = @_; chomp($m); do_log(2,"_WARN: %s",$m) };
  16441. }
  16442. # set up Net::Server configuration
  16443. my(@bind_to);
  16444. { # merge port numbers, unix sockets and default binding host address into
  16445. # a unified list @listen_sockets, which will be passed on to Net::Server
  16446. #
  16447. local($1);
  16448. @bind_to = ref $inet_socket_bind ? @$inet_socket_bind : $inet_socket_bind;
  16449. $_ = !defined $_ || $_ eq '' ? '*' : /^\[(.*)\]\z/s ? $1 : $_ for @bind_to;
  16450. @bind_to = ( '*' ) if !@bind_to;
  16451. my(@merged_listen_sockets);
  16452. for (@listen_sockets) {
  16453. # roughly mimic the Net::Server::Proto and Net::Server::Proto::TCP parsing
  16454. if (m{^/} || m{[/|]unix\z}si) {
  16455. push(@merged_listen_sockets, $_); # looks like a Unix socket
  16456. } elsif (m{^\[[^\]]*\]:}s || m{^[^/|:]*:}s) {
  16457. push(@merged_listen_sockets, $_); # explicit host & port specified
  16458. } else { # assume port (or service) specification only, supply bind addr
  16459. for my $bind_addr (@bind_to) { # Cartesian product: bind_addr x port
  16460. # keep IPv4 addresses without square brackets for the benefit
  16461. # of non-IPv6 -aware Net::Server (pre- 2.000 version)
  16462. push(@merged_listen_sockets,
  16463. $bind_addr =~ /:/ ? "[$bind_addr]:$_" : "$bind_addr:$_");
  16464. }
  16465. }
  16466. }
  16467. @listen_sockets = @merged_listen_sockets;
  16468. do_log(5,"bind to %s", join(', ',@listen_sockets));
  16469. }
  16470. # DESTROY a ZMQ context (if any) of the main process,
  16471. # it would not survive across daemonization / forking,
  16472. # each child process needs to make its own context and sockets
  16473. undef $zmq_obj;
  16474. my $server = Amavis->new({
  16475. # command args to be used after HUP must be untainted, deflt: [$0,@ARGV]
  16476. # commandline => ['/usr/local/sbin/amavisd','-c',$config_file[0] ],
  16477. # commandline => [], # disable
  16478. commandline => [ map(untaint($_), ($0,@ARGV)) ],
  16479. port => \@listen_sockets, # listen on these sockets (Unix, inet, inet6)
  16480. host => $bind_to[0], # default bind, redundant, merged to @listen_sockets
  16481. listen => $listen_queue_size, # undef for a default
  16482. max_servers => $max_servers, # number of pre-forked children
  16483. !defined($min_servers) ? ()
  16484. : ( min_servers => $min_servers,
  16485. min_spare_servers => $min_spare_servers,
  16486. max_spare_servers => $max_spare_servers),
  16487. max_requests => $max_requests > 0 ? $max_requests : 2E9, # avoid dflt 1000
  16488. user => ($> == 0 || $< == 0) ? $daemon_user : undef,
  16489. group => ($> == 0 || $< == 0) ? $daemon_group : undef,
  16490. pid_file => defined $pid_file_override ? $pid_file_override : $pid_file,
  16491. # socket serialization lockfile
  16492. lock_file => defined $lock_file_override? $lock_file_override: $lock_file,
  16493. # serialize => 'flock', # flock, semaphore, pipe
  16494. background => $daemonize ? 1 : undef,
  16495. setsid => $daemonize ? 1 : undef,
  16496. chroot => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef,
  16497. no_close_by_child => 1,
  16498. leave_children_open_on_hup => 1,
  16499. # no_client_stdout introduced with Net::Server 0.92, but is broken in 0.92
  16500. no_client_stdout => (Net::Server->VERSION >= 0.93 ? 1 : 0),
  16501. # controls log level for Net::Server internal log messages:
  16502. # 0=err, 1=warning, 2=notice, 3=info, 4=debug
  16503. log_level => ($DEBUG || c('log_level') >= 5) ? 4 : 2,
  16504. log_file => undef, # method will be overridden by a call to do_log()
  16505. # SSL_cert_file => "$MYHOME/cert/mail-cert.pem",
  16506. # SSL_key_file => "$MYHOME/cert/mail-key.pem",
  16507. });
  16508. $0 = c('myprogram_name') . ' (master)';
  16509. $server->run; # transferring control to Net::Server
  16510. # shouldn't get here
  16511. exit 1;
  16512. 1; # make perlcritic happy
  16513. # we read text (such as notification templates) from DATA sections
  16514. # to avoid any interpretations of special characters (e.g. \ or ') by Perl
  16515. #
  16516. __DATA__
  16517. #
  16518. package Amavis::ZMQ;
  16519. use strict;
  16520. use re 'taint';
  16521. use warnings;
  16522. use warnings FATAL => qw(utf8 void);
  16523. no warnings 'uninitialized';
  16524. BEGIN {
  16525. require Exporter;
  16526. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  16527. $VERSION = '2.316';
  16528. @ISA = qw(Exporter);
  16529. import Amavis::Conf qw(:platform $myversion $myhostname
  16530. $nanny_details_level);
  16531. import Amavis::Util qw(ll do_log do_log_safe
  16532. snmp_initial_oids snmp_counters_get);
  16533. }
  16534. use vars qw($zmq_mod_name $zmq_mod_version $zmq_lib_version);
  16535. BEGIN {
  16536. my($zmq_major, $zmq_minor, $zmq_patch);
  16537. if (eval { require ZMQ::LibZMQ3 && require ZMQ::Constants }) {
  16538. $zmq_mod_name = 'ZMQ::LibZMQ3'; # new interface module to zmq v3 or cx
  16539. import ZMQ::LibZMQ3; import ZMQ::Constants qw(:all);
  16540. ($zmq_major, $zmq_minor, $zmq_patch) = ZMQ::LibZMQ3::zmq_version();
  16541. # *zmq_sendmsg [native] # (socket,msgobj,flags)
  16542. # *zmq_recvmsg [native] # (socket,flags) -> msgobj
  16543. *zmq_sendstr = sub { # (socket,string,flags)
  16544. my $rv = zmq_send($_[0], $_[1], length $_[1], $_[2]||0);
  16545. $rv == -1 ? undef : $rv;
  16546. };
  16547. # *zmq_recvstr = sub { # (socket,buffer,flags)
  16548. # my $len = zmq_recv($_[0], $_[1], 4096, $_[2]);
  16549. # if ($len < 0) { $_[1] = undef; return undef }
  16550. # substr($_[1],$len) = '' if length $_[1] > $len;
  16551. # return $len;
  16552. # };
  16553. } elsif (eval { require ZMQ::LibZMQ2 && require ZMQ::Constants }) {
  16554. $zmq_mod_name = 'ZMQ::LibZMQ2'; # new interface module to zmq v2
  16555. import ZMQ::LibZMQ2; import ZMQ::Constants qw(:all);
  16556. ($zmq_major, $zmq_minor, $zmq_patch) = ZMQ::LibZMQ2::zmq_version();
  16557. # zmq v2/v3 incompatibile renaming
  16558. *zmq_sendmsg = \&ZMQ::LibZMQ2::zmq_send; # (socket,msgobj,flags)
  16559. *zmq_recvmsg = \&ZMQ::LibZMQ2::zmq_recv; # (socket,flags) -> msgobj
  16560. *zmq_sendstr = sub { # (socket,string,flags)
  16561. my $rv = zmq_send(@_); $rv == -1 ? undef : $rv;
  16562. };
  16563. # *zmq_recvstr = sub { ... }
  16564. } elsif (eval { require ZeroMQ::Constants && require ZeroMQ::Raw }) {
  16565. $zmq_mod_name = 'ZeroMQ'; # old interface module to zmq v2
  16566. import ZeroMQ::Raw; import ZeroMQ::Constants qw(:all);
  16567. ($zmq_major, $zmq_minor, $zmq_patch) = ZeroMQ::version();
  16568. # zmq v2/v3 incompatibile renaming
  16569. *zmq_sendmsg = \&ZeroMQ::Raw::zmq_send; # (socket,msgobj,flags)
  16570. *zmq_recvmsg = \&ZeroMQ::Raw::zmq_recv; # (socket,flags) -> msgobj
  16571. *zmq_sendstr = sub { # (socket,string,flags)
  16572. my $rv = zmq_send(@_); $rv == -1 ? undef : $rv;
  16573. };
  16574. # *zmq_recvstr = sub { ... }
  16575. } else {
  16576. die "Perl modules ZMQ::LibZMQ3 or ZMQ::LibZMQ2 or ZeroMQ not available\n";
  16577. }
  16578. *zmq_recvstr = sub { # (socket,buffer) -> len
  16579. my $zm = zmq_recvmsg($_[0]);
  16580. if (!$zm) { $_[1] = undef; return undef }
  16581. $_[1] = zmq_msg_data($zm); zmq_msg_close($zm);
  16582. return length($_[1]);
  16583. };
  16584. $zmq_mod_version = $zmq_mod_name->VERSION;
  16585. $zmq_lib_version = join('.', $zmq_major, $zmq_minor, $zmq_patch);
  16586. 1;
  16587. } # BEGIN
  16588. sub zmq_version {
  16589. sprintf("%s %s, lib %s",
  16590. $zmq_mod_name, $zmq_mod_version, $zmq_lib_version);
  16591. };
  16592. sub new {
  16593. my($class,@socknames) = @_;
  16594. my $self = { ctx => undef, sock => undef,
  16595. inactivated => 0, socknames => [ @socknames ],
  16596. base_timestamp => undef };
  16597. bless $self, $class;
  16598. $self->establish;
  16599. $self;
  16600. }
  16601. sub inactivate {
  16602. my $self = shift;
  16603. $self->{inactivated} = 1;
  16604. }
  16605. use vars qw($zmq_in_establish); # prevents loop if logging to zmq
  16606. sub establish {
  16607. my $self = shift;
  16608. return if $self->{inactivated} || $zmq_in_establish;
  16609. my($ctx,$sock);
  16610. eval {
  16611. $zmq_in_establish = 1;
  16612. $ctx = $self->{ctx};
  16613. if (!$ctx) {
  16614. $self->{sock} = undef; # just in case
  16615. # do_log(5,'zmq: zmq_init');
  16616. $self->{ctx} = $ctx = zmq_init(1);
  16617. $ctx or die "Error creating ZMQ context: $!";
  16618. }
  16619. $sock = $self->{sock};
  16620. if (!$sock && $ctx) { # connect to a socket
  16621. # do_log(5,'zmq: zmq_socket');
  16622. $self->{sock} = $sock = zmq_socket($ctx, ZMQ_PUB); # ZMQ_PUSH
  16623. if (!$sock) {
  16624. die "Error creating ZMQ socket: $!";
  16625. } else {
  16626. # do_log(5,'zmq: zmq_setsockopt');
  16627. zmq_setsockopt($sock, ZMQ_LINGER, 2000) != -1 # milliseconds
  16628. or die "Error setting LINGER on a ZMQ socket: $!";
  16629. # zmq_setsockopt($sock, ZMQ_IPV4ONLY, 0) != -1
  16630. # or die "Error turning off ZMQ_IPV4ONLY on a ZMQ socket: $!";
  16631. # my $hwm = $zmq_lib_version =~ /^[012]\./ && defined &ZMQ_HWM ? &ZMQ_HWM
  16632. # : defined &ZMQ_SNDHWM ? &ZMQ_SNDHWM
  16633. # : undef;
  16634. # if (defined $hwm) {
  16635. # zmq_setsockopt($sock, $hwm, 1000) != -1
  16636. # or die "Error setting highwater mark on a ZMQ socket: $!";
  16637. # }
  16638. for my $name (@{$self->{socknames}}) {
  16639. # do_log(5,'zmq: zmq_connect %s',$name);
  16640. zmq_connect($sock, $name) != -1
  16641. or die "Error connecting ZMQ socket to $name: $!";
  16642. }
  16643. }
  16644. }
  16645. 1;
  16646. } or do { # clean up, disable, and resignal a failure
  16647. zmq_close($sock) if $sock; # ignoring status
  16648. zmq_term($ctx) if $ctx; # ignoring status
  16649. undef $self->{sock}; undef $self->{ctx};
  16650. $self->{inactivated} = 1; $zmq_in_establish = 0;
  16651. chomp @_; die "zmq establish failed: @_\n"; # propagate the exception
  16652. };
  16653. $zmq_in_establish = 0;
  16654. $sock;
  16655. }
  16656. sub DESTROY {
  16657. my $self = shift; local($@,$!,$_);
  16658. # can occur soon after fork, must not use context (like calling a logger)
  16659. if (!$self->{inactivated}) {
  16660. my($ctx, $sock) = ($self->{ctx}, $self->{sock});
  16661. zmq_close($sock) if $sock; # ignoring status
  16662. zmq_term($ctx) if $ctx; # ignoring status
  16663. }
  16664. undef $self->{sock}; undef $self->{ctx};
  16665. %{$self} = (); # then ditch the rest
  16666. }
  16667. sub register_proc {
  16668. my($self, $details_level, $reset_timestamp, $state, $task_id) = @_;
  16669. my $sock = $self->{sock}; # = $self->establish;
  16670. return if !$sock;
  16671. # if (!defined $state || $details_level <= $nanny_details_level) {
  16672. if (1) {
  16673. my $pid = $$;
  16674. my $msg;
  16675. my $now = Time::HiRes::time;
  16676. if ($reset_timestamp || !$self->{base_timestamp}) {
  16677. $self->{base_timestamp} = $now;
  16678. $msg = sprintf('am.st %d %014.3f ', $pid, $now);
  16679. } else {
  16680. my $dt = $now - $self->{base_timestamp};
  16681. $msg = sprintf('am.st %d %d ', $pid, $dt <= 0 ? 0 : int($dt*1000 + 0.5));
  16682. }
  16683. if (!defined $state) {
  16684. $msg .= 'exiting';
  16685. } else {
  16686. $state = '-' if $state eq ' ' || $state eq ''; # simplifies parsing
  16687. $msg .= $state;
  16688. $msg .= ' ' . $task_id if defined $task_id;
  16689. }
  16690. # do_log(5,'zmq: register_proc: %s', $msg);
  16691. defined zmq_sendstr($sock,$msg)
  16692. or die "Error sending a ZMQ message: $!";
  16693. }
  16694. }
  16695. sub write_log {
  16696. my($self, $level, $errmsg) = @_;
  16697. my $sock = $self->{sock}; # = $self->establish;
  16698. return if !$sock;
  16699. my $nstars = 6 - $level;
  16700. $nstars = 7 if $nstars > 7;
  16701. $nstars = 1 if $nstars < 1;
  16702. # ignoring status to prevent a logging loop
  16703. zmq_sendstr($sock, sprintf('am.log.%s %s %014.3f %s', '*' x $nstars, $$,
  16704. Time::HiRes::time, $errmsg));
  16705. }
  16706. # insert startup time SNMP entry, called from the master process at startup
  16707. #
  16708. sub put_initial_snmp_data {
  16709. my($self,$flush) = @_;
  16710. my $sock = $self->{sock}; # = $self->establish;
  16711. return if !$sock;
  16712. # do_log(5,'zmq: publishing initial snmp data');
  16713. if ($flush) {
  16714. defined zmq_sendstr($sock, 'am.snmp FLUSH')
  16715. or die "Error sending a ZMQ flush message: $!";
  16716. }
  16717. my $list_ref = snmp_initial_oids();
  16718. for my $obj (@$list_ref) {
  16719. my($key,$type,$val) = @$obj;
  16720. defined zmq_sendstr($sock, sprintf('am.snmp %s %s %s',$key,$type,$val))
  16721. or die "Error sending a ZMQ message: $!";
  16722. };
  16723. }
  16724. sub update_snmp_variables {
  16725. my($self) = @_;
  16726. my $sock = $self->{sock}; # = $self->establish;
  16727. return if !$sock;
  16728. my $snmp_var_names_ref = snmp_counters_get();
  16729. if (defined $snmp_var_names_ref && @$snmp_var_names_ref) {
  16730. do_log(5,'zmq: updating snmp variables');
  16731. for my $key (@$snmp_var_names_ref) {
  16732. my($snmp_var_name, $val, $type) = ref $key ? @$key : ($key);
  16733. if (!defined $type || $type eq '') { # a counter, same as C32
  16734. $type = 'C32';
  16735. if (!defined($val)) { $val = 1 } # by default counter increments by 1
  16736. elsif ($val < 0) { $val = 0 } # counter is supposed to be unsigned
  16737. $val = "$val"; # convert to a string
  16738. } elsif ($type eq 'C32' || $type eq 'C64') { # a counter
  16739. if (!defined($val)) { $val = 1 } # by default counter increments by 1
  16740. elsif ($val < 0) { $val = 0 } # counter is supposed to be unsigned
  16741. $val = "$val"; # convert to a string
  16742. } elsif ($type eq 'INT') { # integer
  16743. $val = "$val"; # convert to a string
  16744. } elsif ($type eq 'TIM') { # TimeTicks
  16745. if ($val < 0) { $val = 0 } # non-decrementing
  16746. $val = "$val"; # convert to a string
  16747. }
  16748. if ($snmp_var_name ne 'entropy') { # don't broadcast entropy
  16749. defined zmq_sendstr($sock, sprintf('am.snmp %s %s %s',
  16750. $snmp_var_name,$type,$val))
  16751. or die "Error sending a ZMQ message: $!";
  16752. }
  16753. }
  16754. }
  16755. }
  16756. 1;
  16757. __DATA__
  16758. #
  16759. package Amavis::DB::SNMP;
  16760. use strict;
  16761. use re 'taint';
  16762. use warnings;
  16763. use warnings FATAL => qw(utf8 void);
  16764. no warnings 'uninitialized';
  16765. BEGIN {
  16766. require Exporter;
  16767. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  16768. $VERSION = '2.316';
  16769. @ISA = qw(Exporter);
  16770. import Amavis::Conf qw(:platform $myversion $myhostname
  16771. $nanny_details_level);
  16772. import Amavis::Util qw(ll do_log do_log_safe
  16773. snmp_initial_oids snmp_counters_get
  16774. add_entropy fetch_entropy_bytes);
  16775. }
  16776. use BerkeleyDB;
  16777. use MIME::Base64;
  16778. use Time::HiRes ();
  16779. # open existing databases (called by each child process)
  16780. #
  16781. sub new {
  16782. my($class,$db_env) = @_; $! = 0; my $env = $db_env->get_db_env;
  16783. defined $env or die "BDB get_db_env (dbS/dbN): $BerkeleyDB::Error, $!.";
  16784. $! = 0; my $dbs = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env);
  16785. defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
  16786. $! = 0; my $dbn = BerkeleyDB::Hash->new(-Filename=>'nanny.db',-Env=>$env);
  16787. defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
  16788. bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class;
  16789. }
  16790. sub DESTROY {
  16791. my $self = shift;
  16792. local($@,$!,$_); my $myactualpid = $$;
  16793. if (defined($my_pid) && $myactualpid != $my_pid) {
  16794. do_log_safe(5,"Amavis::DB::SNMP DESTROY skip, clone [%s] (born as [%s])",
  16795. $myactualpid, $my_pid);
  16796. } else {
  16797. do_log_safe(5,"Amavis::DB::SNMP DESTROY called");
  16798. for my $db_name ('db_snmp', 'db_nanny') {
  16799. my $db = $self->{$db_name};
  16800. if (defined $db) {
  16801. eval {
  16802. $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!."; 1;
  16803. } or do { $@ = "errno=$!" if $@ eq '' };
  16804. if ($@ ne '' && $@ !~ /\bDatabase is already closed\b/)
  16805. { warn "[$myactualpid] BDB S+N DESTROY INFO ($db_name): $@" }
  16806. undef $db;
  16807. }
  16808. }
  16809. }
  16810. }
  16811. #sub lock_stat($) {
  16812. # my $label = @_;
  16813. # my $s = qx'/usr/local/bin/db_stat-4.2 -c -h /var/amavis/db | /usr/local/bin/perl -ne \'$a{$2}=$1 if /^(\d+)\s+Total number of locks (requested|released)/; END {printf("%d, %d\n",$a{requested}, $a{requested}-$a{released})}\'';
  16814. # do_log(0, "lock_stat %s: %s", $label,$s);
  16815. #}
  16816. # insert startup time SNMP entry, called from the master process at startup
  16817. # (a classical subroutine, not a method)
  16818. #
  16819. sub put_initial_snmp_data($) {
  16820. my($db) = @_;
  16821. my($eval_stat,$interrupt); $interrupt = '';
  16822. { my $cursor;
  16823. my $h1 = sub { $interrupt = $_[0] };
  16824. local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
  16825. eval { # ensure cursor will be unlocked even in case of errors or signals
  16826. $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
  16827. defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!.";
  16828. my $list_ref = snmp_initial_oids();
  16829. for my $obj (@$list_ref) {
  16830. my($key,$type,$val) = @$obj;
  16831. $cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0
  16832. or die "BDB S c_put: $BerkeleyDB::Error, $!.";
  16833. };
  16834. $cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!.";
  16835. undef $cursor; 1;
  16836. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  16837. $cursor->c_close if defined $cursor; # unlock, ignoring status
  16838. undef $cursor;
  16839. }; # restore signal handlers
  16840. if ($interrupt ne '') { kill($interrupt,$$) } # resignal, ignoring status
  16841. elsif (defined $eval_stat) {
  16842. chomp $eval_stat;
  16843. die "put_initial_snmp_data: BDB S $eval_stat\n";
  16844. }
  16845. }
  16846. sub update_snmp_variables {
  16847. my($self) = @_;
  16848. do_log(5,"updating snmp variables in BDB");
  16849. my $snmp_var_names_ref = snmp_counters_get();
  16850. my($eval_stat,$interrupt); $interrupt = '';
  16851. if (defined $snmp_var_names_ref && @$snmp_var_names_ref) {
  16852. my $db = $self->{'db_snmp'}; my $cursor;
  16853. my $h1 = sub { $interrupt = $_[0] };
  16854. local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
  16855. eval { # ensure cursor will be unlocked even in case of errors or signals
  16856. $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
  16857. defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
  16858. for my $key (@$snmp_var_names_ref) {
  16859. my($snmp_var_name,$arg,$type) = ref $key ? @$key : ($key);
  16860. $type = 'C32' if !defined($type) || $type eq '';
  16861. if ($type eq 'C32' || $type eq 'C64') { # a counter
  16862. if (!defined($arg)) { $arg = 1 } # by default counter increments by 1
  16863. elsif ($arg < 0) { $arg = 0 } # counter is supposed to be unsigned
  16864. } elsif ($type eq 'TIM') { # TimeTicks
  16865. if ($arg < 0) { $arg = 0 } # non-decrementing
  16866. }
  16867. my($val,$flags); local($1);
  16868. my $stat = $cursor->c_get($snmp_var_name,$val,DB_SET);
  16869. if ($stat==0) { # exists, update it (or replace it)
  16870. if ($type eq 'C32' && $val=~/^C32 (\d+)\z/) { $val = $1+$arg }
  16871. elsif ($type eq 'C64' && $val=~/^C64 (\d+)\z/) { $val = $1+$arg }
  16872. elsif ($type eq 'TIM' && $val=~/^TIM (\d+)\z/) { $val = $1+$arg }
  16873. elsif ($type eq 'INT' && $val=~/^INT ([+-]?\d+)\z/) { $val = $arg }
  16874. elsif ($type=~/^(STR|OID)\z/ && $val=~/^\Q$type\E (.*)\z/) {
  16875. if ($snmp_var_name ne 'entropy') { $val = $arg }
  16876. else { # blend-in entropy
  16877. $val = $1; add_entropy($val, Time::HiRes::gettimeofday);
  16878. $val = fetch_entropy_bytes(18); # 18 bytes
  16879. $val = encode_base64($val,''); # 18*8/6 = 24 chars
  16880. $val =~ tr{+/}{-_}; # base64 -> RFC 4648 base64url [A-Za-z0-9-_]
  16881. }
  16882. }
  16883. else {
  16884. do_log(-2,"WARN: variable syntax? %s: %s, clearing",
  16885. $snmp_var_name,$val);
  16886. $val = 0;
  16887. }
  16888. $flags = DB_CURRENT;
  16889. } else { # create new entry
  16890. $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
  16891. $flags = DB_KEYLAST; $val = $arg;
  16892. }
  16893. my $fmt = $type eq 'C32' ? "%010d" : $type eq 'C64' ? "%020.0f"
  16894. : $type eq 'INT' ? "%010d" : undef;
  16895. # format for INT should really be %011d, but keep compatibility for now
  16896. my $str = defined($fmt) ? sprintf($fmt,$val) : $val;
  16897. $cursor->c_put($snmp_var_name, $type.' '.$str, $flags) == 0
  16898. or die "c_put: $BerkeleyDB::Error, $!.";
  16899. }
  16900. $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
  16901. undef $cursor; 1;
  16902. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  16903. if (defined $db) {
  16904. $cursor->c_close if defined $cursor; # unlock, ignoring status
  16905. undef $cursor;
  16906. # if (!defined($eval_stat)) {
  16907. # my $stat; $db->db_sync(); # not really needed
  16908. # $stat==0 or warn "BDB S db_sync,status $stat: $BerkeleyDB::Error, $!.";
  16909. # }
  16910. }
  16911. }; # restore signal handlers
  16912. delete $self->{'cnt'};
  16913. if ($interrupt ne '') { kill($interrupt,$$) } # resignal, ignoring status
  16914. elsif (defined $eval_stat) {
  16915. chomp $eval_stat;
  16916. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  16917. die "update_snmp_variables: BDB S $eval_stat\n";
  16918. }
  16919. }
  16920. sub read_snmp_variables {
  16921. my($self,@snmp_var_names) = @_;
  16922. my($eval_stat,$interrupt); $interrupt = '';
  16923. my $db = $self->{'db_snmp'}; my $cursor; my(@values);
  16924. { my $h1 = sub { $interrupt = $_[0] };
  16925. local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
  16926. eval { # ensure cursor will be unlocked even in case of errors or signals
  16927. $cursor = $db->db_cursor; # obtain read lock
  16928. defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
  16929. for my $cname (@snmp_var_names) {
  16930. my $val; my $stat = $cursor->c_get($cname,$val,DB_SET);
  16931. push(@values, $stat==0 ? $val : undef);
  16932. $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
  16933. }
  16934. $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
  16935. undef $cursor; 1;
  16936. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  16937. if (defined $db) {
  16938. $cursor->c_close if defined $cursor; # unlock, ignoring status
  16939. undef $cursor;
  16940. }
  16941. }; # restore signal handlers
  16942. if ($interrupt ne '') { kill($interrupt,$$) } # resignal, ignoring status
  16943. elsif (defined $eval_stat) {
  16944. chomp $eval_stat;
  16945. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  16946. die "read_snmp_variables: BDB S $eval_stat\n";
  16947. }
  16948. for my $val (@values) {
  16949. if (!defined($val)) {} # keep undefined
  16950. elsif ($val =~ /^(?:C32|C64) (\d+)\z/) { $val = 0+$1 }
  16951. elsif ($val =~ /^(?:INT) ([+-]?\d+)\z/) { $val = 0+$1 }
  16952. elsif ($val =~ /^(?:STR|OID) (.*)\z/) { $val = $1 }
  16953. else { do_log(-2,"WARN: counter syntax? %s", $val); undef $val }
  16954. }
  16955. \@values;
  16956. }
  16957. sub register_proc {
  16958. my($self, $details_level, $reset_timestamp, $state, $task_id) = @_;
  16959. my $eval_stat; my $interrupt = '';
  16960. if (!defined($state) || $details_level <= $nanny_details_level) {
  16961. $task_id = '' if !defined $task_id;
  16962. my $db = $self->{'db_nanny'}; my $key = sprintf("%05d",$$);
  16963. my $cursor; my $val;
  16964. my $h1 = sub { $interrupt = $_[0] };
  16965. local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
  16966. eval { # ensure cursor will be unlocked even in case of errors or signals
  16967. $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock
  16968. defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
  16969. my $stat = $cursor->c_get($key,$val,DB_SET);
  16970. $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
  16971. if ($stat==0 && !defined $state) { # remove existing entry
  16972. $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
  16973. } elsif (defined $state) { # add new, or update existing entry
  16974. my $timestamp; local($1);
  16975. # keep its timestamp when updating existing record
  16976. $timestamp = $1 if $stat==0 && $val=~/^(\d+(?:\.\d*)?) /s;
  16977. $timestamp = sprintf("%014.3f", Time::HiRes::time)
  16978. if !defined($timestamp) || $reset_timestamp;
  16979. my $new_val = sprintf("%s %-14s", $timestamp, $state.$task_id);
  16980. $cursor->c_put($key, $new_val,
  16981. $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
  16982. or die "c_put: $BerkeleyDB::Error, $!.";
  16983. }
  16984. $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
  16985. undef $cursor; 1;
  16986. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  16987. if (defined $db) {
  16988. $cursor->c_close if defined $cursor; # unlock, ignoring status
  16989. undef $cursor;
  16990. # if (!defined($eval_stat)) {
  16991. # my $stat = $db->db_sync(); # not really needed
  16992. # $stat==0 or warn "BDB N db_sync,status $stat: $BerkeleyDB::Error, $!.";
  16993. # }
  16994. }
  16995. }; # restore signal handlers
  16996. if ($interrupt ne '') {
  16997. kill($interrupt,$$); # resignal, ignoring status
  16998. } elsif (defined $eval_stat) {
  16999. chomp $eval_stat;
  17000. do_log_safe(5, "register_proc: BDB N %s", $eval_stat);
  17001. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  17002. die "register_proc: BDB N $eval_stat\n";
  17003. }
  17004. }
  17005. 1;
  17006. #
  17007. package Amavis::DB;
  17008. use strict;
  17009. use re 'taint';
  17010. use warnings;
  17011. use warnings FATAL => qw(utf8 void);
  17012. BEGIN {
  17013. require Exporter;
  17014. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  17015. $VERSION = '2.316';
  17016. @ISA = qw(Exporter);
  17017. import Amavis::Conf qw($db_home $daemon_chroot_dir);
  17018. import Amavis::Util qw(untaint ll do_log);
  17019. }
  17020. use BerkeleyDB;
  17021. # create new databases, then close them (called by the parent process)
  17022. # (called only if $db_home is nonempty)
  17023. #
  17024. sub init($$) {
  17025. my($predelete_nanny, $predelete_snmp) = @_;
  17026. my $name = $db_home;
  17027. $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
  17028. if ($predelete_nanny || $predelete_snmp) { # delete existing db files first?
  17029. local(*DIR);
  17030. opendir(DIR,$db_home) or die "db_init: Can't open directory $name: $!";
  17031. # modifying a directory while traversing it can cause surprises, avoid;
  17032. # avoid slurping the whole directory contents into memory
  17033. my($f, @rmfiles);
  17034. while (defined($f = readdir(DIR))) {
  17035. next if $f eq '.' || $f eq '..';
  17036. if ($f eq 'nanny.db') {
  17037. push(@rmfiles, $f) if $predelete_nanny;
  17038. } elsif ($f eq 'snmp.db') {
  17039. push(@rmfiles, $f) if $predelete_snmp;
  17040. } elsif ($f =~ /^__db\.\d+\z/s) {
  17041. push(@rmfiles, $f) if $predelete_nanny && $predelete_snmp;
  17042. } elsif ($f =~ /^(?:cache-expiry|cache)\.db\z/s) {
  17043. push(@rmfiles, $f); # old databases, no longer used since 2.7.0-pre9
  17044. }
  17045. }
  17046. closedir(DIR) or die "db_init: Error closing directory $name: $!";
  17047. do_log(0, 'Deleting db files %s in %s', join(',',@rmfiles), $name);
  17048. for my $f (@rmfiles) {
  17049. my $fname = $db_home . '/' . untaint($f);
  17050. unlink($fname) or die "db_init: Can't delete file $fname: $!";
  17051. }
  17052. undef @rmfiles; # release storage
  17053. }
  17054. $! = 0; my $env = BerkeleyDB::Env->new(-Home=>$db_home, -Mode=>0640,
  17055. -Flags=> DB_CREATE | DB_INIT_CDB | DB_INIT_MPOOL);
  17056. defined $env
  17057. or die "BDB can't create db env. at $db_home: $BerkeleyDB::Error, $!.";
  17058. do_log(0, 'Creating db in %s/; BerkeleyDB %s, libdb %s',
  17059. $name, BerkeleyDB->VERSION, $BerkeleyDB::db_version);
  17060. $! = 0; my $dbs = BerkeleyDB::Hash->new(
  17061. -Filename=>'snmp.db', -Flags=>DB_CREATE, -Env=>$env );
  17062. defined $dbs or die "db_init: BDB no dbS: $BerkeleyDB::Error, $!.";
  17063. $! = 0; my $dbn = BerkeleyDB::Hash->new(
  17064. -Filename=>'nanny.db', -Flags=>DB_CREATE, -Env=>$env );
  17065. defined $dbn or die "db_init: BDB no dbN: $BerkeleyDB::Error, $!.";
  17066. Amavis::DB::SNMP::put_initial_snmp_data($dbs) if $predelete_snmp;
  17067. for my $db ($dbs, $dbn) {
  17068. $db->db_close==0 or die "db_init: BDB db_close: $BerkeleyDB::Error, $!.";
  17069. }
  17070. }
  17071. # open an existing databases environment (called by each child process)
  17072. #
  17073. sub new {
  17074. my($class) = @_; my $env;
  17075. if (defined $db_home) {
  17076. $! = 0; $env = BerkeleyDB::Env->new(
  17077. -Home=>$db_home, -Mode=>0640, -Flags=> DB_INIT_CDB | DB_INIT_MPOOL);
  17078. defined $env
  17079. or die "BDB can't connect db env. at $db_home: $BerkeleyDB::Error, $!.";
  17080. }
  17081. bless \$env, $class;
  17082. }
  17083. sub get_db_env { my $self = shift; $$self }
  17084. 1;
  17085. __DATA__
  17086. #
  17087. package Amavis::Lookup::SQLfield;
  17088. use strict;
  17089. use re 'taint';
  17090. use warnings;
  17091. use warnings FATAL => qw(utf8 void);
  17092. no warnings 'uninitialized';
  17093. BEGIN {
  17094. require Exporter;
  17095. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  17096. $VERSION = '2.316';
  17097. @ISA = qw(Exporter);
  17098. import Amavis::Util qw(ll do_log);
  17099. import Amavis::Conf qw($trim_trailing_space_in_lookup_result_fields);
  17100. }
  17101. # the sub new() is already declared in the always-loaded code section
  17102. # fieldtype: B=boolean, N=numeric, S=string,
  17103. # N-: numeric, nonexistent field returns undef without complaint
  17104. # S-: string, nonexistent field returns undef without complaint
  17105. # B-: boolean, nonexistent field returns undef without complaint
  17106. # B0: boolean, nonexistent field treated as false
  17107. # B1: boolean, nonexistent field treated as true
  17108. sub lookup_sql_field($$$%) {
  17109. my($self, $addr, $get_all, %options) = @_;
  17110. my(@result, @matchingkey, $sql_query, $field);
  17111. if ($self) { $sql_query = $self->{sql_query}; $field = $self->{fieldname} }
  17112. $sql_query = $Amavis::sql_lookups if !defined $sql_query; # global default
  17113. if (!defined $self) {
  17114. do_log(5, 'lookup_sql_field - no field query object, "%s" no match',$addr);
  17115. } elsif (!defined $field || $field eq '') {
  17116. do_log(5, 'lookup_sql_field() - no field name, "%s" no match', $addr);
  17117. } elsif (!defined $sql_query) {
  17118. do_log(5, 'lookup_sql_field(%s) - no sql_lookups object, "%s" no match',
  17119. $field, $addr);
  17120. } else {
  17121. my(@result_attr_names) = !ref $field ? ( $field )
  17122. : ref $field eq 'ARRAY' ? @$field
  17123. : ref $field eq 'HASH' ? keys %$field : ();
  17124. my(%attr_name_to_sqlfield_name) =
  17125. ref $field eq 'HASH' ? %$field
  17126. : map( ($_,$_), @result_attr_names);
  17127. my $fieldtype = $self->{fieldtype};
  17128. $fieldtype = 'S-' if !defined $fieldtype;
  17129. my($res_ref,$mk_ref) = $sql_query->lookup_sql($addr,1, %options,
  17130. !exists($self->{args}) ? () : (ExtraArguments => $self->{args}));
  17131. if (!defined $res_ref || !@$res_ref) {
  17132. ll(5) && do_log(5, 'lookup_sql_field(%s), "%s" no matching records',
  17133. join(',', map(lc($_) eq lc($attr_name_to_sqlfield_name{$_}) ? $_
  17134. : $_ . '/' . $attr_name_to_sqlfield_name{$_},
  17135. @result_attr_names)), $addr);
  17136. } else {
  17137. my %nosuchfield;
  17138. for my $ind (0 .. $#$res_ref) {
  17139. my($any_field_matches, @match_values_by_ind);
  17140. my $h_ref = $res_ref->[$ind]; my $mk = $mk_ref->[$ind];
  17141. for my $result_attr_ind (0 .. $#result_attr_names) {
  17142. my $result_attr_name = $result_attr_names[$result_attr_ind];
  17143. next if !defined $result_attr_name;
  17144. my $fieldname = $attr_name_to_sqlfield_name{$result_attr_name};
  17145. next if !defined $fieldname || $fieldname eq '';
  17146. my $match;
  17147. if (!exists($h_ref->{$fieldname})) {
  17148. $nosuchfield{$fieldname} = 1;
  17149. # record found, but no field with that name in the table
  17150. # fieldtype: B0: boolean, nonexistent field treated as false,
  17151. # B1: boolean, nonexistent field treated as true
  17152. if ($fieldtype =~ /^.-/s) { # allowed to not exist
  17153. # this type is almost universally in use now, continue searching
  17154. } elsif ($fieldtype =~ /^B1/) { # defaults to true
  17155. # only used for the 'local' field
  17156. $match = 1; # nonexistent field treated as 1
  17157. } elsif ($fieldtype =~ /^B0/) { # boolean, defaults to false
  17158. # no longer in use
  17159. $match = 0; # nonexistent field treated as 0
  17160. } else {
  17161. # treated as 'no match', returns undef
  17162. }
  17163. } else { # field exists
  17164. # fieldtype: B=boolean, N=numeric, S=string
  17165. $match = $h_ref->{$fieldname};
  17166. if (!defined $match) {
  17167. # NULL field values represented as undef
  17168. } elsif ($fieldtype =~ /^B/) { # boolean
  17169. # convert values 'N', 'F', '0', ' ' and "\000" to 0
  17170. # to allow value to be used directly as a Perl boolean
  17171. $match = 0 if $match =~ /^([NnFf ]|0+|\000+)\ *\z/;
  17172. } elsif ($fieldtype =~ /^N/) { # numeric
  17173. $match = $match + 0; # convert string into a number
  17174. } elsif ($fieldtype =~ /^S/) { # string
  17175. $match =~ s/ +\z// # trim trailing spaces
  17176. if $trim_trailing_space_in_lookup_result_fields;
  17177. }
  17178. }
  17179. $match_values_by_ind[$result_attr_ind] = $match;
  17180. $any_field_matches = 1 if defined $match;
  17181. }
  17182. ll(5) && do_log(5, 'lookup_sql_field(%s) rec=%d, "%s" result: %s',
  17183. join(',', map(lc($_) eq lc($attr_name_to_sqlfield_name{$_}) ? $_
  17184. : $_ . '/' . $attr_name_to_sqlfield_name{$_},
  17185. @result_attr_names)),
  17186. $ind, $addr,
  17187. join(', ', map(defined $_ ? '"'.$_.'"' : 'undef',
  17188. @match_values_by_ind)) );
  17189. if ($any_field_matches) {
  17190. push(@matchingkey, $mk);
  17191. push(@result, !ref $field ? $match_values_by_ind[0] :
  17192. { map( ($result_attr_names[$_], $match_values_by_ind[$_]),
  17193. grep(defined $match_values_by_ind[$_],
  17194. (0 .. $#result_attr_names) )) } );
  17195. last if !$get_all;
  17196. }
  17197. }
  17198. do_log(5, 'lookup_sql_field, no such fields: %s',
  17199. join(', ', keys %nosuchfield)) if ll(5) && %nosuchfield;
  17200. }
  17201. }
  17202. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  17203. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  17204. }
  17205. 1;
  17206. #
  17207. package Amavis::Lookup::SQL;
  17208. use strict;
  17209. use re 'taint';
  17210. use warnings;
  17211. use warnings FATAL => qw(utf8 void);
  17212. BEGIN {
  17213. require Exporter;
  17214. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  17215. $VERSION = '2.316';
  17216. @ISA = qw(Exporter);
  17217. import Amavis::Conf qw(:platform :confvars c cr ca);
  17218. import Amavis::Timing qw(section_time);
  17219. import Amavis::Util qw(untaint untaint_inplace snmp_count
  17220. ll do_log do_log_safe);
  17221. import Amavis::rfc2821_2822_Tools qw(make_query_keys);
  17222. import Amavis::Out::SQL::Connection ();
  17223. }
  17224. use DBI qw(:sql_types);
  17225. # return a new Lookup::SQL object to contain DBI handle and prepared selects
  17226. #
  17227. sub new {
  17228. my($class, $conn_h, $clause_name) = @_;
  17229. if ($clause_name eq '') { undef }
  17230. else {
  17231. # $clause_name is a key into %sql_clause of the currently selected
  17232. # policy bank; one level of indirection is allowed in %sql_clause result,
  17233. # the resulting SQL clause may include %k, %a, %l, %u, %e, %d placeholders,
  17234. # to be expanded
  17235. bless { conn_h => $conn_h, incarnation => 0, clause_name => $clause_name },
  17236. $class;
  17237. }
  17238. }
  17239. sub DESTROY {
  17240. my $self = shift; local($@,$!,$_);
  17241. do_log_safe(5,"Amavis::Lookup::SQL DESTROY called");
  17242. }
  17243. sub init {
  17244. my $self = $_[0];
  17245. if ($self->{incarnation} != $self->{conn_h}->incarnation) { # invalidated?
  17246. $self->{incarnation} = $self->{conn_h}->incarnation;
  17247. $self->clear_cache; # db handle has changed, invalidate cache
  17248. }
  17249. $self;
  17250. }
  17251. sub clear_cache {
  17252. my $self = $_[0];
  17253. delete $self->{cache};
  17254. }
  17255. # lookup_sql() performs a lookup for an e-mail address against a SQL map.
  17256. # If a match is found it returns whatever the query returns (a reference
  17257. # to a hash containing values of requested fields), otherwise returns undef.
  17258. # A match aborts further fetching sequence, unless $get_all is true.
  17259. #
  17260. # SQL lookups (e.g. for user+foo@example.com) are performed in order
  17261. # which can be requested by 'ORDER BY' in the SELECT statement, otherwise
  17262. # the order is unspecified, which is only useful if only specific entries
  17263. # exist in a database (e.g. only full addresses, not domains).
  17264. #
  17265. # The following order is recommended, going from specific to more general:
  17266. # - lookup for user+foo@example.com
  17267. # - lookup for user@example.com (only if $recipient_delimiter nonempty)
  17268. # - lookup for user+foo ('naked lookup' (i.e. no '@'): only if local)
  17269. # - lookup for user ('naked lookup': local and $recipient_delimiter nonempty)
  17270. # - lookup for @sub.example.com
  17271. # - lookup for @.sub.example.com
  17272. # - lookup for @.example.com
  17273. # - lookup for @.com
  17274. # - lookup for @. (catchall)
  17275. # NOTE:
  17276. # this is different from hash and ACL lookups in two important aspects:
  17277. # - a key without '@' implies a mailbox (=user) name, not domain name;
  17278. # - a naked mailbox name (i.e. no '@' in the query) lookups are only
  17279. # performed when the e-mail address (usually its domain part) matches
  17280. # static local_domains* lookups.
  17281. #
  17282. # Domain part is always lowercased when constructing a key,
  17283. # localpart is lowercased unless $localpart_is_case_sensitive is true.
  17284. #
  17285. sub lookup_sql($$$%) {
  17286. my($self, $addr,$get_all,%options) = @_;
  17287. my(@matchingkey,@result);
  17288. my $extra_args = $options{ExtraArguments};
  17289. my $sel; my $sql_cl_r = cr('sql_clause');
  17290. my $clause_name = $self->{clause_name};
  17291. $sel = $sql_cl_r->{$clause_name} if defined $sql_cl_r;
  17292. $sel = $$sel if ref $sel eq 'SCALAR'; # allow one level of indirection
  17293. if (!defined($sel) || $sel eq '') {
  17294. ll(4) && do_log(4,"lookup_sql disabled for clause: %s", $clause_name);
  17295. return(!wantarray ? undef : (undef,undef));
  17296. } elsif (!defined $extra_args &&
  17297. exists $self->{cache} && exists $self->{cache}->{$addr})
  17298. { # cached ?
  17299. my $c = $self->{cache}->{$addr}; @result = @$c if ref $c;
  17300. @matchingkey = map('/cached/',@result); # will do for now, improve some day
  17301. # if (!ll(5)) {}# don't bother preparing log report which will not be printed
  17302. # elsif (!@result) { do_log(5,'lookup_sql (cached): "%s" no match', $addr) }
  17303. # else {
  17304. # for my $m (@result) {
  17305. # do_log(5, "lookup_sql (cached): \"%s\" matches, result=(%s)",
  17306. # $addr, join(", ", map { sprintf("%s=>%s", $_,
  17307. # !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
  17308. # ) } sort keys(%$m) ) );
  17309. # }
  17310. # }
  17311. if (!$get_all) {
  17312. return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
  17313. } else {
  17314. return(!wantarray ? \@result : (\@result, \@matchingkey));
  17315. }
  17316. }
  17317. my $is_local; # not looked up in SQL and LDAP to avoid recursion!
  17318. $is_local = Amavis::Lookup::lookup(0,$addr,
  17319. grep(ref ne 'Amavis::Lookup::SQL' &&
  17320. ref ne 'Amavis::Lookup::SQLfield' &&
  17321. ref ne 'Amavis::Lookup::LDAP' &&
  17322. ref ne 'Amavis::Lookup::LDAPattr',
  17323. @{ca('local_domains_maps')}));
  17324. my($keys_ref,$rhs_ref) = make_query_keys($addr,
  17325. $sql_lookups_no_at_means_domain,$is_local);
  17326. if (!$sql_allow_8bit_address) { s/[^\040-\176]/?/g for @$keys_ref }
  17327. my $n = scalar(@$keys_ref); # number of keys
  17328. my(@extras_tmp,@pos_args); local($1);
  17329. @extras_tmp = @$extra_args if $extra_args;
  17330. my $sel_taint = substr($sel,0,0); # taintedness
  17331. my $datatype = $sql_allow_8bit_address ? SQL_VARBINARY : SQL_VARCHAR;
  17332. # substitute %k for a list of keys, %a for unmodified full mail address,
  17333. # %l for full unmodified localpart, %u for lowercased username (a localpart
  17334. # without extension), %e for lowercased extension, %d for lowercased domain,
  17335. # and ? for each extra argument
  17336. $sel =~ s{ ( %[kaluedL] | \? ) }
  17337. { push(@pos_args,
  17338. $1 eq '%k' ? map([$_,$datatype], @$keys_ref)
  17339. : $1 eq '%a' ? [$rhs_ref->[0], $datatype] #full addr
  17340. : $1 eq '%l' ? [$rhs_ref->[1], $datatype] #localpart
  17341. : $1 eq '%u' ? [$rhs_ref->[2], $datatype] #username
  17342. : $1 eq '%e' ? [$rhs_ref->[3], $datatype] #extension
  17343. : $1 eq '%d' ? [$rhs_ref->[4], $datatype] #domain
  17344. #*** (%L is experimental, incomplete)
  17345. : $1 eq '%L' ? [($is_local?'1':'0'), SQL_BOOLEAN] #is local
  17346. : shift @extras_tmp),
  17347. $1 eq '%k' ? join(',', ('?') x $n) : '?' }gxe;
  17348. $sel = untaint($sel) . $sel_taint; # keep original clause taintedness
  17349. ll(4) && do_log(4,"lookup_sql %s \"%s\", query args: %s",
  17350. $clause_name, $addr,
  17351. join(', ', map(!ref $_ ? '"'.$_.'"' : '['.join(',',@$_).']',
  17352. @pos_args)) );
  17353. ll(4) && do_log(4,"lookup_sql select: %s", $sel);
  17354. my $a_ref; my $match = {}; my $conn_h = $self->{conn_h};
  17355. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  17356. my $driver = $conn_h->driver_name; # only available when connected
  17357. if ($driver eq 'Pg') {
  17358. $datatype = { pg_type => DBD::Pg::PG_BYTEA() };
  17359. for (@pos_args)
  17360. { $_->[1] = $datatype if ref($_) && $_->[1]==SQL_VARBINARY }
  17361. }
  17362. for (@pos_args)
  17363. { if (ref $_) { untaint_inplace($_->[0]) } else { untaint_inplace($_) } }
  17364. eval {
  17365. snmp_count('OpsSqlSelect');
  17366. $conn_h->execute($sel,@pos_args); # do the query
  17367. # fetch query results
  17368. while ( defined($a_ref=$conn_h->fetchrow_arrayref($sel)) ) {
  17369. my(@names) = @{$conn_h->sth($sel)->{NAME_lc}};
  17370. $match = {}; @$match{@names} = @$a_ref;
  17371. if ($clause_name eq 'sel_policy' && !exists $match->{'local'} &&
  17372. defined $match->{'email'} && $match->{'email'} eq '@.') {
  17373. # UGLY HACK to let a catchall (@.) imply that field 'local' has
  17374. # a value undef (NULL) when that field is not present in the
  17375. # database. This overrides B1 fieldtype default by an explicit
  17376. # undef for '@.', causing a fallback to static lookup tables.
  17377. # The purpose is to provide a useful default for local_domains
  17378. # lookup if the field 'local' is not present in the SQL table.
  17379. # NOTE: field names 'local' and 'email' are hardwired here!!!
  17380. push(@names,'local'); $match->{'local'} = undef;
  17381. do_log(5, 'lookup_sql: "%s" matches catchall, local=>undef', $addr);
  17382. }
  17383. push(@result, {%$match}); # copy hash
  17384. push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_,
  17385. !defined($match->{$_})?'-':'"'.$match->{$_}.'"'
  17386. ) } @names));
  17387. last if !$get_all;
  17388. }
  17389. $conn_h->finish($sel) if defined $a_ref; # only if not all read
  17390. 1;
  17391. } or do {
  17392. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  17393. do_log(-1, "lookup_sql: %s, %s, %s", $err, $DBI::err, $DBI::errstr);
  17394. die $err if $err =~ /^timed out\b/; # resignal timeout
  17395. die $err;
  17396. };
  17397. if (!ll(4)) {
  17398. # don't bother preparing log report which will not be printed
  17399. } elsif (!@result) {
  17400. do_log(4,'lookup_sql, "%s" no match', $addr);
  17401. } else {
  17402. do_log(4,'lookup_sql(%s) matches, result=(%s)', $addr,$_) for @matchingkey;
  17403. }
  17404. # save for future use, but only within processing of this message
  17405. $self->{cache}->{$addr} = \@result;
  17406. section_time('lookup_sql');
  17407. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  17408. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  17409. }
  17410. 1;
  17411. __DATA__
  17412. #^L
  17413. package Amavis::LDAP::Connection;
  17414. use strict;
  17415. use re 'taint';
  17416. use warnings;
  17417. use warnings FATAL => qw(utf8 void);
  17418. no warnings 'uninitialized';
  17419. use Net::LDAP;
  17420. use Net::LDAP::Util;
  17421. BEGIN {
  17422. require Exporter;
  17423. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
  17424. $have_sasl $ldap_sys_default);
  17425. $VERSION = '2.316';
  17426. @ISA = qw(Exporter);
  17427. $have_sasl = eval { require Authen::SASL };
  17428. import Amavis::Conf qw(:platform :confvars c cr ca);
  17429. import Amavis::Util qw(ll do_log);
  17430. import Amavis::Timing qw(section_time);
  17431. }
  17432. BEGIN {
  17433. # must be in a separate BEGIN block to be able to see imported symbols
  17434. $ldap_sys_default = {
  17435. hostname => 'localhost',
  17436. localaddr => undef,
  17437. port => undef, # 389 or 636, default provided by Net::LDAP
  17438. scheme => undef, # 'ldaps' or 'ldap', depending on hostname
  17439. inet6 => $have_inet6 ? 1 : 0,
  17440. version => 3,
  17441. timeout => 120,
  17442. deref => 'find',
  17443. bind_dn => undef,
  17444. bind_password => undef,
  17445. tls => 0,
  17446. verify => 'none',
  17447. sslversion => 'tlsv1',
  17448. clientcert => undef,
  17449. clientkey => undef,
  17450. cafile => undef,
  17451. capath => undef,
  17452. sasl => 0,
  17453. sasl_mech => undef, # space-separated list of mech names
  17454. sasl_auth_id => undef,
  17455. };
  17456. 1;
  17457. }
  17458. sub new {
  17459. my($class,$default) = @_;
  17460. my $self = bless { ldap => undef }, $class;
  17461. $self->{incarnation} = 1;
  17462. for (qw(hostname localaddr port scheme inet6 version timeout
  17463. base scope deref bind_dn bind_password
  17464. tls verify sslversion clientcert clientkey cafile capath
  17465. sasl sasl_mech sasl_auth_id)) {
  17466. # replace undefined attributes with user values or defaults
  17467. $self->{$_} = $default->{$_} if !defined($self->{$_});
  17468. $self->{$_} = $ldap_sys_default->{$_} if !defined($self->{$_});
  17469. }
  17470. if (!defined $self->{scheme}) {
  17471. $self->{scheme} = $self->{hostname} =~ /^ldaps/i ? 'ldaps' : 'ldap';
  17472. }
  17473. $self;
  17474. }
  17475. sub ldap { # get/set ldap handle
  17476. my $self = shift;
  17477. !@_ ? $self->{ldap} : ($self->{ldap}=shift);
  17478. }
  17479. sub DESTROY {
  17480. my $self = shift; local($@,$!,$_);
  17481. do_log_safe(5,"Amavis::LDAP::Connection DESTROY called");
  17482. # ignore failure, make perlcritic happy
  17483. eval { $self->disconnect_from_ldap } or 1;
  17484. }
  17485. sub incarnation { my $self = shift; $self->{incarnation} }
  17486. sub in_transaction { 0 }
  17487. sub begin_work {
  17488. my $self = shift;
  17489. do_log(5,"ldap begin_work");
  17490. $self->ldap or $self->connect_to_ldap;
  17491. }
  17492. sub connect_to_ldap {
  17493. my $self = shift;
  17494. my($bind_err,$start_tls_err);
  17495. do_log(3,"Connecting to LDAP server");
  17496. my $hostlist = ref $self->{hostname} eq 'ARRAY' ?
  17497. join(", ",@{$self->{hostname}}) : $self->{hostname};
  17498. do_log(4,"connect_to_ldap: trying %s", $hostlist);
  17499. my $ldap = Net::LDAP->new($self->{hostname},
  17500. localaddr => $self->{localaddr},
  17501. port => $self->{port},
  17502. scheme => $self->{scheme},
  17503. inet6 => $self->{inet6},
  17504. version => $self->{version},
  17505. timeout => $self->{timeout},
  17506. );
  17507. if (!$ldap) { # connect failed
  17508. do_log(-1,"connect_to_ldap: unable to connect to host %s", $hostlist);
  17509. } else {
  17510. do_log(3,"connect_to_ldap: connected to %s", $hostlist);
  17511. if ($self->{tls}) { # TLS required
  17512. my $mesg = $ldap->start_tls(verify => $self->{verify},
  17513. sslversion => $self->{sslversion},
  17514. clientcert => $self->{clientcert},
  17515. clientkey => $self->{clientkey},
  17516. cafile => $self->{cafile},
  17517. capath => $self->{capath});
  17518. if ($mesg->code) { # start TLS failed
  17519. my $err = $mesg->error_name;
  17520. do_log(-1,"connect_to_ldap: start TLS failed: %s", $err);
  17521. $self->ldap(undef);
  17522. $start_tls_err = 1;
  17523. } else { # started TLS
  17524. do_log(3,"connect_to_ldap: TLS version %s enabled", $mesg);
  17525. }
  17526. }
  17527. if ($self->{bind_dn} || $self->{sasl}) { # bind required
  17528. my $sasl;
  17529. my $passw = $self->{bind_password};
  17530. if ($self->{sasl}) { # using SASL to authenticate?
  17531. $have_sasl or die "connect_to_ldap: SASL requested but no Authen::SASL";
  17532. $sasl = Authen::SASL->new(mechanism => $self->{sasl_mech},
  17533. callback => { user => $self->{sasl_auth_id},
  17534. pass => $passw } );
  17535. }
  17536. my $mesg = $ldap->bind($self->{bind_dn},
  17537. $sasl ? (sasl => $sasl)
  17538. : defined $passw ? (password => $passw) : ());
  17539. $passw = 'X' x length($passw) if defined $passw; # can't hurt
  17540. if ($mesg->code) { # bind failed
  17541. my $err = $mesg->error_name;
  17542. do_log(-1,"connect_to_ldap: bind failed: %s", $err);
  17543. $self->ldap(undef);
  17544. $bind_err = 1;
  17545. } else { # bind succeeded
  17546. do_log(3,"connect_to_ldap: bind %s succeeded", $self->{bind_dn});
  17547. }
  17548. }
  17549. }
  17550. $self->ldap($ldap); $self->{incarnation}++;
  17551. $ldap or die "connect_to_ldap: unable to connect";
  17552. if ($start_tls_err) { die "connect_to_ldap: start TLS failed" }
  17553. if ($bind_err) { die "connect_to_ldap: bind failed" }
  17554. section_time('ldap-connect');
  17555. $self;
  17556. }
  17557. sub disconnect_from_ldap {
  17558. my $self = shift;
  17559. return if !$self->ldap;
  17560. do_log(4,"disconnecting from LDAP");
  17561. $self->ldap->disconnect;
  17562. $self->ldap(undef);
  17563. 1;
  17564. }
  17565. sub do_search {
  17566. my($self,$base,$scope,$filter) = @_;
  17567. my($result,$error_name);
  17568. $self->ldap or die "do_search: ldap not available";
  17569. do_log(5,'lookup_ldap: searching base="%s", scope="%s", filter="%s"',
  17570. $base, $scope, $filter);
  17571. eval {
  17572. $result = $self->{ldap}->search(base => $base,
  17573. scope => $scope,
  17574. filter => $filter,
  17575. deref => $self->{deref},
  17576. );
  17577. if ($result->code) {
  17578. $error_name = $result->error_name;
  17579. if ($error_name eq 'LDAP_NO_SUCH_OBJECT') {
  17580. # probably alright, e.g. a foreign %d
  17581. do_log(4, 'do_search failed in "%s": %s', $base, $error_name);
  17582. } else {
  17583. die $error_name."\n";
  17584. }
  17585. }
  17586. 1;
  17587. } or do {
  17588. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  17589. die $err if $err =~ /^timed out\b/; # resignal timeout
  17590. if ($err !~ /^LDAP_/) {
  17591. die "do_search: $err";
  17592. } elsif ($error_name !~ /^LDAP_(?:BUSY|UNAVAILABLE|UNWILLING_TO_PERFORM|
  17593. TIMEOUT|SERVER_DOWN|CONNECT_ERROR|OTHER)\z/x) {
  17594. die "do_search: failed: $error_name\n";
  17595. } else { # LDAP related error, worth retrying
  17596. do_log(0, "NOTICE: do_search: trying again: %s", $error_name);
  17597. $self->disconnect_from_ldap;
  17598. $self->connect_to_ldap;
  17599. $self->ldap or die "do_search: reconnect failed";
  17600. do_log(5,
  17601. 'lookup_ldap: searching (again) base="%s", scope="%s", filter="%s"',
  17602. $base, $scope, $filter);
  17603. eval {
  17604. $result = $self->{ldap}->search(base => $base,
  17605. scope => $scope,
  17606. filter => $filter,
  17607. deref => $self->{deref},
  17608. );
  17609. if ($result->code) { die $result->error_name, "\n"; }
  17610. 1;
  17611. } or do {
  17612. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  17613. $self->disconnect_from_ldap;
  17614. die $err if $err =~ /^timed out\b/; # resignal timeout
  17615. die "do_search: failed again, $err";
  17616. };
  17617. };
  17618. };
  17619. $result;
  17620. }
  17621. 1;
  17622. #
  17623. package Amavis::Lookup::LDAPattr;
  17624. use strict;
  17625. use re 'taint';
  17626. BEGIN {
  17627. require Exporter;
  17628. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  17629. $VERSION = '2.316';
  17630. @ISA = qw(Exporter);
  17631. import Amavis::Util qw(ll do_log);
  17632. import Amavis::Conf qw($trim_trailing_space_in_lookup_result_fields);
  17633. }
  17634. # the sub new() is already declared in the always-loaded code section
  17635. # attrtype: B=boolean, N=numeric, S=string, L=list
  17636. # N-: numeric, nonexistent field returns undef without complaint
  17637. # S-: string, nonexistent field returns undef without complaint
  17638. # L-: list, nonexistent field returns undef without complaint
  17639. # B-: boolean, nonexistent field returns undef without complaint
  17640. # B0: boolean, nonexistent field treated as false
  17641. # B1: boolean, nonexistent field treated as true
  17642. sub lookup_ldap_attr($$$%) {
  17643. my($self, $addr, $get_all, %options) = @_;
  17644. my(@result, @matchingkey, $ldap_query, $attr);
  17645. if ($self) { $ldap_query = $self->{ldap_query}; $attr = $self->{attrname} }
  17646. $ldap_query = $Amavis::ldap_lookups if !defined $ldap_query; # global dflt
  17647. if (!defined $self) {
  17648. do_log(5, 'lookup_ldap_attr - no attr query object, "%s" no match',$addr);
  17649. } elsif (!defined $attr || $attr eq '') {
  17650. do_log(5, 'lookup_ldap_attr() - no attribute name, "%s" no match', $addr);
  17651. } elsif (!defined $ldap_query) {
  17652. do_log(5, 'lookup_ldap_attr(%s) - no ldap_lookups object, "%s" no match',
  17653. $attr, $addr);
  17654. } else {
  17655. # result attribute names are case-sensitive
  17656. # LDAP attribute names are case-INsensitive
  17657. my(@result_attr_names) = !ref $attr ? ( $attr )
  17658. : ref $attr eq 'ARRAY' ? @$attr
  17659. : ref $attr eq 'HASH' ? keys %$attr : ();
  17660. my(%attr_name_to_ldapattr_name) =
  17661. ref $attr eq 'HASH' ? %$attr
  17662. : map( ($_,$_), @result_attr_names);
  17663. my $attrtype = $self->{attrtype};
  17664. $attrtype = 'S-' if !defined $attrtype;
  17665. my($res_ref,$mk_ref) = $ldap_query->lookup_ldap($addr,1, %options,
  17666. !exists($self->{args}) ? () : (ExtraArguments => $self->{args}));
  17667. if (!defined $res_ref || !@$res_ref) {
  17668. ll(5) && do_log(5, 'lookup_ldap_attr(%s), "%s" no matching entries',
  17669. join(',', map(lc($_) eq lc($attr_name_to_ldapattr_name{$_}) ? $_
  17670. : $_ . '/' . lc($attr_name_to_ldapattr_name{$_}),
  17671. @result_attr_names)), $addr);
  17672. } else {
  17673. my %nosuchattr;
  17674. for my $ind (0 .. $#$res_ref) {
  17675. my($any_attr_matches, @match_values_by_ind);
  17676. my $h_ref = $res_ref->[$ind]; my $mk = $mk_ref->[$ind];
  17677. for my $result_attr_ind (0 .. $#result_attr_names) {
  17678. my $result_attr_name = $result_attr_names[$result_attr_ind];
  17679. next if !defined $result_attr_name;
  17680. my $attrname = $attr_name_to_ldapattr_name{$result_attr_name};
  17681. next if !defined $attrname || $attrname eq '';
  17682. my $match;
  17683. if (!exists($h_ref->{lc $attrname})) {
  17684. $nosuchattr{$attrname} = 1;
  17685. # LDAP entry found, but no attribute with that name in it
  17686. if ($attrtype =~ /^.-/s) { # allowed to not exist
  17687. # this type is almost universally in use now, continue searching
  17688. } elsif ($attrtype =~ /^B1/) { # defaults to true
  17689. # only used for the 'local' attr
  17690. $match = 1; # nonexistent attribute treated as 1
  17691. } elsif ($attrtype =~ /^B0/) { # boolean, defaults to false
  17692. # no longer in use
  17693. $match = 0; # nonexistent attribute treated as 0
  17694. } else {
  17695. # treated as 'no match', returns undef
  17696. }
  17697. } else { # attribute exists
  17698. # attrtype: B=boolean, N=numeric, S=string
  17699. $match = $h_ref->{lc $attrname};
  17700. if (!defined $match) {
  17701. # NULL attribute values represented as undef
  17702. } elsif ($attrtype =~ /^B/) { # boolean
  17703. $match = $match eq 'TRUE' ? 1 : 0; # convert TRUE|FALSE to 1|0
  17704. } elsif ($attrtype =~ /^N/) { # numeric
  17705. $match = $match + 0; # unify different numeric forms
  17706. } elsif ($attrtype =~ /^S/) { # string
  17707. $match =~ s/ +\z// # trim trailing spaces
  17708. if $trim_trailing_space_in_lookup_result_fields;
  17709. } elsif ($self->{attrtype} =~ /^L/) { # list
  17710. #$match = join(', ',@$match);
  17711. }
  17712. }
  17713. $match_values_by_ind[$result_attr_ind] = $match;
  17714. $any_attr_matches = 1 if defined $match;
  17715. }
  17716. ll(5) && do_log(5, 'lookup_ldap_attr(%s) rec=%d, "%s" result: %s',
  17717. join(',', map(lc($_) eq lc($attr_name_to_ldapattr_name{$_}) ? $_
  17718. : $_ . '/' . lc($attr_name_to_ldapattr_name{$_}),
  17719. @result_attr_names)),
  17720. $ind, $addr,
  17721. join(', ', map(defined $_ ? '"'.$_.'"' : 'undef',
  17722. @match_values_by_ind)) );
  17723. if ($any_attr_matches) {
  17724. push(@matchingkey, $mk);
  17725. push(@result, !ref $attr ? $match_values_by_ind[0] :
  17726. { map( ($result_attr_names[$_], $match_values_by_ind[$_]),
  17727. grep(defined $match_values_by_ind[$_],
  17728. (0 .. $#result_attr_names) )) } );
  17729. last if !$get_all;
  17730. }
  17731. }
  17732. do_log(5, 'lookup_ldap_attr, no such attrs: %s',
  17733. join(', ', keys %nosuchattr)) if ll(5) && %nosuchattr;
  17734. }
  17735. }
  17736. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  17737. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  17738. }
  17739. 1;
  17740. #
  17741. package Amavis::Lookup::LDAP;
  17742. use strict;
  17743. use re 'taint';
  17744. BEGIN {
  17745. require Exporter;
  17746. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
  17747. $ldap_sys_default @ldap_attrs @mv_ldap_attrs);
  17748. $VERSION = '2.316';
  17749. @ISA = qw(Exporter);
  17750. import Amavis::Conf qw(:platform :confvars c cr ca);
  17751. import Amavis::Timing qw(section_time);
  17752. import Amavis::Util qw(untaint untaint_inplace snmp_count
  17753. ll do_log do_log_safe);
  17754. import Amavis::rfc2821_2822_Tools qw(make_query_keys split_address);
  17755. import Amavis::LDAP::Connection ();
  17756. $ldap_sys_default = {
  17757. base => undef,
  17758. scope => 'sub',
  17759. query_filter => '(&(objectClass=amavisAccount)(mail=%m))',
  17760. };
  17761. @ldap_attrs = qw(amavisLocal amavisMessageSizeLimit
  17762. amavisVirusLover amavisSpamLover amavisUncheckedLover
  17763. amavisBannedFilesLover amavisBadHeaderLover
  17764. amavisBypassVirusChecks amavisBypassSpamChecks
  17765. amavisBypassBannedChecks amavisBypassHeaderChecks
  17766. amavisSpamTagLevel amavisSpamTag2Level amavisSpamKillLevel
  17767. amavisSpamDsnCutoffLevel amavisSpamQuarantineCutoffLevel
  17768. amavisSpamSubjectTag amavisSpamSubjectTag2 amavisSpamModifiesSubj
  17769. amavisVirusQuarantineTo amavisSpamQuarantineTo amavisBannedQuarantineTo
  17770. amavisUncheckedQuarantineTo amavisBadHeaderQuarantineTo
  17771. amavisCleanQuarantineTo amavisArchiveQuarantineTo
  17772. amavisAddrExtensionVirus amavisAddrExtensionSpam
  17773. amavisAddrExtensionBanned amavisAddrExtensionBadHeader
  17774. amavisWarnVirusRecip amavisWarnBannedRecip amavisWarnBadHeaderRecip
  17775. amavisVirusAdmin amavisNewVirusAdmin amavisSpamAdmin
  17776. amavisBannedAdmin amavisBadHeaderAdmin
  17777. amavisBannedRuleNames amavisDisclaimerOptions
  17778. amavisForwardMethod amavisSaUserConf amavisSaUserName
  17779. amavisBlacklistSender amavisWhitelistSender
  17780. );
  17781. @mv_ldap_attrs = qw(amavisBlacklistSender amavisWhitelistSender);
  17782. 1;
  17783. }
  17784. sub new {
  17785. my($class,$default,$conn_h) = @_;
  17786. my $self = bless {}, $class;
  17787. $self->{conn_h} = $conn_h; $self->{incarnation} = 0;
  17788. for (qw(base scope query_filter)) {
  17789. # replace undefined attributes with config values or defaults
  17790. $self->{$_} = $default->{$_} unless defined($self->{$_});
  17791. $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_});
  17792. }
  17793. $self;
  17794. }
  17795. sub DESTROY {
  17796. my $self = shift; local($@,$!,$_);
  17797. do_log_safe(5,"Amavis::Lookup::LDAP DESTROY called");
  17798. }
  17799. sub init {
  17800. my $self = $_[0];
  17801. if ($self->{incarnation} != $self->{conn_h}->incarnation) { # invalidated?
  17802. $self->{incarnation} = $self->{conn_h}->incarnation;
  17803. $self->clear_cache; # db handle has changed, invalidate cache
  17804. }
  17805. $self;
  17806. }
  17807. sub clear_cache {
  17808. my $self = $_[0];
  17809. delete $self->{cache};
  17810. }
  17811. sub lookup_ldap($$$%) {
  17812. my($self,$addr,$get_all,%options) = @_;
  17813. my(@result,@matchingkey,@tmp_result,@tmp_matchingkey);
  17814. if (exists $self->{cache} && exists $self->{cache}->{$addr}) { # cached?
  17815. my $c = $self->{cache}->{$addr}; @result = @$c if ref $c;
  17816. @matchingkey = map('/cached/',@result); # will do for now, improve some day
  17817. # if (!ll(5)) {
  17818. # # don't bother preparing log report which will not be printed
  17819. # } elsif (!@result) {
  17820. # do_log(5,'lookup_ldap (cached): "%s" no match', $addr);
  17821. # } else {
  17822. # for my $m (@result) {
  17823. # do_log(5, 'lookup_ldap (cached): "%s" matches, result=(%s)',
  17824. # $addr, join(", ", map { sprintf("%s=>%s", $_,
  17825. # !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
  17826. # ) } sort keys(%$m) ) );
  17827. # }
  17828. # }
  17829. if (!$get_all) {
  17830. return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
  17831. } else {
  17832. return(!wantarray ? \@result : (\@result, \@matchingkey));
  17833. }
  17834. }
  17835. my $is_local; # not looked up in SQL and LDAP to avoid recursion!
  17836. $is_local = Amavis::Lookup::lookup(0,$addr,
  17837. grep(ref ne 'Amavis::Lookup::SQL' &&
  17838. ref ne 'Amavis::Lookup::SQLfield' &&
  17839. ref ne 'Amavis::Lookup::LDAP' &&
  17840. ref ne 'Amavis::Lookup::LDAPattr',
  17841. @{ca('local_domains_maps')}));
  17842. my($keys_ref,$rhs_ref,@keys);
  17843. ($keys_ref,$rhs_ref) = make_query_keys($addr,
  17844. $ldap_lookups_no_at_means_domain,$is_local);
  17845. @keys = @$keys_ref;
  17846. unshift(@keys, '<>') if $addr eq ''; # a hack for a null return path
  17847. untaint_inplace($_) for @keys; # untaint keys
  17848. $_ = Net::LDAP::Util::escape_filter_value($_) for @keys;
  17849. # process %m
  17850. my $filter = $self->{query_filter};
  17851. my @filter_attr; my $expanded_filter = '';
  17852. for my $t ($filter =~ /\G( \( [^(=]+ = %m \) | [ \t0-9A-Za-z]+ | . )/gsx) {
  17853. if ($t !~ m{ \( ([^(=]+) = %m \) }sx) { $expanded_filter .= $t }
  17854. else {
  17855. push(@filter_attr, $1);
  17856. $expanded_filter .= '(|' . join('', map("($1=$_)", @keys)) . ')';
  17857. }
  17858. }
  17859. $filter = $expanded_filter;
  17860. # process %d
  17861. my $base = $self->{base};
  17862. if ($base =~ /%d/) {
  17863. my($localpart,$domain) = split_address($addr);
  17864. if ($domain) {
  17865. untaint_inplace($domain); $domain = lc($domain); local($1);
  17866. $domain =~ s/^\@?(.*?)\.*\z/$1/s;
  17867. $base =~ s/%d/&Net::LDAP::Util::escape_dn_value($domain)/ge;
  17868. }
  17869. }
  17870. # build hash of keys and array position
  17871. my(%xref); my $key_num = 0;
  17872. $xref{$_} = $key_num++ for @keys;
  17873. #
  17874. do_log(4,'lookup_ldap "%s", query keys: %s, base: %s, filter: %s',
  17875. $addr,join(', ',map("\"$_\"",@keys)),$self->{base},$self->{query_filter});
  17876. my $conn_h = $self->{conn_h};
  17877. $conn_h->begin_work; # (re)connect if not connected
  17878. eval {
  17879. snmp_count('OpsLDAPSearch');
  17880. my(@entry);
  17881. my $search_obj = $conn_h->do_search($base, $self->{scope}, $filter);
  17882. @entry = $search_obj->entries if $search_obj && !$search_obj->code;
  17883. my(%mv_ldap_attrs) = map((lc($_), 1), @mv_ldap_attrs);
  17884. for my $entry (@entry) {
  17885. my $match = {};
  17886. $match->{dn} = $entry->dn;
  17887. for my $attr (@ldap_attrs) {
  17888. my $value;
  17889. do_log(9,'lookup_ldap: reading attribute "%s" from object', $attr);
  17890. $attr = lc($attr);
  17891. if ($mv_ldap_attrs{$attr}) { # multivalued
  17892. $value = $entry->get_value($attr, asref => 1);
  17893. } else {
  17894. $value = $entry->get_value($attr);
  17895. }
  17896. $match->{$attr} = $value if defined $value;
  17897. }
  17898. my $pos;
  17899. for my $attr (@filter_attr) {
  17900. my $value = scalar($entry->get_value($attr));
  17901. if (defined $value) {
  17902. if (!exists $match->{'amavislocal'} && $value eq '@.') {
  17903. # NOTE: see lookup_sql
  17904. $match->{'amavislocal'} = undef;
  17905. do_log(5, 'lookup_ldap: "%s" matches catchall, amavislocal=>undef',
  17906. $addr);
  17907. }
  17908. $pos = $xref{$value};
  17909. last;
  17910. }
  17911. }
  17912. my $key_str = join(", ",map {sprintf("%s=>%s",$_,!defined($match->{$_})?
  17913. '-':'"'.$match->{$_}.'"')} keys(%$match));
  17914. push(@tmp_result, [$pos,{%$match}]); # copy hash
  17915. push(@tmp_matchingkey, [$pos,$key_str]);
  17916. last if !$get_all;
  17917. }
  17918. 1;
  17919. } or do {
  17920. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  17921. do_log(-1,"lookup_ldap: %s", $err);
  17922. die $err;
  17923. };
  17924. @result = map($_->[1], sort {$a->[0] <=> $b->[0]} @tmp_result);
  17925. @matchingkey = map($_->[1], sort {$a->[0] <=> $b->[0]} @tmp_matchingkey);
  17926. if (!ll(4)) {
  17927. # don't bother preparing log report which will not be printed
  17928. } elsif (!@result) {
  17929. do_log(4,'lookup_ldap, "%s" no match', $addr);
  17930. } else {
  17931. do_log(4,'lookup_ldap(%s) matches, result=(%s)',$addr,$_) for @matchingkey;
  17932. }
  17933. # save for future use, but only within processing of this message
  17934. $self->{cache}->{$addr} = \@result;
  17935. section_time('lookup_ldap');
  17936. if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
  17937. else { !wantarray ? \@result : (\@result, \@matchingkey) }
  17938. }
  17939. 1;
  17940. __DATA__
  17941. #
  17942. package Amavis::In::AMPDP;
  17943. use strict;
  17944. use re 'taint';
  17945. use warnings;
  17946. use warnings FATAL => qw(utf8 void);
  17947. no warnings 'uninitialized';
  17948. BEGIN {
  17949. require Exporter;
  17950. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  17951. $VERSION = '2.316';
  17952. @ISA = qw(Exporter);
  17953. import Amavis::Conf qw(:platform :confvars c cr ca);
  17954. import Amavis::Util qw(ll do_log debug_oneshot dump_captured_log
  17955. untaint snmp_counters_init read_file
  17956. snmp_count proto_encode proto_decode orcpt_encode
  17957. switch_to_my_time switch_to_client_time
  17958. am_id new_am_id add_entropy rmdir_recursively);
  17959. import Amavis::Lookup qw(lookup lookup2);
  17960. import Amavis::Lookup::IP qw(lookup_ip_acl normalize_ip_addr);
  17961. import Amavis::Timing qw(section_time);
  17962. import Amavis::rfc2821_2822_Tools;
  17963. import Amavis::In::Message;
  17964. import Amavis::In::Connection;
  17965. import Amavis::IO::Zlib;
  17966. import Amavis::Out::EditHeader qw(hdr);
  17967. import Amavis::Out qw(mail_dispatch);
  17968. import Amavis::Notify qw(msg_from_quarantine);
  17969. }
  17970. use subs @EXPORT;
  17971. use Errno qw(ENOENT EACCES);
  17972. use IO::File ();
  17973. use Time::HiRes ();
  17974. use Digest::MD5;
  17975. use MIME::Base64;
  17976. sub new($) { my($class) = @_; bless {}, $class }
  17977. # used with sendmail milter and traditional (non-SMTP) MTA interface,
  17978. # but also to request a message release from a quarantine
  17979. #
  17980. sub process_policy_request($$$$) {
  17981. my($self, $sock, $conn, $check_mail, $old_amcl) = @_;
  17982. # $sock: connected socket from Net::Server
  17983. # $conn: information about client connection
  17984. # $check_mail: subroutine ref to be called with file handle
  17985. my(%attr);
  17986. $0 = sprintf("%s (ch%d-P-idle)",
  17987. c('myprogram_name'), $Amavis::child_invocation_count);
  17988. ll(5) && do_log(5, "process_policy_request: %s, %s, fileno=%s",
  17989. $old_amcl, c('myprogram_name'), fileno($sock));
  17990. if ($old_amcl) {
  17991. # Accept a single request from traditional amavis helper program.
  17992. # Receive TEMPDIR/SENDER/RCPTS/LDA/LDAARGS from client
  17993. # Simple protocol: \2 means LDA follows; \3 means EOT (end of transmission)
  17994. die "process_policy_request: old AM.CL protocol is no longer supported\n";
  17995. } else { # new amavis helper protocol AM.PDP or a Postfix policy server
  17996. # for Postfix policy server see Postfix docs SMTPD_POLICY_README
  17997. my(@response); local($1,$2,$3);
  17998. local($/) = "\012"; # set line terminator to LF (Postfix idiosyncrasy)
  17999. my $ln; # can accept multiple tasks
  18000. switch_to_client_time("start receiving AM.PDP data");
  18001. $conn->appl_proto('AM.PDP');
  18002. for ($! = 0; defined($ln=$sock->getline); $! = 0) {
  18003. my $end_of_request = $ln =~ /^\015?\012\z/ ? 1 : 0; # end of request?
  18004. switch_to_my_time($end_of_request ? 'rx entire AM.PDP request'
  18005. : 'rx AM.PDP line');
  18006. $0 = sprintf("%s (ch%d-P)",
  18007. c('myprogram_name'), $Amavis::child_invocation_count);
  18008. Amavis::Timing::init(); snmp_counters_init();
  18009. # must not use \r and \n, not the same as \015 and \012 on some platforms
  18010. if ($end_of_request) { # end of request
  18011. section_time('got data');
  18012. my $msg_size;
  18013. eval {
  18014. my($msginfo,$bank_names_ref) = preprocess_policy_query(\%attr,$conn);
  18015. $Amavis::MSGINFO = $msginfo; # ugly
  18016. my $req = lc($attr{'request'});
  18017. @response = $req eq 'smtpd_access_policy'
  18018. ? postfix_policy($msginfo,\%attr)
  18019. : $req =~ /^(?:release|requeue|report)\z/
  18020. ? dispatch_from_quarantine($msginfo, $req,
  18021. $req eq 'report' ? 'abuse' : 'miscategorized')
  18022. : check_ampdp_policy($msginfo,$check_mail,0,$bank_names_ref);
  18023. $msg_size = $msginfo->msg_size;
  18024. undef $Amavis::MSGINFO; # release global reference
  18025. 1;
  18026. } or do {
  18027. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  18028. do_log(-2, "policy_server FAILED: %s", $err);
  18029. @response = (proto_encode('setreply','450','4.5.0',"Failure: $err"),
  18030. proto_encode('return_value','tempfail'),
  18031. proto_encode('exit_code',sprintf("%d",EX_TEMPFAIL)));
  18032. die $err if $err =~ /^timed out\b/; # resignal timeout
  18033. # last;
  18034. };
  18035. $sock->print( join('', map($_."\015\012", (@response,'')) ))
  18036. or die "Can't write response to socket: $!, fileno=".fileno($sock);
  18037. %attr = (); @response = ();
  18038. ll(2) && do_log(2,"size: %d, %s", $msg_size, Amavis::Timing::report());
  18039. } elsif ($ln =~ /^ ([^=\000\012]*?) (=|:[ \t]*)
  18040. ([^\012]*?) \015?\012 \z/xsi) {
  18041. my $attr_name = proto_decode($1);
  18042. my $attr_val = proto_decode($3);
  18043. if (!exists $attr{$attr_name}) {
  18044. $attr{$attr_name} = $attr_val;
  18045. } else {
  18046. $attr{$attr_name} = [ $attr{$attr_name} ] if !ref $attr{$attr_name};
  18047. push(@{$attr{$attr_name}}, $attr_val);
  18048. }
  18049. my $known_attr = scalar(grep($_ eq $attr_name, qw(
  18050. request protocol_state version_client protocol_name helo_name
  18051. client_name client_address client_port client_source sender recipient
  18052. delivery_care_of queue_id partition_tag mail_id secret_id quar_type
  18053. mail_file tempdir tempdir_removed_by policy_bank requested_by) ));
  18054. do_log(!$known_attr?1:3,
  18055. "policy protocol: %s=%s", $attr_name,$attr_val);
  18056. } else {
  18057. do_log(-1, "policy protocol: INVALID AM.PDP ATTRIBUTE LINE: %s", $ln);
  18058. }
  18059. $0 = sprintf("%s (ch%d-P-idle)",
  18060. c('myprogram_name'), $Amavis::child_invocation_count);
  18061. switch_to_client_time("receiving AM.PDP data");
  18062. }
  18063. defined $ln || $! == 0 or die "Read from client socket FAILED: $!";
  18064. switch_to_my_time('end of AM.PDP session');
  18065. };
  18066. $0 = sprintf("%s (ch%d-P)",
  18067. c('myprogram_name'), $Amavis::child_invocation_count);
  18068. }
  18069. # Based on given query attributes describing a message to be checked or
  18070. # released, return a new Amavis::In::Message object with filled-in information
  18071. #
  18072. sub preprocess_policy_query($$) {
  18073. my($attr_ref,$conn) = @_;
  18074. my $now = Time::HiRes::time;
  18075. my $msginfo = Amavis::In::Message->new;
  18076. $msginfo->rx_time($now);
  18077. $msginfo->log_id(am_id());
  18078. $msginfo->conn_obj($conn);
  18079. add_entropy(%$attr_ref);
  18080. # amavisd -> amavis-helper protocol query consists of any number of
  18081. # the following lines, the response is terminated by an empty line.
  18082. # The 'request=AM.PDP' is a required first field, the order of
  18083. # remaining fields is arbitrary, but multivalued attributes such as
  18084. # 'recipient' must retain their relative order.
  18085. # Required AM.PDP fields are: request, tempdir, sender, recipient(s)
  18086. # request=AM.PDP
  18087. # version_client=n (currently ignored)
  18088. # tempdir=/var/amavis/amavis-milter-MWZmu9Di
  18089. # tempdir_removed_by=client (tempdir_removed_by=server is a default)
  18090. # mail_file=/var/amavis/am.../email.txt (defaults to tempdir/email.txt)
  18091. # sender=<foo@example.com>
  18092. # recipient=<bar1@example.net>
  18093. # recipient=<bar2@example.net>
  18094. # recipient=<bar3@example.net>
  18095. # delivery_care_of=server (client or server, client is a default)
  18096. # queue_id=qid
  18097. # protocol_name=ESMTP
  18098. # helo_name=host.example.com
  18099. # client_address=10.2.3.4
  18100. # client_port=45678
  18101. # client_name=host.example.net
  18102. # client_source=LOCAL/REMOTE/[UNAVAILABLE]
  18103. # (matches local_header_rewrite_clients, see Postfix XFORWARD_README)
  18104. # policy_bank=SMTP_AUTH,TLS,ORIGINATING,MYNETS,...
  18105. # Required 'release' or 'requeue' or 'report' fields are: request, mail_id
  18106. # request=release (or request=requeue, or request=report)
  18107. # mail_id=xxxxxxxxxxxx
  18108. # secret_id=xxxxxxxxxxxx (authorizes a release/report)
  18109. # partition_tag=xx (required if mail_id is not unique)
  18110. # quar_type=x F/Z/B/Q/M (defaults to Q or F)
  18111. # file/zipfile/bsmtp/sql/mailbox
  18112. # mail_file=... (optional: overrides automatics; $QUARANTINEDIR prepended)
  18113. # requested_by=<releaser@example.com> (optional: lands in Resent-From:)
  18114. # sender=<foo@example.com> (optional: replaces envelope sender)
  18115. # recipient=<bar1@example.net> (optional: replaces envelope recips)
  18116. # recipient=<bar2@example.net>
  18117. # recipient=<bar3@example.net>
  18118. my(@recips); my(@bank_names);
  18119. exists $attr_ref->{'request'} or die "Missing 'request' field";
  18120. my $ampdp = $attr_ref->{'request'} =~
  18121. /^(?:AM\.CL|AM\.PDP|release|requeue|report)\z/i;
  18122. @bank_names = grep($_ ne '',
  18123. map { my $s = $_; $s =~ s/^[ \t]+//; $s =~ s/[ \t]+\z//; $s }
  18124. split(/,/, $attr_ref->{'policy_bank'}))
  18125. if exists $attr_ref->{'policy_bank'};
  18126. my $d_co = $attr_ref->{'delivery_care_of'};
  18127. my $td_rm = $attr_ref->{'tempdir_removed_by'};
  18128. $msginfo->client_delete(defined($td_rm) && lc($td_rm) eq 'client' ? 1 : 0);
  18129. $msginfo->queue_id($attr_ref->{'queue_id'})
  18130. if exists $attr_ref->{'queue_id'};
  18131. $msginfo->client_proto($attr_ref->{'protocol_name'})
  18132. if exists $attr_ref->{'protocol_name'};
  18133. if (exists $attr_ref->{'client_address'}) {
  18134. $msginfo->client_addr(normalize_ip_addr($attr_ref->{'client_address'}));
  18135. }
  18136. $msginfo->client_port($attr_ref->{'client_port'})
  18137. if exists $attr_ref->{'client_port'};
  18138. $msginfo->client_name($attr_ref->{'client_name'})
  18139. if exists $attr_ref->{'client_name'};
  18140. $msginfo->client_source($attr_ref->{'client_source'})
  18141. if exists $attr_ref->{'client_source'}
  18142. && uc($attr_ref->{'client_source'}) ne '[UNAVAILABLE]';
  18143. $msginfo->client_helo($attr_ref->{'helo_name'})
  18144. if exists $attr_ref->{'helo_name'};
  18145. # $msginfo->body_type('8BITMIME');
  18146. $msginfo->requested_by(unquote_rfc2821_local($attr_ref->{'requested_by'}))
  18147. if exists $attr_ref->{'requested_by'};
  18148. if (exists $attr_ref->{'sender'}) {
  18149. my $sender = $attr_ref->{'sender'};
  18150. $sender = '<'.$sender.'>' if $sender !~ /^<.*>\z/;
  18151. $msginfo->sender_smtp($sender);
  18152. $sender = unquote_rfc2821_local($sender);
  18153. $msginfo->sender($sender);
  18154. }
  18155. if (exists $attr_ref->{'recipient'}) {
  18156. my $r = $attr_ref->{'recipient'}; @recips = ();
  18157. for my $addr (!ref($r) ? $r : @$r) {
  18158. my $addr_quo = $addr;
  18159. my $addr_unq = unquote_rfc2821_local($addr);
  18160. $addr_quo = '<'.$addr_quo.'>' if $addr_quo !~ /^<.*>\z/;
  18161. my $recip_obj = Amavis::In::Message::PerRecip->new;
  18162. $recip_obj->recip_addr($addr_unq);
  18163. $recip_obj->recip_addr_smtp($addr_quo);
  18164. $recip_obj->dsn_orcpt(orcpt_encode($addr_quo));
  18165. $recip_obj->recip_destiny(D_PASS); # default is Pass
  18166. $recip_obj->delivery_method('') if !defined($d_co) ||
  18167. lc($d_co) eq 'client';
  18168. push(@recips,$recip_obj);
  18169. }
  18170. $msginfo->per_recip_data(\@recips);
  18171. }
  18172. if (!exists $attr_ref->{'tempdir'}) {
  18173. my $tempdir = Amavis::TempDir->new;
  18174. $tempdir->prepare_dir;
  18175. $msginfo->mail_tempdir($tempdir->path);
  18176. # Save the Amavis::TempDir object from destruction by keeping a ref to it
  18177. # in $msginfo. When $msginfo is destroyed, the temporary directory will be
  18178. # automatically destroyed too. This is specific to AM.PDP requests without
  18179. # a working directory provided by a caller, and different from usual
  18180. # SMTP sessions which keep a per-process permanent reference to an
  18181. # Amavis::TempDir object, which makes keeping it in mail_tempdir_obj
  18182. # not necessary.
  18183. $msginfo->mail_tempdir_obj($tempdir);
  18184. } else {
  18185. local($1,$2); my $tempdir = $attr_ref->{tempdir};
  18186. $tempdir =~ m{^ (?: \Q$TEMPBASE\E | \Q$MYHOME\E )
  18187. (?: / (?! \.\. (?:\z|/)) [A-Za-z0-9_.-]+ )*
  18188. / [A-Za-z0-9_.-]+ \z}xso
  18189. or die "Suspicious temporary directory name '$tempdir'";
  18190. $msginfo->mail_tempdir(untaint($tempdir));
  18191. }
  18192. my $quar_type;
  18193. if (!$ampdp) {
  18194. # don't bother with filenames
  18195. } elsif ($attr_ref->{'request'} =~ /^(?:release|requeue|report)\z/i) {
  18196. exists $attr_ref->{'mail_id'} or die "Missing 'mail_id' field";
  18197. $msginfo->partition_tag($attr_ref->{'partition_tag'}); # may be undef
  18198. my $mail_id = $attr_ref->{'mail_id'};
  18199. # amavisd almost-base64: 62 +, 63 - (in use up to 2.6.4, dropped in 2.7.0)
  18200. # RFC 4648 base64: 62 +, 63 / (not used here)
  18201. # RFC 4648 base64url: 62 -, 63 _
  18202. $mail_id =~ m{^ [A-Za-z0-9] [A-Za-z0-9_+-]* ={0,2} \z}xs
  18203. or die "Invalid mail_id '$mail_id'";
  18204. $msginfo->mail_id(untaint($mail_id));
  18205. if (!exists($attr_ref->{'secret_id'}) || $attr_ref->{'secret_id'} eq '') {
  18206. die "Secret_id is required, but missing" if c('auth_required_release');
  18207. } else {
  18208. # version 2.7.0 and later uses RFC 4648 base64url and id=b64(md5(sec)),
  18209. # versions before 2.7.0 used almost-base64 and id=b64(md5(b64(sec)))
  18210. { # begin block, 'last' exits it
  18211. my $secret_b64 = $attr_ref->{'secret_id'};
  18212. $secret_b64 = '' if !defined $secret_b64;
  18213. if (index($secret_b64,'+') < 0) { # new or undetermined format
  18214. local($_) = $secret_b64; tr{-_}{+/}; # revert base64url to base64
  18215. my $secret_bin = decode_base64($_);
  18216. my $id_new_b64 = Digest::MD5->new->add($secret_bin)->b64digest;
  18217. $id_new_b64 = substr($id_new_b64, 0, 12);
  18218. $id_new_b64 =~ tr{+/}{-_}; # base64 -> RFC 4648 base64url
  18219. last if $id_new_b64 eq $mail_id; # exit enclosing block
  18220. }
  18221. if (index($secret_b64,'_') < 0) { # old or undetermined format
  18222. my $id_old_b64 = Digest::MD5->new->add($secret_b64)->b64digest;
  18223. $id_old_b64 = substr($id_old_b64, 0, 12);
  18224. $id_old_b64 =~ tr{/}{-}; # base64 -> almost-base64
  18225. last if $id_old_b64 eq $mail_id; # exit enclosing block
  18226. }
  18227. die "Secret_id $secret_b64 does not match mail_id $mail_id";
  18228. }; # end block, 'last' arrives here
  18229. }
  18230. $quar_type = $attr_ref->{'quar_type'};
  18231. if (!defined($quar_type) || $quar_type eq '') {
  18232. # choose some reasonable default (simpleminded)
  18233. $quar_type = c('spam_quarantine_method') =~ /^sql:/i ? 'Q' : 'F';
  18234. }
  18235. my $fn = $mail_id;
  18236. if ($quar_type eq 'F' || $quar_type eq 'Z') {
  18237. $QUARANTINEDIR ne '' or die "Config variable \$QUARANTINEDIR is empty";
  18238. if ($attr_ref->{'mail_file'} ne '') {
  18239. $fn = $attr_ref->{'mail_file'};
  18240. $fn =~ m{^[A-Za-z0-9][A-Za-z0-9/_.+-]*\z}s && $fn !~ m{\.\.(?:/|\z)}
  18241. or die "Unsafe filename '$fn'";
  18242. $fn = $QUARANTINEDIR.'/'.untaint($fn);
  18243. } else { # automatically guess a filename - simpleminded
  18244. if ($quarantine_subdir_levels < 1) { $fn = "$QUARANTINEDIR/$fn" }
  18245. else { my $subd = substr($fn,0,1); $fn = "$QUARANTINEDIR/$subd/$fn" }
  18246. $fn .= '.gz' if $quar_type eq 'Z';
  18247. }
  18248. }
  18249. $msginfo->mail_text_fn($fn);
  18250. } elsif (!exists $attr_ref->{'mail_file'}) {
  18251. $msginfo->mail_text_fn($msginfo->mail_tempdir . '/email.txt');
  18252. } else {
  18253. # SECURITY: just believe the supplied file name, blindly untainting it
  18254. $msginfo->mail_text_fn(untaint($attr_ref->{'mail_file'}));
  18255. }
  18256. my $fname = $msginfo->mail_text_fn;
  18257. if ($ampdp && defined($fname) && $fname ne '') {
  18258. my $fh;
  18259. my $releasing = $attr_ref->{'request'}=~ /^(?:release|requeue|report)\z/i;
  18260. new_am_id('rel-'.$msginfo->mail_id) if $releasing;
  18261. if ($releasing && $quar_type eq 'Q') { # releasing from SQL
  18262. do_log(5, "preprocess_policy_query: opening in sql: %s",
  18263. $msginfo->mail_id);
  18264. my $obj = $Amavis::sql_storage;
  18265. $Amavis::extra_code_sql_quar && $obj
  18266. or die "SQL quarantine code not enabled (3)";
  18267. my $conn_h = $obj->{conn_h}; my $sql_cl_r = cr('sql_clause');
  18268. my $sel_msg = $sql_cl_r->{'sel_msg'};
  18269. my $sel_quar = $sql_cl_r->{'sel_quar'};
  18270. if (!defined($msginfo->partition_tag) &&
  18271. defined($sel_msg) && $sel_msg ne '') {
  18272. do_log(5, "preprocess_policy_query: missing partition_tag in request,".
  18273. " fetching msgs record for mail_id=%s", $msginfo->mail_id);
  18274. # find a corresponding partition_tag if missing from a release request
  18275. $conn_h->begin_work_nontransaction; #(re)connect if necessary
  18276. $conn_h->execute($sel_msg, untaint($msginfo->mail_id));
  18277. my $a_ref; my $cnt = 0; my $partition_tag;
  18278. while ( defined($a_ref=$conn_h->fetchrow_arrayref($sel_msg)) ) {
  18279. $cnt++;
  18280. $partition_tag = $a_ref->[0] if !defined $partition_tag;
  18281. ll(5) && do_log(5, "release: got msgs record for mail_id=%s: %s",
  18282. $msginfo->mail_id, join(', ',@$a_ref));
  18283. }
  18284. $conn_h->finish($sel_msg) if defined $a_ref; # only if not all read
  18285. $cnt <= 1 or die "Multiple ($cnt) records with same mail_id exist, ".
  18286. "specify a partition_tag in the AM.PDP request";
  18287. if ($cnt < 1) {
  18288. do_log(0, "release: no records with msgs.mail_id=%s in a database, ".
  18289. "trying to read from a quar. anyway", $msginfo->mail_id);
  18290. }
  18291. $msginfo->partition_tag($partition_tag); # could still be undef/NULL !
  18292. }
  18293. ll(5) && do_log(5, "release: opening mail_id=%s, partition_tag=%s",
  18294. $msginfo->mail_id, $msginfo->partition_tag);
  18295. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  18296. $fh = Amavis::IO::SQL->new;
  18297. $fh->open($conn_h, $sel_quar, untaint($msginfo->mail_id),
  18298. 'r', untaint($msginfo->partition_tag))
  18299. or die "Can't open sql obj for reading: $!"; 1;
  18300. } else { # mail checking or releasing from a file
  18301. do_log(5, "preprocess_policy_query: opening mail '%s'", $fname);
  18302. # set new amavis message id
  18303. new_am_id( ($fname =~ m{amavis-(milter-)?([^/ \t]+)}s ? $2 : undef) )
  18304. if !$releasing;
  18305. # file created by amavis helper program or other client, just open it
  18306. my(@stat_list) = lstat($fname); my $errn = @stat_list ? 0 : 0+$!;
  18307. if ($errn == ENOENT) { die "File $fname does not exist" }
  18308. elsif ($errn) { die "File $fname inaccessible: $!" }
  18309. elsif (!-f _) { die "File $fname is not a plain file" }
  18310. add_entropy(@stat_list);
  18311. if ($fname =~ /\.gz\z/) {
  18312. $fh = Amavis::IO::Zlib->new;
  18313. $fh->open($fname,'rb') or die "Can't open gzipped file $fname: $!";
  18314. } else {
  18315. # $msginfo->msg_size(0 + (-s _)); # underestimates the RFC 1870 size
  18316. $fh = IO::File->new;
  18317. $fh->open($fname,'<') or die "Can't open file $fname: $!";
  18318. binmode($fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  18319. my $file_size = $stat_list[7];
  18320. if ($file_size < 100*1024) { # 100 KiB 'small mail', read into memory
  18321. do_log(5, 'preprocess_policy_query: reading from %s to memory, '.
  18322. 'file size %d bytes', $fname, $file_size);
  18323. my $str = ''; read_file($fh,\$str);
  18324. $fh->seek(0,0) or die "Can't rewind file $fname: $!";
  18325. $msginfo->mail_text_str(\$str); # save mail as a string
  18326. }
  18327. }
  18328. }
  18329. $msginfo->mail_text($fh); # save file handle to object
  18330. $msginfo->log_id(am_id());
  18331. }
  18332. if ($ampdp) {
  18333. do_log(1, "Request: %s %s %s: %s -> %s", $attr_ref->{'request'},
  18334. $attr_ref->{'mail_id'}, $msginfo->mail_tempdir,
  18335. $msginfo->sender_smtp,
  18336. join(',', map($_->recip_addr_smtp, @recips)) );
  18337. } else {
  18338. do_log(1, "Request: %s(%s): %s %s %s: %s[%s] <%s> -> <%s>",
  18339. @$attr_ref{qw(request protocol_state mail_id protocol_name
  18340. queue_id client_name client_address sender recipient)});
  18341. }
  18342. ($msginfo, \@bank_names);
  18343. }
  18344. sub check_ampdp_policy($$$$) {
  18345. my($msginfo,$check_mail,$old_amcl,$bank_names_ref) = @_;
  18346. my($smtp_resp, $exit_code, $preserve_evidence);
  18347. my(%baseline_policy_bank) = %current_policy_bank;
  18348. # do some sanity checks before deciding to call check_mail()
  18349. if (!ref($msginfo->per_recip_data) || !defined($msginfo->mail_text)) {
  18350. $smtp_resp = '450 4.5.0 Incomplete request'; $exit_code = EX_TEMPFAIL;
  18351. } else {
  18352. # loading a policy bank can affect subsequent c(), cr() and ca() results,
  18353. # so it is necessary to load each policy bank in the right order and soon
  18354. # after information becomes available; general principle is that policy
  18355. # banks are loaded in order in which information becomes available:
  18356. # interface/socket, client IP, SMTP session info, sender, ...
  18357. my $cl_ip = $msginfo->client_addr;
  18358. my $cl_src = $msginfo->client_source;
  18359. my($cl_ip_mynets, $policy_name_requested);
  18360. {
  18361. my $cl_ip_tmp = $cl_ip;
  18362. # treat unknown client IP addr as 0.0.0.0, from "This" Network, RFC 1700
  18363. $cl_ip_tmp = '0.0.0.0' if !defined($cl_ip) || $cl_ip eq '';
  18364. my(@cp) = @{ca('client_ipaddr_policy')};
  18365. do_log(-1,"\@client_ipaddr_policy must contain pairs, ".
  18366. "number of elements is not even") if @cp % 2 != 0;
  18367. while (@cp) {
  18368. my $lookup_table = shift(@cp); my $policy_name = shift(@cp);
  18369. if (lookup_ip_acl($cl_ip_tmp, $lookup_table)) {
  18370. if (defined $policy_name && $policy_name ne '') {
  18371. $policy_name_requested = $policy_name;
  18372. $cl_ip_mynets = 1 if $policy_name eq 'MYNETS'; # compatibility
  18373. }
  18374. last;
  18375. }
  18376. }
  18377. }
  18378. if (($cl_ip_mynets?1:0) > ($msginfo->originating?1:0)) {
  18379. $current_policy_bank{'originating'} = $cl_ip_mynets; # compatibility
  18380. }
  18381. if (defined $policy_name_requested &&
  18382. defined $policy_bank{$policy_name_requested}) {
  18383. Amavis::load_policy_bank($policy_name_requested,$msginfo);
  18384. }
  18385. for my $bank_name (@$bank_names_ref) { # additional banks from the request
  18386. if (defined $policy_bank{$bank_name})
  18387. { Amavis::load_policy_bank(untaint($bank_name),$msginfo) }
  18388. }
  18389. $msginfo->originating(c('originating'));
  18390. my $sender = $msginfo->sender;
  18391. if (defined $policy_bank{'MYUSERS'} &&
  18392. $sender ne '' && $msginfo->originating &&
  18393. lookup2(0,$sender, ca('local_domains_maps'))) {
  18394. Amavis::load_policy_bank('MYUSERS',$msginfo);
  18395. $msginfo->originating(c('originating')); # may have changed by a p.b.load
  18396. }
  18397. my $debrecipm = ca('debug_recipient_maps');
  18398. if (lookup2(0, $sender, ca('debug_sender_maps')) ||
  18399. @$debrecipm && grep(lookup2(0, $_->recip_addr, $debrecipm),
  18400. @{$msginfo->per_recip_data})) {
  18401. debug_oneshot(1);
  18402. }
  18403. # check_mail() expects open file on $fh, need not be rewound
  18404. Amavis::check_mail_begin_task();
  18405. ($smtp_resp, $exit_code, $preserve_evidence) = &$check_mail($msginfo,0);
  18406. my $fh = $msginfo->mail_text; my $tempdir = $msginfo->mail_tempdir;
  18407. $fh->close or die "Error closing temp file: $!" if $fh;
  18408. undef $fh; $msginfo->mail_text(undef);
  18409. $msginfo->mail_text_str(undef); $msginfo->body_start_pos(undef);
  18410. my $errn = $tempdir eq '' ? ENOENT : (stat($tempdir) ? 0 : 0+$!);
  18411. if ($tempdir eq '' || $errn == ENOENT) {
  18412. # do nothing
  18413. } elsif ($msginfo->client_delete) {
  18414. do_log(4, "AM.PDP: deletion of %s is client's responsibility", $tempdir);
  18415. } elsif ($preserve_evidence) {
  18416. do_log(-1,"AM.PDP: tempdir is to be PRESERVED: %s", $tempdir);
  18417. } else {
  18418. my $fname = $msginfo->mail_text_fn;
  18419. do_log(4, "AM.PDP: tempdir and file being removed: %s, %s",
  18420. $tempdir,$fname);
  18421. unlink($fname) or die "Can't remove file $fname: $!" if $fname ne '';
  18422. # must step out of the directory which is about to be deleted,
  18423. # otherwise rmdir can fail (e.g. on Solaris)
  18424. chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
  18425. rmdir_recursively($tempdir);
  18426. }
  18427. }
  18428. # amavisd -> amavis-helper protocol response consists of any number of
  18429. # the following lines, the response is terminated by an empty line:
  18430. # version_server=2
  18431. # log_id=xxx
  18432. # delrcpt=<recipient>
  18433. # addrcpt=<recipient>
  18434. # delheader=hdridx hdr_head
  18435. # chgheader=hdridx hdr_head hdr_body
  18436. # insheader=hdridx hdr_head hdr_body
  18437. # addheader=hdr_head hdr_body
  18438. # replacebody=new_body (not implemented)
  18439. # quarantine=reason (currently never used, supposed to call
  18440. # smfi_quarantine, placing message on hold)
  18441. # return_value=continue|reject|discard|accept|tempfail
  18442. # setreply=rcode xcode message
  18443. # exit_code=n
  18444. my(@response); my($rcpt_deletes,$rcpt_count)=(0,0);
  18445. push(@response, proto_encode('version_server', '2'));
  18446. push(@response, proto_encode('log_id', $msginfo->log_id));
  18447. for my $r (@{$msginfo->per_recip_data}) {
  18448. $rcpt_count++;
  18449. $rcpt_deletes++ if $r->recip_done;
  18450. }
  18451. local($1,$2,$3);
  18452. if ($smtp_resp=~/^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
  18453. { push(@response, proto_encode('setreply', $1,$2,$3)) }
  18454. if ( $exit_code == EX_TEMPFAIL) {
  18455. push(@response, proto_encode('return_value','tempfail'));
  18456. } elsif ($exit_code == EX_NOUSER) { # reject the whole message
  18457. push(@response, proto_encode('return_value','reject'));
  18458. } elsif ($exit_code == EX_UNAVAILABLE) { # reject the whole message
  18459. push(@response, proto_encode('return_value','reject'));
  18460. } elsif ($exit_code == 99 || $rcpt_deletes >= $rcpt_count) {
  18461. $exit_code = 99; # let MTA discard the message, it was already handled here
  18462. push(@response, proto_encode('return_value','discard'));
  18463. } elsif (grep($_->delivery_method ne '', @{$msginfo->per_recip_data})) {
  18464. # explicit forwarding by us
  18465. die "Not all recips done, but explicit forwarding"; # just in case
  18466. } else { # EX_OK
  18467. for my $r (@{$msginfo->per_recip_data}) { # modified recipient addresses?
  18468. my $newaddr = $r->recip_final_addr;
  18469. if ($r->recip_done) { # delete
  18470. push(@response, proto_encode('delrcpt', $r->recip_addr_smtp))
  18471. if defined $r->recip_addr; # if in the original list, not always_bcc
  18472. } elsif ($newaddr ne $r->recip_addr) { # modify, e.g. adding extension
  18473. push(@response, proto_encode('delrcpt', $r->recip_addr_smtp))
  18474. if defined $r->recip_addr; # if in the original list, not always_bcc
  18475. push(@response, proto_encode('addrcpt',
  18476. qquote_rfc2821_local($newaddr)));
  18477. }
  18478. }
  18479. my $hdr_edits = $msginfo->header_edits;
  18480. if ($hdr_edits) { # any added or modified header fields?
  18481. local($1,$2); my($field_name,$edit,$field_body);
  18482. while ( ($field_name,$edit) = each %{$hdr_edits->{edit}} ) {
  18483. $field_body = $msginfo->get_header_field_body($field_name,0); # first
  18484. if (!defined($field_body)) {
  18485. # such header field does not exist or is not available, do nothing
  18486. } else { # edit the first occurrence
  18487. chomp($field_body);
  18488. my $orig_field_body = $field_body;
  18489. for my $e (@$edit) { # possibly multiple (iterative) edits
  18490. if (!defined($e)) { $field_body = undef; last } # delete existing
  18491. my($new_fbody,$verbatim) = &$e($field_name,$field_body);
  18492. if (!defined($new_fbody)) { $field_body = undef; last } # delete
  18493. my $curr_head = $verbatim ? ($field_name . ':' . $new_fbody)
  18494. : hdr($field_name, $new_fbody, 0);
  18495. chomp($curr_head); $curr_head .= "\n";
  18496. $curr_head =~ /^([^:]*?)[ \t]*:(.*)\z/s;
  18497. $field_body = $2; chomp($field_body); # carry to next iteration
  18498. }
  18499. if (!defined($field_body)) {
  18500. push(@response, proto_encode('delheader','1',$field_name));
  18501. } elsif ($field_body ne $orig_field_body) {
  18502. # sendmail inserts a space after a colon, remove ours
  18503. $field_body =~ s/^[ \t]//;
  18504. push(@response, proto_encode('chgheader','1',
  18505. $field_name,$field_body));
  18506. }
  18507. }
  18508. }
  18509. my $hdridx = c('prepend_header_fields_hdridx'); # milter insertion index
  18510. $hdridx = 0 if !defined($hdridx) || $hdridx < 0;
  18511. $hdridx = sprintf("%d",$hdridx); # convert to string
  18512. # prepend header fields one at a time, topmost field last
  18513. for my $hf (map(ref $hdr_edits->{$_} ? reverse @{$hdr_edits->{$_}} : (),
  18514. qw(addrcvd prepend)) ) {
  18515. if ($hf =~ /^([^:]*?)[ \t]*:[ \t]*(.*?)$/s)
  18516. { push(@response, proto_encode('insheader',$hdridx,$1,$2)) }
  18517. }
  18518. # append header fields
  18519. for my $hf (map(ref $hdr_edits->{$_} ? @{$hdr_edits->{$_}} : (),
  18520. qw(append)) ) {
  18521. if ($hf =~ /^([^:]*?)[ \t]*:[ \t]*(.*?)$/s)
  18522. { push(@response, proto_encode('addheader',$1,$2)) }
  18523. }
  18524. }
  18525. if ($old_amcl) { # milter via old amavis helper program
  18526. # warn if there is anything that should be done but MTA is not capable of
  18527. # (or a helper program cannot pass the request)
  18528. for (grep(/^(delrcpt|addrcpt)=/, @response))
  18529. { do_log(-1, "WARN: MTA can't do: %s", $_) }
  18530. if ($rcpt_deletes && $rcpt_count-$rcpt_deletes > 0) {
  18531. do_log(-1, "WARN: ACCEPT THE WHOLE MESSAGE, ".
  18532. "MTA-in can't do selective recips deletion");
  18533. }
  18534. }
  18535. push(@response, proto_encode('return_value','continue'));
  18536. }
  18537. push(@response, proto_encode('exit_code',sprintf("%d",$exit_code)));
  18538. ll(2) && do_log(2, "mail checking ended: %s", join("\n",@response));
  18539. dump_captured_log(1, c('enable_log_capture_dump'));
  18540. %current_policy_bank = %baseline_policy_bank; # restore bank settings
  18541. @response;
  18542. }
  18543. # just a proof-of-concept, experimental
  18544. #
  18545. sub postfix_policy($$) {
  18546. my($msginfo,$attr_ref) = @_;
  18547. my(@response);
  18548. if ($attr_ref->{'request'} ne 'smtpd_access_policy') {
  18549. die("unknown 'request' value: " . $attr_ref->{'request'});
  18550. } else {
  18551. @response = 'action=DUNNO';
  18552. }
  18553. @response;
  18554. }
  18555. sub dispatch_from_quarantine($$$) {
  18556. my($msginfo,$request_type,$feedback_type) = @_;
  18557. my $err;
  18558. eval {
  18559. # feed information to a msginfo object, possibly replacing it
  18560. $msginfo = msg_from_quarantine($msginfo,$request_type,$feedback_type);
  18561. mail_dispatch($msginfo,0,1); # re-send the original mail or report
  18562. 1;
  18563. } or do {
  18564. $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  18565. do_log(0, "WARN: dispatch_from_quarantine failed: %s",$err);
  18566. die $err if $err =~ /^timed out\b/; # resignal timeout
  18567. };
  18568. my(@response);
  18569. my $per_recip_data = $msginfo->per_recip_data;
  18570. if (!defined($per_recip_data) || !@$per_recip_data) {
  18571. push(@response, proto_encode('setreply','250','2.5.0',
  18572. "No recipients, nothing to do"));
  18573. } else {
  18574. for my $r (@$per_recip_data) {
  18575. local($1,$2,$3); my($smtp_s,$smtp_es,$msg);
  18576. my $resp = $r->recip_smtp_response;
  18577. if ($err ne '')
  18578. { ($smtp_s,$smtp_es,$msg) = ('450', '4.5.0', "ERROR: $err") }
  18579. elsif ($resp =~ /^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
  18580. { ($smtp_s,$smtp_es,$msg) = ($1,$2,$3) }
  18581. elsif ($resp =~ /^(([1-5])\d\d)(?: |\z)(.*)\z/s)
  18582. { ($smtp_s,$smtp_es,$msg) = ($1, "$2.0.0" ,$3) }
  18583. else
  18584. { ($smtp_s,$smtp_es,$msg) = ('450', '4.5.0', "Unexpected: $resp") }
  18585. push(@response, proto_encode('setreply',$smtp_s,$smtp_es,$msg));
  18586. }
  18587. }
  18588. @response;
  18589. }
  18590. 1;
  18591. __DATA__
  18592. #
  18593. package Amavis::In::SMTP;
  18594. use strict;
  18595. use re 'taint';
  18596. use warnings;
  18597. use warnings FATAL => qw(utf8 void);
  18598. no warnings 'uninitialized';
  18599. BEGIN {
  18600. require Exporter;
  18601. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  18602. $VERSION = '2.316';
  18603. @ISA = qw(Exporter);
  18604. import Amavis::Conf qw(:platform :confvars c cr ca);
  18605. import Amavis::Util qw(ll do_log do_log_safe untaint
  18606. dump_captured_log log_capture_enabled
  18607. am_id new_am_id snmp_counters_init
  18608. orcpt_encode xtext_decode debug_oneshot
  18609. waiting_for_client prolong_timer
  18610. switch_to_my_time switch_to_client_time
  18611. sanitize_str add_entropy
  18612. setting_by_given_contents_category);
  18613. import Amavis::Lookup qw(lookup lookup2);
  18614. import Amavis::Lookup::IP qw(lookup_ip_acl normalize_ip_addr);
  18615. import Amavis::Timing qw(section_time);
  18616. import Amavis::rfc2821_2822_Tools;
  18617. import Amavis::TempDir;
  18618. import Amavis::In::Message;
  18619. import Amavis::In::Connection;
  18620. }
  18621. use Errno qw(ENOENT EACCES EINTR EAGAIN);
  18622. use MIME::Base64;
  18623. use Time::HiRes ();
  18624. #use IO::Socket::SSL;
  18625. BEGIN { # due to dynamic loading runs only after config files have been read
  18626. my $tls_security_level = c('tls_security_level_in');
  18627. $tls_security_level = 0 if !defined($tls_security_level) ||
  18628. lc($tls_security_level) eq 'none';
  18629. if ($tls_security_level) {
  18630. defined $smtpd_tls_cert_file && $smtpd_tls_cert_file ne ''
  18631. or die '$tls_security_level is enabled '.
  18632. 'but $smtpd_tls_cert_file is not provided'."\n";
  18633. defined $smtpd_tls_key_file && $smtpd_tls_key_file ne ''
  18634. or die '$tls_security_level is enabled '.
  18635. 'but $smtpd_tls_key_file is not provided'."\n";
  18636. }
  18637. 1;
  18638. }
  18639. sub new($) {
  18640. my($class) = @_;
  18641. my $self = bless {}, $class;
  18642. undef $self->{sock}; # SMTP socket
  18643. $self->{proto} = undef; # SMTP / ((ESMTP / LMTP) (A | S | SA)? )
  18644. $self->{smtp_outbuf} = undef; # SMTP responses buffer for PIPELINING
  18645. undef $self->{pipelining}; # may we buffer responses?
  18646. undef $self->{session_closed_normally}; # closed properly with QUIT
  18647. $self->{within_data_transfer} = 0;
  18648. $self->{smtp_inpbuf} = ''; # SMTP input buffer
  18649. $self->{tempdir} = Amavis::TempDir->new; # TempDir object
  18650. $self;
  18651. }
  18652. sub DESTROY {
  18653. my $self = shift;
  18654. local($@,$!,$_); my $myactualpid = $$;
  18655. eval {
  18656. if (defined($my_pid) && $myactualpid != $my_pid) {
  18657. do_log(5,"Skip closing SMTP session in a clone [%s] (born as [%s])",
  18658. $myactualpid, $my_pid);
  18659. } elsif (ref($self->{sock}) && ! $self->{session_closed_normally}) {
  18660. my $msg = "421 4.3.2 Service shutting down, closing channel";
  18661. $msg .= ", during waiting for input from client" if waiting_for_client();
  18662. $msg .= ", sig: " .
  18663. join(',', keys %Amavisd::got_signals) if %Amavisd::got_signals;
  18664. $self->smtp_resp(1,$msg);
  18665. }
  18666. 1;
  18667. } or do {
  18668. my $eval_stat = $@ ne '' ? $@ : "errno=$!";
  18669. do_log_safe(1,"SMTP shutdown: %s", $eval_stat);
  18670. };
  18671. }
  18672. sub readline {
  18673. my($self, $timeout) = @_;
  18674. my($rout,$eout,$rin,$ein);
  18675. my $ifh = $self->{sock};
  18676. for (;;) {
  18677. local($1);
  18678. return $1 if $self->{smtp_inpbuf} =~ s/^(.*?\015\012)//s;
  18679. # if (defined $timeout) {
  18680. # if (!defined $rin) {
  18681. # $rin = $ein = ''; vec($rin, fileno $self->{sock}, 1) = 1; $ein = $rin;
  18682. # }
  18683. # my($nfound,$timeleft) =
  18684. # select($rout=$rin, undef, $eout=$ein, $timeout);
  18685. # defined $nfound && $nfound >= 0
  18686. # or die "Select failed: ".
  18687. # (!$self->{ssl_active} ? $! : $ifh->errstr.", $!");
  18688. # if (!$nfound) {
  18689. # do_log(2, 'smtp readline: timed out, %s s', $timeout);
  18690. # $timeout = undef; next; # carry on as usual
  18691. # }
  18692. # }
  18693. my $nbytes = $ifh->sysread($self->{smtp_inpbuf}, 16384,
  18694. length($self->{smtp_inpbuf}));
  18695. if ($nbytes) {
  18696. ll(5) && do_log(5, 'smtp readline: read %d bytes, new size: %d',
  18697. $nbytes, length($self->{smtp_inpbuf}));
  18698. } elsif (defined $nbytes) { # defined but zero
  18699. do_log(5, 'smtp readline: EOF');
  18700. $! = 0; # eof, no error
  18701. last;
  18702. } elsif ($! == EAGAIN || $! == EINTR) {
  18703. do_log(5, 'smtp readline: interrupted: %s',
  18704. !$self->{ssl_active} ? $! : $ifh->errstr.", $!");
  18705. # retry
  18706. } else {
  18707. do_log(5, 'smtp readline: error: %s',
  18708. !$self->{ssl_active} ? $! : $ifh->errstr.", $!");
  18709. last;
  18710. }
  18711. }
  18712. undef;
  18713. }
  18714. # Efficiently copy mail text from an SMTP socket to a file, converting
  18715. # CRLF to a local filesystem newlines \n, and handling dot-destuffing.
  18716. # Should be called just after the DATA command has been responded to,
  18717. # stops reading at a CRLF DOT CRLF or eof. Does not report stuffing errors.
  18718. #
  18719. # Our current statistics (Q4 2011) shows that 80 % of messages are below
  18720. # 30.000 bytes, and 90 % of messages are below 100.000 bytes in size.
  18721. #
  18722. sub copy_smtp_data {
  18723. my($self, $ofh, $out_str_ref, $size_limit) = @_;
  18724. my $ifh = $self->{sock};
  18725. my $buff = $self->{smtp_inpbuf}; # work with a local copy
  18726. $$out_str_ref = '' if ref $out_str_ref;
  18727. # assumes to be called right after a DATA<CR><LF>
  18728. my $eof = 0; my $at_the_beginning = 1;
  18729. my $size = 0; my $oversized = 0;
  18730. my($errno,$nreads,$j);
  18731. my $smtpd_t_o = c('smtpd_timeout');
  18732. while (!$eof) {
  18733. # alarm should apply per-line, but we are dealing with whole chunks here
  18734. alarm($smtpd_t_o);
  18735. $nreads = $ifh->sysread($buff, 65536, length($buff));
  18736. if ($nreads) {
  18737. ll(5) && do_log(5, "smtp copy: read %d bytes into buffer, new size: %d",
  18738. $nreads, length($buff));
  18739. } elsif (defined $nreads) {
  18740. $eof = 1;
  18741. do_log(5, "smtp copy: EOF");
  18742. } else {
  18743. $eof = 1;
  18744. $errno = !$self->{ssl_active} ? $! : $ifh->errstr.", $!";
  18745. do_log(5, "smtp copy: error: %s", $errno);
  18746. }
  18747. if ($at_the_beginning && substr($buff,0,3) eq ".\015\012") {
  18748. # a preceding \015\012 is implied, although no longer in the buffer
  18749. substr($buff,0,3) = '';
  18750. $self->{within_data_transfer} = 0;
  18751. last;
  18752. } elsif ( ($j=index($buff,"\015\012.\015\012")) >= 0 ) { # last chunk
  18753. my $carry = substr($buff,$j+5); # often empty
  18754. substr($buff,$j+2) = ''; # ditch the dot and the rest
  18755. $size += length($buff);
  18756. if (!$oversized) {
  18757. $buff =~ s/\015\012\.?/\n/gs;
  18758. # the last chunk is allowed to overshoot the 'small mail' limit
  18759. $$out_str_ref .= $buff if $out_str_ref;
  18760. if ($ofh) {
  18761. my $nwrites;
  18762. for (my $ofs = 0; $ofs < length($buff); $ofs += $nwrites) {
  18763. $nwrites = syswrite($ofh, $buff, length($buff)-$ofs, $ofs);
  18764. defined $nwrites or die "Error writing to mail file: $!";
  18765. }
  18766. }
  18767. if ($size_limit && $size > $size_limit) {
  18768. do_log(1,"Message size exceeded %d B", $size_limit);
  18769. $oversized = 1;
  18770. }
  18771. }
  18772. $buff = $carry;
  18773. $self->{within_data_transfer} = 0;
  18774. last;
  18775. }
  18776. my $carry = '';
  18777. if ($eof) {
  18778. # flush whatever is in the buffer, no more data coming
  18779. } elsif ($at_the_beginning &&
  18780. ($buff eq ".\015" || $buff eq '.' || $buff eq '')) {
  18781. $carry = $buff; $buff = '';
  18782. } elsif (substr($buff,-4,4) eq "\015\012.\015") {
  18783. substr($buff,-4,4) = ''; $carry = "\015\012.\015";
  18784. } elsif (substr($buff,-3,3) eq "\015\012.") {
  18785. substr($buff,-3,3) = ''; $carry = "\015\012.";
  18786. } elsif (substr($buff,-2,2) eq "\015\012") {
  18787. substr($buff,-2,2) = ''; $carry = "\015\012";
  18788. } elsif (substr($buff,-1,1) eq "\015") {
  18789. substr($buff,-1,1) = ''; $carry = "\015";
  18790. }
  18791. if ($buff ne '') {
  18792. $at_the_beginning = 0;
  18793. # message size is defined in RFC 1870, includes CRLF but no stuffed dots
  18794. # NOTE: we overshoot here by the number of stuffed dots, for performance;
  18795. # the message size will be finely adjusted in get_body_digest()
  18796. $size += length($buff);
  18797. if (!$oversized) {
  18798. # The RFC 5321 is quite clear, leading "." characters in
  18799. # SMTP are stripped regardless of the following character.
  18800. # Some MTAs only trim "." when the next character is also
  18801. # a ".", but this violates the RFC.
  18802. $buff =~ s/\015\012\.?/\n/gs; # quite fast, but still a bottleneck
  18803. if (!$out_str_ref) {
  18804. # not writing to memory
  18805. } elsif (length($$out_str_ref) < 100*1024) { # 100 KiB 'small mail'
  18806. $$out_str_ref .= $buff;
  18807. } else { # large mail, hand over writing to a file
  18808. # my $nwrites;
  18809. # for (my $ofs = 0; $ofs < length($$out_str_ref); $ofs += $nwrites) {
  18810. # $nwrites = syswrite($ofh, $$out_str_ref,
  18811. # length($$out_str_ref)-$ofs, $ofs);
  18812. # defined $nwrites or die "Error writing to mail file: $!";
  18813. # }
  18814. $$out_str_ref = '';
  18815. $out_str_ref = undef;
  18816. }
  18817. if ($ofh) {
  18818. my $nwrites;
  18819. for (my $ofs = 0; $ofs < length($buff); $ofs += $nwrites) {
  18820. $nwrites = syswrite($ofh, $buff, length($buff)-$ofs, $ofs);
  18821. defined $nwrites or die "Error writing to mail file: $!";
  18822. }
  18823. }
  18824. if ($size_limit && $size > $size_limit) {
  18825. do_log(1,"Message size exceeded %d B, ".
  18826. "skipping further input", $size_limit);
  18827. my $trunc_str = "\n***TRUNCATED***\n";
  18828. $$out_str_ref .= $trunc_str if $out_str_ref;
  18829. if ($ofh) {
  18830. my $nwrites = syswrite($ofh, $trunc_str);
  18831. defined $nwrites or die "Error writing to mail file: $!";
  18832. }
  18833. $oversized = 1;
  18834. }
  18835. }
  18836. }
  18837. $buff = $carry;
  18838. }
  18839. do_log(5, "smtp copy: %d bytes still buffered at end", length($buff));
  18840. $self->{smtp_inpbuf} = $buff; # put a local copy back into object
  18841. !$self->{within_data_transfer} or die "Connection broken during DATA: ".
  18842. (!$self->{ssl_active} ? $! : $ifh->errstr.", $!");
  18843. # return a message size and an indication of exceeded size limit
  18844. ($size,$oversized);
  18845. }
  18846. sub preserve_evidence { # preserve temporary files etc in case of trouble
  18847. my $self = shift;
  18848. !$self->{tempdir} ? undef : $self->{tempdir}->preserve(@_);
  18849. }
  18850. sub authenticate($$$) {
  18851. my($state,$auth_mech,$auth_resp) = @_;
  18852. my($result,$newchallenge);
  18853. if ($auth_mech eq 'ANONYMOUS') { # RFC 2245
  18854. $result = [$auth_resp,undef];
  18855. } elsif ($auth_mech eq 'PLAIN') { # RFC 2595, "user\0authname\0pass"
  18856. if (!defined($auth_resp)) { $newchallenge = '' }
  18857. else { $result = [ (split(/\000/,$auth_resp,-1))[0,2] ] }
  18858. } elsif ($auth_mech eq 'LOGIN' && !defined $state) {
  18859. $newchallenge = 'Username:'; $state = [];
  18860. } elsif ($auth_mech eq 'LOGIN' && @$state==0) {
  18861. push(@$state, $auth_resp); $newchallenge = 'Password:';
  18862. } elsif ($auth_mech eq 'LOGIN' && @$state==1) {
  18863. push(@$state, $auth_resp); $result = $state;
  18864. } # CRAM-MD5:RFC 2195, DIGEST-MD5:RFC 2831
  18865. ($state,$result,$newchallenge);
  18866. }
  18867. # Accept an SMTP or LMTP connect (which can do any number of transactions)
  18868. # and call content checking for each message received
  18869. #
  18870. sub process_smtp_request($$$$) {
  18871. my($self, $sock, $lmtp, $conn, $check_mail) = @_;
  18872. # $sock: connected socket from Net::Server
  18873. # $lmtp: greet as an LMTP server instead of (E)SMTP
  18874. # $conn: information about client connection
  18875. # $check_mail: subroutine ref to be called with file handle
  18876. my($msginfo,$authenticated,$auth_user,$auth_pass);
  18877. $self->{sock} = $sock;
  18878. $self->{pipelining} = 0; # may we buffer responses?
  18879. $self->{smtp_outbuf} = []; # SMTP responses buffer for PIPELINING
  18880. $self->{session_closed_normally} = 0; # closed properly with QUIT?
  18881. $self->{ssl_active} = 0; # session upgraded to SSL
  18882. my $tls_security_level = c('tls_security_level_in');
  18883. $tls_security_level = 0 if !defined($tls_security_level) ||
  18884. lc($tls_security_level) eq 'none';
  18885. my $myheloname;
  18886. # $myheloname = c('myhostname');
  18887. # $myheloname = 'localhost';
  18888. # $myheloname = '[127.0.0.1]';
  18889. for ($conn->socket_ip) { # just aliasing, not a loop
  18890. $myheloname = defined($_) && $_ ne '' ? "[$_]" : '[localhost]';
  18891. }
  18892. new_am_id(undef, $Amavis::child_invocation_count, undef);
  18893. my $initial_am_id = 1; my($sender_unq,$sender_quo,@recips,$got_rcpt);
  18894. my $max_recip_size_limit; # maximum of per-recipient message size limits
  18895. my($terminating,$aborting,$eof,$voluntary_exit); my(%xforward_args);
  18896. my $seq = 0;
  18897. my(%baseline_policy_bank) = %current_policy_bank;
  18898. $conn->appl_proto($self->{proto} = $lmtp ? 'LMTP' : 'SMTP');
  18899. # system-wide message size limit, if any
  18900. my $final_oversized_destiny = setting_by_given_contents_category(
  18901. CC_OVERSIZED, cr('final_destiny_by_ccat'));
  18902. my $message_size_limit = c('smtpd_message_size_limit');
  18903. if ($enforce_smtpd_message_size_limit_64kb_min &&
  18904. $message_size_limit && $message_size_limit < 65536)
  18905. { $message_size_limit = 65536 } # RFC 5321 requires at least 64k
  18906. my $smtpd_greeting_banner_tmp = c('smtpd_greeting_banner');
  18907. $smtpd_greeting_banner_tmp =~
  18908. s{ \$ (?: \{ ([^\}]+) \} |
  18909. ([a-zA-Z](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?\b) ) }
  18910. { { 'helo-name' => $myheloname,
  18911. 'myhostname' => c('myhostname'),
  18912. 'version' => $myversion,
  18913. 'version-id' => $myversion_id,
  18914. 'version-date' => $myversion_date,
  18915. 'product' => $myproduct_name,
  18916. 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
  18917. }egx;
  18918. $self->smtp_resp(1,"220 $smtpd_greeting_banner_tmp");
  18919. section_time('SMTP greeting');
  18920. # each call to smtp_resp starts a $smtpd_timeout timeout to tame slow clients
  18921. $0 = sprintf("%s (ch%d-idle)",
  18922. c('myprogram_name'), $Amavis::child_invocation_count);
  18923. Amavis::Timing::go_idle(4);
  18924. local($_); local($/) = "\012"; # input line terminator set to LF
  18925. for ($! = 0; defined($_ = $self->readline); $! = 0) {
  18926. $0 = sprintf("%s (ch%d-%s)",
  18927. c('myprogram_name'), $Amavis::child_invocation_count, am_id());
  18928. Amavis::Timing::go_busy(5);
  18929. # the ball is now in our courtyard, (re)start our timer;
  18930. # each of our smtp responses will switch back to a $smtpd_timeout timer
  18931. { # a block is used as a 'switch' statement - 'last' will exit from it
  18932. my $cmd = $_;
  18933. ll(4) && do_log(4, '%s< %s', $self->{proto},$cmd);
  18934. if (!/^ [ \t]* ( [A-Za-z] [A-Za-z0-9]* ) (?: [ \t]+ (.*?) )? [ \t]*
  18935. \015 \012 \z /xs) {
  18936. $self->smtp_resp(1,"500 5.5.2 Error: bad syntax", 1, $cmd); last;
  18937. };
  18938. $_ = uc($1); my $args = $2;
  18939. switch_to_my_time("rx SMTP $_");
  18940. # (causes holdups in Postfix, it doesn't retry immediately; better set max_use)
  18941. # $Amavis::child_task_count >= $max_requests # exceeded max_requests
  18942. # && /^(?:HELO|EHLO|LHLO|DATA|NOOP|QUIT|VRFY|EXPN|TURN)\z/ && do {
  18943. # # pipelining checkpoints;
  18944. # # in case of multiple-transaction protocols (e.g. SMTP, LMTP)
  18945. # # we do not like to keep running indefinitely at the MTA's mercy
  18946. # my $msg = "Closing transmission channel ".
  18947. # "after $Amavis::child_task_count transactions, $_";
  18948. # do_log(2,"%s",$msg); $self->smtp_resp(1,"421 4.3.0 ".$msg); #flush!
  18949. # $terminating=1; last;
  18950. # };
  18951. $tls_security_level && lc($tls_security_level) ne 'may' &&
  18952. !$self->{ssl_active} && !/^(?:NOOP|EHLO|STARTTLS|QUIT)\z/ && do {
  18953. $self->smtp_resp(1,"530 5.7.0 Must issue a STARTTLS command first",
  18954. 1,$cmd);
  18955. last;
  18956. };
  18957. # lc($tls_security_level) eq 'verify' && !/^QUIT\z/ && do {
  18958. # $self->smtp_resp(1,"554 5.7.0 Command refused due to lack of security",
  18959. # 1,$cmd);
  18960. # last;
  18961. # };
  18962. /^NOOP\z/ && do { $self->smtp_resp(1,"250 2.0.0 Ok $_"); last }; #flush!
  18963. /^QUIT\z/ && do {
  18964. if ($args ne '') {
  18965. $self->smtp_resp(1,"501 5.5.4 Error: QUIT does not accept arguments",
  18966. 1,$cmd); #flush
  18967. } else {
  18968. my $smtpd_quit_banner_tmp = c('smtpd_quit_banner');
  18969. $smtpd_quit_banner_tmp =~
  18970. s{ \$ (?: \{ ([^\}]+) \} |
  18971. ([a-zA-Z](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?\b) ) }
  18972. { { 'helo-name' => $myheloname,
  18973. 'myhostname' => c('myhostname'),
  18974. 'version' => $myversion,
  18975. 'version-id' => $myversion_id,
  18976. 'version-date' => $myversion_date,
  18977. 'product' => $myproduct_name,
  18978. 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
  18979. }egx;
  18980. $self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp"); #flush!
  18981. $terminating = 1;
  18982. }
  18983. last;
  18984. };
  18985. /^(?:RSET|HELO|EHLO|LHLO|STARTTLS)\z/ && do {
  18986. # explicit or implicit session reset
  18987. $sender_unq = $sender_quo = undef; @recips = (); $got_rcpt = 0;
  18988. undef $max_recip_size_limit; undef $msginfo; # forget previous
  18989. %current_policy_bank = %baseline_policy_bank; # restore bank settings
  18990. %xforward_args = ();
  18991. if (/^(?:RSET|STARTTLS)\z/ && $args ne '') {
  18992. $self->smtp_resp(1,"501 5.5.4 Error: $_ does not accept arguments",
  18993. 1,$cmd);
  18994. } elsif (/^RSET\z/) {
  18995. $self->smtp_resp(0,"250 2.0.0 Ok $_");
  18996. } elsif (/^STARTTLS\z/) { # RFC 3207 (ex RFC 2487)
  18997. if ($self->{ssl_active}) {
  18998. $self->smtp_resp(1,"554 5.5.1 Error: TLS already active");
  18999. } elsif (!$tls_security_level) {
  19000. $self->smtp_resp(1,"502 5.5.1 Error: command not available");
  19001. } else {
  19002. $self->smtp_resp(1,"220 2.0.0 Ready to start TLS"); #flush!
  19003. IO::Socket::SSL->start_SSL($sock,
  19004. SSL_server => 1, SSL_session_cache => 2,
  19005. SSL_error_trap => sub { my($sock,$msg)=@_;
  19006. do_log(-2,"Error on socket: %s",$msg) },
  19007. SSL_passwd_cb => sub { 'example' },
  19008. SSL_key_file => $smtpd_tls_key_file,
  19009. SSL_cert_file => $smtpd_tls_cert_file,
  19010. ) or die "Error upgrading socket to SSL: ".
  19011. IO::Socket::SSL::errstr();
  19012. if ($self->{smtp_inpbuf} ne '') {
  19013. do_log(-1, "STARTTLS pipelining violation attempt, sanitized");
  19014. $self->{smtp_inpbuf} = ''; # ditch any buffered data
  19015. }
  19016. $self->{ssl_active} = 1;
  19017. ll(3) && do_log(3,"smtpd TLS cipher: %s", $sock->get_cipher);
  19018. section_time('SMTP starttls');
  19019. }
  19020. } elsif (/^HELO\z/) {
  19021. $self->{pipelining} = 0; $lmtp = 0;
  19022. $conn->appl_proto($self->{proto} = 'SMTP');
  19023. $self->smtp_resp(0,"250 $myheloname");
  19024. $conn->smtp_helo($args); section_time('SMTP HELO');
  19025. } elsif (/^(?:EHLO|LHLO)\z/) {
  19026. $self->{pipelining} = 1; $lmtp = /^LHLO\z/ ? 1 : 0;
  19027. $conn->appl_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP');
  19028. my(@ehlo_keywords) = (
  19029. 'VRFY',
  19030. 'PIPELINING', # RFC 2920
  19031. !defined($message_size_limit) ? 'SIZE' # RFC 1870
  19032. : sprintf('SIZE %d',$message_size_limit),
  19033. 'ENHANCEDSTATUSCODES', # RFC 2034, RFC 3463
  19034. '8BITMIME', # RFC 1652
  19035. 'DSN', # RFC 3461
  19036. !$tls_security_level || $self->{ssl_active} ? ()
  19037. : 'STARTTLS', # RFC 3207 (ex RFC 2487)
  19038. !@{ca('auth_mech_avail')} ? () # RFC 4954 (ex RFC 2554)
  19039. : join(' ','AUTH',@{ca('auth_mech_avail')}),
  19040. 'XFORWARD NAME ADDR PORT PROTO HELO IDENT SOURCE' );
  19041. my(%smtpd_discard_ehlo_keywords) =
  19042. map((uc($_),1), @{ca('smtpd_discard_ehlo_keywords')});
  19043. @ehlo_keywords =
  19044. grep(/^([A-Za-z0-9]+)/ &&
  19045. !$smtpd_discard_ehlo_keywords{uc($1)}, @ehlo_keywords);
  19046. $self->smtp_resp(1,"250 $myheloname\n" .
  19047. join("\n",@ehlo_keywords)); #flush!
  19048. $conn->smtp_helo($args); section_time("SMTP $_");
  19049. };
  19050. last;
  19051. };
  19052. /^XFORWARD\z/ && do { # Postfix extension
  19053. if (defined $sender_unq) {
  19054. $self->smtp_resp(1,"503 5.5.1 Error: XFORWARD not allowed ".
  19055. "within transaction",1,$cmd);
  19056. last;
  19057. }
  19058. my $bad;
  19059. for (split(' ',$args)) {
  19060. if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) {
  19061. $self->smtp_resp(1,"501 5.5.4 Syntax error in XFORWARD parameters",
  19062. 1, $cmd);
  19063. $bad = 1; last;
  19064. } else {
  19065. my($name,$val) = (uc($1), $2);
  19066. if ($name =~ /^(?:NAME|ADDR|PORT|PROTO|HELO|IDENT|SOURCE)\z/) {
  19067. $val = undef if uc($val) eq '[UNAVAILABLE]';
  19068. # Postfix since vers 2.3 (20060610) uses xtext-encoded (RFC 3461)
  19069. # strings in XCLIENT and XFORWARD attribute values, previous
  19070. # versions sent plain text with neutered special characters.
  19071. # The IDENT option is available since postfix 2.8.0 .
  19072. $val = xtext_decode($val) if defined $val &&
  19073. $val =~ /\+([0-9a-fA-F]{2})/;
  19074. $xforward_args{$name} = $val;
  19075. } else {
  19076. $self->smtp_resp(1,"501 5.5.4 XFORWARD command parameter ".
  19077. "error: $name=$val",1,$cmd);
  19078. $bad = 1; last;
  19079. }
  19080. }
  19081. }
  19082. $self->smtp_resp(1,"250 2.5.0 Ok $_") if !$bad;
  19083. last;
  19084. };
  19085. /^HELP\z/ && do {
  19086. $self->smtp_resp(0,"214 2.0.0 See $myproduct_name home page at:\n".
  19087. "http://www.ijs.si/software/amavisd/");
  19088. last;
  19089. };
  19090. /^AUTH\z/ && @{ca('auth_mech_avail')} && do { # RFC 4954 (ex RFC 2554)
  19091. if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) {
  19092. $self->smtp_resp(1,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd);
  19093. last;
  19094. }
  19095. my($auth_mech,$auth_resp) = (uc($1), $2);
  19096. if ($authenticated) {
  19097. $self->smtp_resp(1,"503 5.5.1 Error: session already authenticated",
  19098. 1,$cmd);
  19099. } elsif (defined $sender_unq) {
  19100. $self->smtp_resp(1,"503 5.5.1 Error: AUTH not allowed within ".
  19101. "transaction",1,$cmd);
  19102. } elsif (!grep(uc($_) eq $auth_mech, @{ca('auth_mech_avail')})) {
  19103. $self->smtp_resp(1,"504 5.5.4 Error: requested authentication ".
  19104. "mechanism not supported",1,$cmd);
  19105. } else {
  19106. my($state,$result,$challenge);
  19107. if ($auth_resp eq '=') { $auth_resp = '' } # zero length
  19108. elsif ($auth_resp eq '') { $auth_resp = undef }
  19109. for (;;) {
  19110. if ($auth_resp !~ m{^[A-Za-z0-9+/]*=*\z}) {
  19111. $self->smtp_resp(1,"501 5.5.2 Authentication failed: ".
  19112. "malformed authentication response",1,$cmd);
  19113. last;
  19114. } else {
  19115. $auth_resp = decode_base64($auth_resp) if $auth_resp ne '';
  19116. ($state,$result,$challenge) =
  19117. authenticate($state, $auth_mech, $auth_resp);
  19118. if (ref($result) eq 'ARRAY') {
  19119. $self->smtp_resp(0,"235 2.7.0 Authentication succeeded");
  19120. $authenticated = 1; ($auth_user,$auth_pass) = @$result;
  19121. do_log(2,"AUTH %s, user=%s", $auth_mech,$auth_user); #auth_resp
  19122. last;
  19123. } elsif (defined $result && !$result) {
  19124. $self->smtp_resp(0,"535 5.7.8 Authentication credentials ".
  19125. "invalid", 1, $cmd);
  19126. last;
  19127. }
  19128. }
  19129. # server challenge or ready prompt
  19130. $self->smtp_resp(1,"334 ".encode_base64($challenge,''));
  19131. $! = 0; $auth_resp = $self->readline;
  19132. defined $auth_resp or die "Error reading auth resp: ".
  19133. (!$self->{ssl_active} ? $! : $sock->errstr.", $!");
  19134. switch_to_my_time('rx AUTH challenge reply');
  19135. do_log(5, "%s< %s", $self->{proto},$auth_resp);
  19136. $auth_resp =~ s/\015?\012\z//;
  19137. if (length($auth_resp) > 12288) { # RFC 4954
  19138. $self->smtp_resp(1,"500 5.5.6 Authentication exchange ".
  19139. "line is too long");
  19140. last;
  19141. } elsif ($auth_resp eq '*') {
  19142. $self->smtp_resp(1,"501 5.7.1 Authentication aborted");
  19143. last;
  19144. }
  19145. }
  19146. }
  19147. last;
  19148. };
  19149. /^VRFY\z/ && do {
  19150. if ($args eq '') {
  19151. $self->smtp_resp(1,"501 5.5.2 Syntax: VRFY address", 1,$cmd); #flush!
  19152. } else { # RFC 2505
  19153. $self->smtp_resp(1,"252 2.0.0 Argument not checked", 0,$cmd); #flush!
  19154. }
  19155. last;
  19156. };
  19157. /^MAIL\z/ && do { # begin new SMTP transaction
  19158. if (defined $sender_unq) {
  19159. $self->smtp_resp(1,"503 5.5.1 Error: nested MAIL command", 1, $cmd);
  19160. last;
  19161. }
  19162. if (!$authenticated &&
  19163. c('auth_required_inp') && @{ca('auth_mech_avail')} ) {
  19164. $self->smtp_resp(1,"530 5.7.0 Authentication required", 1, $cmd);
  19165. last;
  19166. }
  19167. # begin SMTP transaction
  19168. my $now = Time::HiRes::time;
  19169. if (!$seq) { # the first connect
  19170. section_time('SMTP pre-MAIL');
  19171. } else { # establish a new time reference for each transaction
  19172. Amavis::Timing::init(); snmp_counters_init();
  19173. }
  19174. $seq++;
  19175. new_am_id(undef, $Amavis::child_invocation_count, $seq)
  19176. if !$initial_am_id;
  19177. $initial_am_id = 0;
  19178. # enter 'in transaction' state
  19179. $Amavis::zmq_obj->register_proc(1,1,'m',am_id()) if $Amavis::zmq_obj;
  19180. $Amavis::snmp_db->register_proc(1,1,'m',am_id()) if $Amavis::snmp_db;
  19181. Amavis::check_mail_begin_task();
  19182. $self->{tempdir}->prepare_dir;
  19183. $self->{tempdir}->prepare_file;
  19184. $msginfo = Amavis::In::Message->new;
  19185. $msginfo->rx_time($now);
  19186. $msginfo->log_id(am_id());
  19187. $msginfo->conn_obj($conn);
  19188. my $cl_ip = normalize_ip_addr($xforward_args{'ADDR'});
  19189. my $cl_port = $xforward_args{'PORT'};
  19190. my $cl_src = $xforward_args{'SOURCE'}; # local_header_rewrite_clients
  19191. $cl_port = undef if $cl_port !~ /^\d{1,9}\z/ || $cl_port > 65535;
  19192. my($cl_ip_mynets, $policy_name_requested);
  19193. { my $cl_ip_tmp = $cl_ip;
  19194. # treat unknown client IP address as 0.0.0.0,
  19195. # from "This" Network, RFC 1700
  19196. $cl_ip_tmp = '0.0.0.0' if !defined($cl_ip) || $cl_ip eq '';
  19197. my(@cp) = @{ca('client_ipaddr_policy')};
  19198. do_log(-1,"\@client_ipaddr_policy must contain pairs, ".
  19199. "number of elements is not even") if @cp % 2 != 0;
  19200. while (@cp) {
  19201. my $lookup_table = shift(@cp); my $policy_name = shift(@cp);
  19202. if (lookup_ip_acl($cl_ip_tmp, $lookup_table)) {
  19203. if (defined $policy_name && $policy_name ne '') {
  19204. $policy_name_requested = $policy_name;
  19205. $cl_ip_mynets = 1 if $policy_name eq 'MYNETS'; # compatibility
  19206. }
  19207. last;
  19208. }
  19209. }
  19210. }
  19211. if (($cl_ip_mynets?1:0) > ($msginfo->originating?1:0)) {
  19212. $current_policy_bank{'originating'} = $cl_ip_mynets; # compatibility
  19213. }
  19214. if (defined $policy_name_requested &&
  19215. defined $policy_bank{$policy_name_requested}) {
  19216. Amavis::load_policy_bank($policy_name_requested,$msginfo);
  19217. }
  19218. $msginfo->originating(c('originating'));
  19219. $msginfo->client_addr($cl_ip); # ADDR
  19220. $msginfo->client_port($cl_port); # PORT
  19221. $msginfo->client_source($cl_src); # SOURCE
  19222. $msginfo->client_name($xforward_args{'NAME'});
  19223. $msginfo->client_helo($xforward_args{'HELO'});
  19224. $msginfo->client_proto($xforward_args{'PROTO'});
  19225. $msginfo->queue_id($xforward_args{'IDENT'});
  19226. # $msginfo->body_type('7BIT'); # presumed, unless explicitly declared
  19227. %xforward_args = (); # reset values for the next transaction
  19228. if ($self->{ssl_active}) {
  19229. $msginfo->tls_cipher($sock->get_cipher);
  19230. $conn->appl_proto($self->{proto}.'S') # RFC 3848
  19231. if $self->{proto} =~ /^(LMTP|ESMTP)\z/i;
  19232. }
  19233. my $submitter;
  19234. if ($authenticated) {
  19235. $msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass);
  19236. $conn->appl_proto($self->{proto}.'A') # RFC 3848
  19237. if $self->{proto} =~ /^(LMTP|ESMTP)S?\z/i;
  19238. } elsif (c('auth_reauthenticate_forwarded') &&
  19239. c('amavis_auth_user') ne '') {
  19240. $msginfo->auth_user(c('amavis_auth_user'));
  19241. $msginfo->auth_pass(c('amavis_auth_pass'));
  19242. # $submitter = quote_rfc2821_local(c('mailfrom_notify_recip'));
  19243. # $submitter = expand_variables($submitter) if defined $submitter;
  19244. }
  19245. local($1,$2);
  19246. if ($args !~ /^FROM: [ \t]*
  19247. ( < (?: " (?: \\. | [^\\"] ){0,999} " | [^"\@ \t] )*
  19248. (?: \@ (?: \[ (?: \\. | [^\]\\] ){0,999} \] |
  19249. [^\[\]\\> \t] )* )? > )
  19250. (?: [ \t]+ (.+) )? \z/isx ) {
  19251. $self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM:<address>",1,$cmd);
  19252. last;
  19253. }
  19254. my($addr,$opt) = ($1,$2); my($size,$dsn_ret,$dsn_envid);
  19255. my $msg ; my $msg_nopenalize = 0;
  19256. for (split(' ',$opt)) {
  19257. if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
  19258. ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP
  19259. $msg = "501 5.5.4 Syntax error in MAIL FROM parameters";
  19260. } else {
  19261. my($name,$val) = (uc($1),$2);
  19262. if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) { # RFC 1870
  19263. if (!defined($size)) { $size = untaint($val) }
  19264. else { $msg = "501 5.5.4 Syntax error in MAIL parameter: $name" }
  19265. } elsif ($name eq 'BODY' && $val=~/^(?:7BIT|8BITMIME)\z/i) {
  19266. $msginfo->body_type(uc($val));
  19267. } elsif ($name eq 'RET') { # RFC 3461
  19268. if (!defined($dsn_ret)) { $dsn_ret = uc($val) }
  19269. else { $msg = "501 5.5.4 Syntax error in MAIL parameter: $name" }
  19270. } elsif ($name eq 'ENVID') { # RFC 3461, value encoded as xtext
  19271. if (!defined($dsn_envid)) { $dsn_envid = $val }
  19272. else { $msg = "501 5.5.4 Syntax error in MAIL parameter: $name" }
  19273. } elsif ($name eq 'AUTH') { # RFC 4954 (ex RFC 2554)
  19274. my $s = xtext_decode($val); # encoded as xtext: RFC 3461
  19275. do_log(5, "MAIL command, %s, submitter: %s", $authenticated,$s);
  19276. if (defined $submitter) { # authorized identity
  19277. $msg = "504 5.5.4 MAIL command duplicate param.: $name=$val";
  19278. } elsif (!@{ca('auth_mech_avail')}) {
  19279. do_log(3, "MAIL command parameter AUTH supplied, but ".
  19280. "authentication capability not announced, ignored");
  19281. $submitter = '<>';
  19282. # mercifully ignore invalid parameter for the benefit of
  19283. # running amavisd as a Postfix pre-queue smtp proxy filter
  19284. # $msg = "503 5.7.4 Error: authentication disabled";
  19285. } else {
  19286. $submitter = $s;
  19287. }
  19288. } else {
  19289. $msg = "504 5.5.4 MAIL command parameter error: $name=$val";
  19290. }
  19291. }
  19292. last if defined $msg;
  19293. }
  19294. if (!defined($msg) && defined $dsn_ret && $dsn_ret!~/^(FULL|HDRS)\z/) {
  19295. $msg = "501 5.5.4 Syntax error in MAIL parameter RET: $dsn_ret";
  19296. }
  19297. if (!defined($msg) && defined($size) &&
  19298. $message_size_limit && $size > $message_size_limit &&
  19299. $final_oversized_destiny == D_REJECT) {
  19300. $msg = "552 5.3.4 Declared message size ($size B) ".
  19301. "exceeds fixed size limit";
  19302. $msg_nopenalize = 1;
  19303. do_log(0, "%s REJECT 'MAIL FROM': %s", $self->{proto},$msg);
  19304. }
  19305. if (!defined $msg) {
  19306. $sender_quo = $addr; $sender_unq = unquote_rfc2821_local($addr);
  19307. $addr = $1 if $addr =~ /^<(.*)>\z/s;
  19308. my $requoted = qquote_rfc2821_local($sender_unq);
  19309. do_log(2, "address modified (sender): %s -> %s",
  19310. $sender_quo, $requoted) if $requoted ne $sender_quo;
  19311. if (defined $policy_bank{'MYUSERS'} &&
  19312. $sender_unq ne '' && $msginfo->originating &&
  19313. lookup2(0,$sender_unq, ca('local_domains_maps'))) {
  19314. Amavis::load_policy_bank('MYUSERS',$msginfo);
  19315. $msginfo->originating(c('originating')); # may have changed
  19316. }
  19317. debug_oneshot(
  19318. lookup2(0,$sender_unq, ca('debug_sender_maps')) ? 1 : 0,
  19319. $self->{proto} . "< $cmd");
  19320. # $submitter = $addr if !defined($submitter); # RFC 4954/RFC 2554: MAY
  19321. $submitter = '<>' if !defined($msginfo->auth_user);
  19322. $msginfo->auth_submitter($submitter);
  19323. if (defined $size) {
  19324. do_log(5, "mesage size set to a declared size %s", $size);
  19325. $msginfo->msg_size(0+$size);
  19326. }
  19327. if (defined $dsn_ret || defined $dsn_envid) {
  19328. # keep ENVID in xtext-encoded form
  19329. $msginfo->dsn_ret($dsn_ret) if defined $dsn_ret;
  19330. $msginfo->dsn_envid($dsn_envid) if defined $dsn_envid;
  19331. }
  19332. $msg = "250 2.1.0 Sender $sender_quo OK";
  19333. };
  19334. $self->smtp_resp(0,$msg, !$msg_nopenalize && $msg=~/^5/ ? 1 : 0, $cmd);
  19335. last;
  19336. };
  19337. /^RCPT\z/ && do {
  19338. if (!defined($sender_unq)) {
  19339. $self->smtp_resp(1,"503 5.5.1 Need MAIL command before RCPT",1,$cmd);
  19340. @recips = (); $got_rcpt = 0;
  19341. last;
  19342. }
  19343. $got_rcpt++;
  19344. local($1,$2);
  19345. if ($args !~ /^TO: [ \t]*
  19346. ( < (?: " (?: \\. | [^\\"] ){0,999} " | [^"\@ \t] )*
  19347. (?: \@ (?: \[ (?: \\. | [^\]\\] ){0,999} \] |
  19348. [^\[\]\\> \t] )* )? > )
  19349. (?: [ \t]+ (.+) )? \z/isx ) {
  19350. $self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO:<address>",1,$cmd);
  19351. last;
  19352. }
  19353. my($addr,$opt) = ($1,$2); my($notify,$orcpt);
  19354. my $msg; my $msg_nopenalize = 0;
  19355. for (split(' ',$opt)) {
  19356. if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
  19357. ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP
  19358. $msg = "501 5.5.4 Syntax error in RCPT parameters";
  19359. } else {
  19360. my($name,$val) = (uc($1),$2);
  19361. if ($name eq 'NOTIFY') { # RFC 3461
  19362. if (!defined($notify)) { $notify = $val }
  19363. else { $msg = "501 5.5.4 Syntax error in RCPT parameter $name" }
  19364. } elsif ($name eq 'ORCPT') { # RFC 3461, value encoded as xtext
  19365. if (!defined($orcpt)) { $orcpt = $val }
  19366. else { $msg = "501 5.5.4 Syntax error in RCPT parameter $name" }
  19367. } else {
  19368. $msg = "555 5.5.4 RCPT command parameter unrecognized: $name";
  19369. # 504 5.5.4 RCPT command parameter not implemented:
  19370. # 504 5.5.4 RCPT command parameter error:
  19371. # 555 5.5.4 RCPT command parameter unrecognized:
  19372. }
  19373. }
  19374. last if defined $msg;
  19375. }
  19376. my $addr_unq = unquote_rfc2821_local($addr);
  19377. my $requoted = qquote_rfc2821_local($addr_unq);
  19378. if ($requoted ne $addr) { # check for valid canonical quoting
  19379. do_log(0, "WARN: address modified (recip): %s -> %s",
  19380. $addr, $requoted);
  19381. # RFC 3461: If no ORCPT parameter was present in the RCPT command
  19382. # when the message was received, an ORCPT parameter MAY be added
  19383. # to the RCPT command when the message is relayed. If an ORCPT
  19384. # parameter is added by the relaying MTA, it MUST contain the
  19385. # recipient address from the RCPT command used when the message
  19386. # was received by that MTA
  19387. $orcpt = orcpt_encode($addr) if !defined $orcpt;
  19388. }
  19389. if (lookup2(0,$addr_unq, ca('debug_recipient_maps'))) {
  19390. debug_oneshot(1, $self->{proto} . "< $cmd");
  19391. }
  19392. my $recip_size_limit; my $mslm = ca('message_size_limit_maps');
  19393. $recip_size_limit = lookup2(0,$addr_unq,$mslm) if @$mslm;
  19394. if ($enforce_smtpd_message_size_limit_64kb_min &&
  19395. $recip_size_limit && $recip_size_limit < 65536)
  19396. { $recip_size_limit = 65536 } # RFC 5321 requires at least 64k
  19397. if ($recip_size_limit > $max_recip_size_limit)
  19398. { $max_recip_size_limit = $recip_size_limit }
  19399. my $mail_size = $msginfo->msg_size;
  19400. if (!defined($msg) && defined($notify)) {
  19401. my(@v) = split(/,/,uc($notify),-1);
  19402. if (grep(!/^(?:NEVER|SUCCESS|FAILURE|DELAY)\z/, @v)) {
  19403. $msg = "501 5.5.4 Error in RCPT parameter NOTIFY, ".
  19404. "illegal value: $notify";
  19405. } elsif (grep($_ eq 'NEVER', @v) && grep($_ ne 'NEVER', @v)) {
  19406. $msg = "501 5.5.4 Error in RCPT parameter NOTIFY, ".
  19407. "illegal combination of values: $notify";
  19408. } elsif (!@v) {
  19409. $msg = "501 5.5.4 Error in RCPT parameter NOTIFY, ".
  19410. "missing value: $notify";
  19411. }
  19412. $notify = \@v; # replace a string with a listref of items
  19413. }
  19414. if (!defined($msg) && defined($mail_size) &&
  19415. $recip_size_limit && $mail_size > $recip_size_limit &&
  19416. $final_oversized_destiny == D_REJECT) {
  19417. $msg = "552 5.3.4 Declared message size ($mail_size B) ".
  19418. "exceeds size limit for recipient $addr";
  19419. $msg_nopenalize = 1;
  19420. do_log(0, "%s REJECT 'RCPT TO': %s", $self->{proto},$msg);
  19421. }
  19422. if (!defined($msg) && $got_rcpt > $smtpd_recipient_limit) {
  19423. $msg = "452 4.5.3 Too many recipients";
  19424. }
  19425. if (!defined $msg) {
  19426. my $recip_obj = Amavis::In::Message::PerRecip->new;
  19427. $recip_obj->recip_addr($addr_unq);
  19428. $recip_obj->recip_addr_smtp($addr);
  19429. $recip_obj->recip_destiny(D_PASS); # default is Pass
  19430. $recip_obj->dsn_notify($notify) if defined $notify;
  19431. $recip_obj->dsn_orcpt($orcpt) if defined $orcpt;
  19432. push(@recips,$recip_obj);
  19433. $msg = "250 2.1.5 Recipient $addr OK";
  19434. }
  19435. $self->smtp_resp(0,$msg, !$msg_nopenalize && $msg=~/^5/ ? 1 : 0, $cmd);
  19436. last;
  19437. };
  19438. /^DATA\z/ && $args ne '' && do {
  19439. $self->smtp_resp(1,"501 5.5.4 Error: DATA does not accept arguments",
  19440. 1,$cmd); #flush
  19441. last;
  19442. };
  19443. /^DATA\z/ && !@recips && do {
  19444. if (!defined($sender_unq)) {
  19445. $self->smtp_resp(1,"503 5.5.1 Need MAIL command before DATA",1,$cmd);
  19446. } elsif (!$got_rcpt) {
  19447. $self->smtp_resp(1,"503 5.5.1 Need RCPT command before DATA",1,$cmd);
  19448. } elsif ($lmtp) { # RFC 2033 requires 503 code!
  19449. $self->smtp_resp(1,"503 5.1.1 Error (DATA): no valid recipients",
  19450. 0,$cmd); #flush!
  19451. } else {
  19452. $self->smtp_resp(1,"554 5.1.1 Error (DATA): no valid recipients",
  19453. 0,$cmd); #flush!
  19454. }
  19455. last;
  19456. };
  19457. # /^DATA\z/ && uc($msginfo->body_type) eq "BINARYMIME" && do { # RFC 3030
  19458. # $self->smtp_resp(1,"503 5.5.1 DATA is incompatible with BINARYMIME",
  19459. # 0,$cmd); #flush!
  19460. # last;
  19461. # };
  19462. /^DATA\z/ && do {
  19463. # set timer to the initial value, MTA timer starts here
  19464. if ($message_size_limit) { # enforce system-wide size limit
  19465. if (!$max_recip_size_limit ||
  19466. $max_recip_size_limit > $message_size_limit) {
  19467. $max_recip_size_limit = $message_size_limit;
  19468. }
  19469. }
  19470. my $size = 0; my $oversized = 0; my $eval_stat; my $complete;
  19471. # preallocate some storage
  19472. my $out_str = ''; vec($out_str,65536,8) = 0; $out_str = '';
  19473. eval {
  19474. $msginfo->sender($sender_unq); $msginfo->sender_smtp($sender_quo);
  19475. $msginfo->per_recip_data(\@recips);
  19476. ll(1) && do_log(1, "%s:%s:%s %s: %s -> %s%s Received: %s",
  19477. $conn->appl_proto,
  19478. !ref $inet_socket_bind && $conn->socket_ip eq $inet_socket_bind
  19479. ? '' : '['.$conn->socket_ip.']',
  19480. $conn->socket_port, $self->{tempdir}->path,
  19481. $sender_quo,
  19482. join(',', map($_->recip_addr_smtp, @{$msginfo->per_recip_data})),
  19483. join('',
  19484. !defined $msginfo->msg_size ? () : # RFC 1870
  19485. ' SIZE='.$msginfo->msg_size,
  19486. !defined $msginfo->body_type ? () : ' BODY='.$msginfo->body_type,
  19487. !defined $msginfo->auth_submitter ||
  19488. $msginfo->auth_submitter eq '<>' ? ():
  19489. ' AUTH='.$msginfo->auth_submitter,
  19490. !defined $msginfo->dsn_ret ? () : ' RET='.$msginfo->dsn_ret,
  19491. !defined $msginfo->dsn_envid ? () :
  19492. ' ENVID='.xtext_decode($msginfo->dsn_envid),
  19493. ),
  19494. make_received_header_field($msginfo,0) );
  19495. # pipelining checkpoint
  19496. $self->smtp_resp(1,"354 End data with <CR><LF>.<CR><LF>"); #flush!
  19497. $self->{within_data_transfer} = 1;
  19498. # data transferring state
  19499. $Amavis::zmq_obj->register_proc(2,0,'d',am_id()) if $Amavis::zmq_obj;
  19500. $Amavis::snmp_db->register_proc(2,0,'d',am_id()) if $Amavis::snmp_db;
  19501. section_time('SMTP pre-DATA-flush') if $self->{pipelining};
  19502. $self->{tempdir}->empty(0); # mark the mail file as non-empty
  19503. switch_to_client_time('receiving data');
  19504. my $fh = $self->{tempdir}->fh;
  19505. # the copy_smtp_data() will use syswrite, flush buffer just in case
  19506. if ($fh) { $fh->flush or die "Can't flush mail file: $!" }
  19507. if (!$max_recip_size_limit || $final_oversized_destiny == D_PASS) {
  19508. # no message size limit enforced
  19509. ($size,$oversized) = $self->copy_smtp_data($fh, \$out_str, undef);
  19510. } else { # enforce size limit
  19511. do_log(5,"enforcing size limit %s during DATA",
  19512. $max_recip_size_limit);
  19513. ($size,$oversized) =
  19514. $self->copy_smtp_data($fh, \$out_str, $max_recip_size_limit);
  19515. };
  19516. switch_to_my_time('rx data-end');
  19517. $complete = !$self->{within_data_transfer};
  19518. $eof = 1 if !$complete;
  19519. # normal data termination, eof on socket, timeout, fatal error
  19520. do_log(4, "%s< .<CR><LF>", $self->{proto}) if $complete;
  19521. if ($fh) {
  19522. $fh->flush or die "Can't flush mail file: $!";
  19523. # On some systems you have to do a seek whenever you
  19524. # switch between reading and writing. Among other things,
  19525. # this may have the effect of calling stdio's clearerr(3).
  19526. $fh->seek(0,1) or die "Can't seek on file: $!";
  19527. }
  19528. section_time('SMTP DATA');
  19529. 1;
  19530. } or do { # end eval
  19531. $eval_stat = $@ ne '' ? $@ : "errno=$!";
  19532. };
  19533. if ( defined $eval_stat || !$complete || # err or connection broken
  19534. ($oversized && $final_oversized_destiny == D_REJECT) ) {
  19535. chomp $eval_stat if defined $eval_stat;
  19536. # on error, either send: '421 Shutting down',
  19537. # or: '451 Aborted, error in processing' and NOT shut down!
  19538. if ($oversized && !defined $eval_stat &&
  19539. !$self->{within_data_transfer}) {
  19540. my $msg = "552 5.3.4 Message size ($size B) exceeds size limit";
  19541. do_log(0, "%s REJECT: %s", $self->{proto},$msg);
  19542. $self->smtp_resp(1,$msg, 0,$cmd);
  19543. } elsif (!$self->{within_data_transfer}) {
  19544. my $msg = 'Error in processing: ' .
  19545. (defined $eval_stat ? $eval_stat
  19546. : !$complete ? 'incomplete' : '(no error?)');
  19547. do_log(-2, "%s TROUBLE: 451 4.5.0 %s", $self->{proto},$msg);
  19548. $self->smtp_resp(1,"451 4.5.0 $msg");
  19549. ### $aborting = $msg;
  19550. } else {
  19551. $aborting = "Connection broken during data transfer" if $eof;
  19552. $aborting .= ', ' if $aborting ne '' && defined $eval_stat;
  19553. $aborting .= $eval_stat if defined $eval_stat;
  19554. $aborting .= " during waiting for input from client"
  19555. if defined $eval_stat && $eval_stat =~ /^timed out\b/
  19556. && waiting_for_client();
  19557. $aborting = '???' if $aborting eq '';
  19558. do_log(defined $eval_stat ? -1 : 3,
  19559. "%s ABORTING: %s", $self->{proto}, $aborting);
  19560. }
  19561. } else { # all OK
  19562. # According to RFC 1047 it is not a good idea to do lengthy
  19563. # processing here, but we do not have much choice, amavis has no
  19564. # queuing mechanism and cannot accept responsibility for delivery.
  19565. #
  19566. # check contents before responding
  19567. # check_mail() expects an open file handle in $msginfo->mail_text,
  19568. # need not be rewound
  19569. $msginfo->mail_tempdir($self->{tempdir}->path);
  19570. $msginfo->mail_text_fn($self->{tempdir}->path . '/email.txt');
  19571. $msginfo->mail_text($self->{tempdir}->fh);
  19572. $msginfo->mail_text_str(\$out_str) if defined $out_str &&
  19573. $out_str ne '';
  19574. #
  19575. # RFC 1870: The message size is defined as the number of octets,
  19576. # including CR-LF pairs, but not counting the SMTP DATA command's
  19577. # terminating dot or doubled (stuffing) dots
  19578. my $declared_size = $msginfo->msg_size; # RFC 1870
  19579. if (!defined($declared_size)) {
  19580. do_log(5, "message size set to %s", $size);
  19581. } elsif ($size > $declared_size) { # shouldn't happen with decent MTA
  19582. do_log(4,"Actual message size %s B greater than the ".
  19583. "declared %s B", $size,$declared_size);
  19584. } elsif ($size < $declared_size) { # not unusual, but permitted
  19585. do_log(4,"Actual message size %d B less than the declared %d B",
  19586. $size,$declared_size);
  19587. }
  19588. $msginfo->msg_size(untaint($size)); # store actual RFC 1870 mail size
  19589. # some fatal errors are not catchable by eval (like exceeding virtual
  19590. # memory), but may still allow processing to continue in a DESTROY or
  19591. # END method; turn on trouble flag here to allow DESTROY to deal with
  19592. # such a case correctly, then clear the flag after content checking
  19593. # if everything turned out well
  19594. $self->{tempdir}->preserve(1);
  19595. my($smtp_resp, $exit_code, $preserve_evidence) =
  19596. &$check_mail($msginfo,$lmtp); # do all the contents checking
  19597. $self->{tempdir}->preserve(0) if !$preserve_evidence; # clear if ok
  19598. prolong_timer('check done');
  19599. if ($smtp_resp =~ /^4/) {
  19600. # ok, not-done recipients are to be expected, do not check
  19601. } elsif (grep(!$_->recip_done && $_->delivery_method ne '',
  19602. @{$msginfo->per_recip_data})) {
  19603. die "TROUBLE: (MISCONFIG?) not all recipients done";
  19604. } elsif (grep(!$_->recip_done && $_->delivery_method eq '',
  19605. @{$msginfo->per_recip_data})) {
  19606. die "NOT ALL RECIPIENTS DONE, EMPTY DELIVERY_METHOD!";
  19607. # do_log(0, "NOT ALL RECIPIENTS DONE, EMPTY DELIVERY_METHOD!");
  19608. }
  19609. section_time('SMTP pre-response');
  19610. if (!$lmtp) { # smtp
  19611. do_log(3, 'sending SMTP response: "%s"', $smtp_resp);
  19612. $self->smtp_resp(0, $smtp_resp);
  19613. } else { # lmtp
  19614. my $bounced = $msginfo->dsn_sent; # 1=bounced, 2=suppressed
  19615. for my $r (@{$msginfo->per_recip_data}) {
  19616. my $resp = $r->recip_smtp_response;
  19617. my $recip_quoted = $r->recip_addr_smtp;
  19618. if ($resp=~/^[24]/) {
  19619. # success or tempfail, no need to change status
  19620. } elsif ($bounced && $bounced == 1) { # genuine bounce
  19621. # a non-delivery notifications was already sent by us, so
  19622. # MTA must not bounce it again; turn status into a success
  19623. $resp = sprintf("250 2.5.0 Ok %s, DSN was sent (%s)",
  19624. $recip_quoted, $resp);
  19625. } elsif ($bounced) { # fake bounce - bounce was suppressed
  19626. $resp = sprintf("250 2.5.0 Ok %s, DSN suppressed (%s)",
  19627. $recip_quoted, $resp);
  19628. } elsif ($resp=~/^5/ && $r->recip_destiny != D_REJECT) {
  19629. # just in case, if the bounce suppression scheme did not work
  19630. $resp = sprintf("250 2.5.0 Ok %s, DSN suppressed_2 (%s)",
  19631. $recip_quoted, $resp);
  19632. }
  19633. do_log(3, 'LMTP response for %s: "%s"', $recip_quoted, $resp);
  19634. $self->smtp_resp(0, $resp);
  19635. }
  19636. }
  19637. $self->smtp_resp_flush; # optional, but nice to report timing right
  19638. section_time('SMTP response');
  19639. }; # end all OK
  19640. $self->{tempdir}->clean;
  19641. my $msg_size = $msginfo->msg_size;
  19642. $sender_unq = $sender_quo = undef; @recips = (); $got_rcpt = 0;
  19643. undef $max_recip_size_limit; undef $msginfo; # forget previous
  19644. %xforward_args = ();
  19645. section_time('dump_captured_log') if log_capture_enabled();
  19646. dump_captured_log(1, c('enable_log_capture_dump'));
  19647. %current_policy_bank = %baseline_policy_bank; # restore bank settings
  19648. # report elapsed times by section for each transaction
  19649. # (the time for a QUIT remains unaccounted for)
  19650. do_log(2, "size: %d, %s", $msg_size, Amavis::Timing::report());
  19651. Amavis::Timing::init(); snmp_counters_init();
  19652. $Amavis::last_task_completed_at = Time::HiRes::time;
  19653. last;
  19654. }; # DATA
  19655. /^(?:EXPN|TURN|ETRN|SEND|SOML|SAML)\z/ && do {
  19656. $self->smtp_resp(1,"502 5.5.1 Error: command $_ not implemented",
  19657. 0,$cmd);
  19658. last;
  19659. };
  19660. # catchall (unknown commands): #flush!
  19661. $self->smtp_resp(1,"500 5.5.2 Error: command $_ not recognized", 1,$cmd);
  19662. }; # end of 'switch' block
  19663. if ($terminating || defined $aborting) { # exit SMTP-session loop
  19664. $voluntary_exit = 1; last;
  19665. }
  19666. # don't bother, just flush any responses regardless of pending input;
  19667. # this also keeps us on the safe side when a Postfix pre-queue setup
  19668. # turns HELO into EHLO sessions and smtpd_proxy_options=speed_adjust
  19669. # is not in use
  19670. $self->smtp_resp_flush;
  19671. #
  19672. # if ($self->{smtp_outbuf} && @{$self->{smtp_outbuf}} &&
  19673. # $self->{pipelining}) {
  19674. # # RFC 2920 requires a flush whenever a local TCP input buffer is emptied
  19675. # my $fd_sock = fileno($sock);
  19676. # my $rout; my $rin = ''; vec($rin,$fd_sock,1) = 1;
  19677. # my($nfound, $timeleft) = select($rout=$rin, undef, undef, 0);
  19678. # if (defined $nfound && $nfound > 0 && vec($rout, $fd_sock, 1)) {
  19679. # # input is available, do not bother flushing output yet
  19680. # do_log(2,"pipelining in effect, input available, flush delayed");
  19681. # } else {
  19682. # $self->smtp_resp_flush;
  19683. # }
  19684. # }
  19685. $0 = sprintf("%s (ch%d-%s-idle)",
  19686. c('myprogram_name'), $Amavis::child_invocation_count, am_id());
  19687. Amavis::Timing::go_idle(6);
  19688. } # end of loop
  19689. my($errn,$errs);
  19690. if (!$voluntary_exit) {
  19691. $eof = 1;
  19692. if (!defined($_)) {
  19693. $errn = 0+$!;
  19694. $errs = !$self->{ssl_active} ? "$!" : $sock->errstr.", $!";
  19695. }
  19696. }
  19697. # come here when: QUIT is received, eof or err on socket, or we need to abort
  19698. $0 = sprintf("%s (ch%d)",
  19699. c('myprogram_name'), $Amavis::child_invocation_count);
  19700. alarm(0); do_log(4,"SMTP session over, timer stopped");
  19701. Amavis::Timing::go_busy(7);
  19702. # flush just in case, session might have been disconnected
  19703. eval {
  19704. $self->smtp_resp_flush; 1;
  19705. } or do {
  19706. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  19707. do_log(1, "flush failed: %s", $eval_stat);
  19708. };
  19709. my $msg =
  19710. defined $aborting && !$eof ? "ABORTING the session: $aborting" :
  19711. defined $aborting ? $aborting :
  19712. !$terminating ? "client broke the connection without a QUIT ($errs)" : '';
  19713. if ($msg eq '') {
  19714. # ok
  19715. } elsif ($aborting) {
  19716. do_log(-1, "%s: NOTICE: %s", $self->{proto},$msg);
  19717. } else {
  19718. do_log( 3, "%s: notice: %s", $self->{proto},$msg);
  19719. }
  19720. if (defined $aborting && !$eof)
  19721. { $self->smtp_resp(1,"421 4.3.2 Service shutting down, ".$aborting) }
  19722. $self->{session_closed_normally} = 1;
  19723. # Net::Server closes connection after child_finish_hook
  19724. }
  19725. # sends an SMTP response consisting of a 3-digit code and an optional message;
  19726. # slow down evil clients by delaying response on permanent errors
  19727. #
  19728. sub smtp_resp($$$;$$) {
  19729. my($self, $flush,$resp, $penalize,$line) = @_;
  19730. if ($penalize) { # PENALIZE syntax errors?
  19731. do_log(0, "%s: %s; smtp_resp: %s", $self->{proto},$resp,$line);
  19732. # sleep 1;
  19733. # section_time('SMTP penalty wait');
  19734. }
  19735. push(@{$self->{smtp_outbuf}}, @{wrap_smtp_resp(sanitize_str($resp,1))});
  19736. $self->smtp_resp_flush if $flush || !$self->{pipelining} ||
  19737. @{$self->{smtp_outbuf}} > 200;
  19738. }
  19739. sub smtp_resp_flush($) {
  19740. my $self = shift;
  19741. my $outbuf_ref = $self->{smtp_outbuf};
  19742. if ($outbuf_ref && @$outbuf_ref) {
  19743. if (ll(4)) { do_log(4, "%s> %s", $self->{proto}, $_) for @$outbuf_ref }
  19744. my $sock = $self->{sock};
  19745. my $stat = $sock->print(join('', map($_."\015\012", @$outbuf_ref)));
  19746. @$outbuf_ref = (); # prevent printing again even if error
  19747. $stat or die "Error writing an SMTP response to the socket: ".
  19748. (!$self->{ssl_active} ? $! : $sock->errstr.", $!");
  19749. $sock->flush or die "Error flushing an SMTP response to the socket: ".
  19750. (!$self->{ssl_active} ? $! : $sock->errstr.", $!");
  19751. # put a ball in client's courtyard, start his timer
  19752. switch_to_client_time('smtp response sent');
  19753. }
  19754. }
  19755. 1;
  19756. __DATA__
  19757. #
  19758. package Amavis::In::Courier;
  19759. use strict;
  19760. use re 'taint';
  19761. use warnings;
  19762. use warnings FATAL => qw(utf8 void);
  19763. no warnings 'uninitialized';
  19764. BEGIN { die "Code not available for module Amavis::In::Courier" }
  19765. 1;
  19766. __DATA__
  19767. #
  19768. package Amavis::Out::SMTP::Protocol;
  19769. use strict;
  19770. use re 'taint';
  19771. use warnings;
  19772. use warnings FATAL => qw(utf8 void);
  19773. no warnings 'uninitialized';
  19774. BEGIN {
  19775. require Exporter;
  19776. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  19777. $VERSION = '2.316';
  19778. @ISA = qw(Exporter);
  19779. import Amavis::Conf qw(:platform);
  19780. import Amavis::Util qw(ll do_log min max minmax);
  19781. }
  19782. use Errno qw(EIO EINTR EAGAIN ECONNRESET);
  19783. use Time::HiRes ();
  19784. sub init {
  19785. my $self = shift;
  19786. delete $self->{domain}; delete $self->{supports};
  19787. $self->{pipelining} = 0;
  19788. }
  19789. sub new {
  19790. my($class,$socket_specs,%arg) = @_;
  19791. my $self = bless {}, $class;
  19792. $self->{at_line_boundary} = 1;
  19793. $self->{dotstuffing} = 1; # defaults to on
  19794. $self->{dotstuffing} = 0 if defined $arg{DotStuffing} && !$arg{DotStuffing};
  19795. $self->{strip_cr} = 1; # sanitizing bare CR defaults to on
  19796. $self->{strip_cr} = 0 if defined $arg{StripCR} && !$arg{StripCR};
  19797. $self->{io} = Amavis::IO::RW->new($socket_specs, Eol => "\015\012", %arg);
  19798. $self->init;
  19799. $self;
  19800. }
  19801. sub close {
  19802. my $self = $_[0];
  19803. $self->{io}->close;
  19804. }
  19805. sub DESTROY {
  19806. my $self = $_[0]; local($@,$!,$_);
  19807. eval { $self->close } or 1; # ignore failure, make perlcritic happy
  19808. }
  19809. sub ehlo_response_parse {
  19810. my($self,$smtp_resp) = @_;
  19811. delete $self->{domain}; delete $self->{supports};
  19812. my(@ehlo_lines) = split(/\n/,$smtp_resp,-1);
  19813. my $bad; my $first = 1; local($1,$2);
  19814. for my $el (@ehlo_lines) {
  19815. if ($first) {
  19816. if ($el =~ /^(\d{3})(?:[ \t]+(.*))?\z/s) { $self->{domain} = $2 }
  19817. elsif (!defined($bad)) { $bad = $el }
  19818. $first = 0;
  19819. } elsif ($el =~ /^([A-Z0-9][A-Z0-9-]*)(?:[ =](.*))?\z/si) {
  19820. $self->{supports}{uc($1)} = defined($2) ? $2 : '';
  19821. } elsif ($el =~ /^[ \t]*\z/s) {
  19822. # don't bother (e.g. smtp-sink)
  19823. } elsif (!defined($bad)) {
  19824. $bad = $el;
  19825. }
  19826. }
  19827. $self->{pipelining} = defined $self->{supports}{'PIPELINING'} ? 1 : 0;
  19828. do_log(0, "Bad EHLO kw %s ignored in %s, socket %s",
  19829. $bad, $smtp_resp, $self->socketname) if defined $bad;
  19830. 1;
  19831. }
  19832. sub domain
  19833. { my $self = $_[0]; $self->{domain} }
  19834. sub supports
  19835. { my($self,$keyword) = @_; $self->{supports}{uc($keyword)} }
  19836. *print = \&datasend; # alias name for datasend
  19837. sub datasend {
  19838. my $self = shift;
  19839. my $buff = @_ == 1 ? $_[0] : join('',@_);
  19840. do_log(-1,"WARN: Unicode string passed to datasend")
  19841. if Encode::is_utf8($buff); # always false on tainted, Perl 5.8 bug #32687
  19842. # ll(5) && do_log(5, 'smtp print %d bytes>', length($buff));
  19843. $buff =~ tr/\r//d if $self->{strip_cr}; # sanitize bare CR if necessary
  19844. # CR/LF are never split across a buffer boundary
  19845. $buff =~ s{\n}{\015\012}gs; # quite fast, but still a bottleneck
  19846. if ($self->{dotstuffing}) {
  19847. $buff =~ s{\015\012\.}{\015\012..}gs; # dot stuffing
  19848. $self->{io}->print('.') if substr($buff,0,1) eq '.' &&
  19849. $self->{at_line_boundary};
  19850. }
  19851. $self->{io}->print($buff);
  19852. $self->{at_line_boundary} = $self->{io}->at_line_boundary;
  19853. $self->{io}->out_buff_large ? $self->flush : 1;
  19854. }
  19855. sub socketname
  19856. { my $self = shift; $self->{io}->socketname(@_) }
  19857. sub protocol
  19858. { my $self = shift; $self->{io}->protocol(@_) }
  19859. sub timeout
  19860. { my $self = shift; $self->{io}->timeout(@_) }
  19861. sub ssl_active
  19862. { my $self = shift; $self->{io}->ssl_active(@_) }
  19863. sub ssl_upgrade
  19864. { my $self = shift; $self->{io}->ssl_upgrade(@_) }
  19865. sub last_io_event_timestamp
  19866. { my $self = shift; $self->{io}->last_io_event_timestamp(@_) }
  19867. sub last_io_event_tx_timestamp
  19868. { my $self = shift; $self->{io}->last_io_event_tx_timestamp(@_) }
  19869. sub eof
  19870. { my $self = shift; $self->{io}->eof(@_) }
  19871. sub flush
  19872. { my $self = shift; $self->{io}->flush(@_) }
  19873. sub dataend {
  19874. my($self) = @_;
  19875. if (!$self->{at_line_boundary}) {
  19876. $self->datasend("\n");
  19877. }
  19878. if ($self->{dotstuffing}) {
  19879. $self->{dotstuffing} = 0;
  19880. $self->datasend(".\n");
  19881. $self->{dotstuffing} = 1;
  19882. }
  19883. $self->{io}->out_buff_large ? $self->flush : 1;
  19884. }
  19885. sub command {
  19886. my($self,$command,@args) = @_;
  19887. my $line = $command =~ /:\z/ ? $command.join(' ',@args)
  19888. : join(' ',$command,@args);
  19889. ll(3) && do_log(3, 'smtp cmd> %s', $line);
  19890. $self->datasend($line."\n"); $self->{at_line_boundary} = 1;
  19891. # RFC 2920: commands that can appear anywhere in a pipelined command group
  19892. # RSET, MAIL FROM, SEND FROM, SOML FROM, SAML FROM, RCPT TO, (data)
  19893. if (!$self->{pipelining} || $self->{io}->out_buff_large ||
  19894. $command !~ /^(?:RSET|MAIL|SEND|SOML|SAML|RCPT)\b/is) {
  19895. return $self->flush;
  19896. }
  19897. 1;
  19898. }
  19899. sub smtp_response {
  19900. my($self) = @_;
  19901. my $resp = ''; my($line,$code,$enh); my $first = 1;
  19902. for (;;) {
  19903. $line = $self->{io}->get_response_line;
  19904. last if !defined $line; # eof, error, timeout
  19905. my $line_complete = $line =~ s/\015\012\z//s;
  19906. $line .= ' INCOMPLETE' if !$line_complete;
  19907. my $more; local($1,$2,$3);
  19908. $line =~ s/^(\d{3}) (-|\ |\z)
  19909. (?: ([245] \. \d{1,3} \. \d{1,3}) (\ |\z) )?//xs;
  19910. if ($first) { $code = $1; $enh = $3; $first = 0 } else { $resp .= "\n" }
  19911. $resp .= $line; $more = $2 eq '-';
  19912. last if !$more || !$line_complete;
  19913. }
  19914. !defined $code ? undef : $code . (defined $enh ? " $enh" : '') . ' '. $resp;
  19915. }
  19916. sub helo { my $self = shift; $self->init; $self->command("HELO",@_) }
  19917. sub ehlo { my $self = shift; $self->init; $self->command("EHLO",@_) }
  19918. sub lhlo { my $self = shift; $self->init; $self->command("LHLO",@_) }
  19919. sub noop { my $self = shift; $self->command("NOOP",@_) }
  19920. sub rset { my $self = shift; $self->command("RSET",@_) }
  19921. sub auth { my $self = shift; $self->command("AUTH",@_) }
  19922. sub data { my $self = shift; $self->command("DATA",@_) }
  19923. sub quit { my $self = shift; $self->command("QUIT",@_) }
  19924. sub mail {
  19925. my($self,$reverse_path,%params) = @_;
  19926. my(@mail_parameters) =
  19927. map { my $v = $params{$_}; defined($v) ? "$_=$v" : "$_" } (keys %params);
  19928. $self->command("MAIL FROM:", $reverse_path, @mail_parameters);
  19929. }
  19930. sub recipient {
  19931. my($self,$forward_path,%params) = @_;
  19932. my(@rcpt_parameters) =
  19933. map { my $v = $params{$_}; defined($v) ? "$_=$v" : "$_" } (keys %params);
  19934. $self->command("RCPT TO:", $forward_path, @rcpt_parameters);
  19935. }
  19936. 1;
  19937. package Amavis::Out::SMTP::Session;
  19938. # provides a mechanism for SMTP session caching
  19939. use strict;
  19940. use re 'taint';
  19941. use warnings;
  19942. use warnings FATAL => qw(utf8 void);
  19943. no warnings 'uninitialized';
  19944. BEGIN {
  19945. require Exporter;
  19946. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  19947. $VERSION = '2.316';
  19948. @ISA = qw(Exporter);
  19949. @EXPORT_OK = qw(&rundown_stale_sessions);
  19950. import Amavis::Conf qw(:platform c cr ca $smtp_connection_cache_enable);
  19951. import Amavis::Util qw(min max minmax ll do_log snmp_count);
  19952. }
  19953. use subs @EXPORT_OK;
  19954. use vars qw(%sessions_cache);
  19955. use Time::HiRes qw(time);
  19956. sub new {
  19957. my($class, $socket_specs, $deadline,
  19958. $wildcard_implied_host, $wildcard_implied_port) = @_;
  19959. my $self; my $cache_key; my $found_cached = 0;
  19960. for my $proto_sockname (ref $socket_specs ? @$socket_specs : $socket_specs) {
  19961. $cache_key = $proto_sockname;
  19962. local($1,$2,$3,$4);
  19963. if ($proto_sockname =~ # deal with dynamic destinations (wildcards)
  19964. /^([a-z][a-z0-9.+-]*) : (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*)/sx) {
  19965. my $peeraddress = defined $2 ? $2 : $3; my $peerport = $4;
  19966. $peeraddress = $wildcard_implied_host if $peeraddress eq '*';
  19967. $peerport = $wildcard_implied_port if $peerport eq '*';
  19968. $cache_key = sprintf("%s:[%s]:%s", $1, $peeraddress, $peerport);
  19969. }
  19970. if (exists $sessions_cache{$cache_key}) { $found_cached = 1; last }
  19971. }
  19972. if ($found_cached) {
  19973. $self = $sessions_cache{$cache_key};
  19974. $self->{deadline} = $deadline;
  19975. do_log(3, "smtp session reuse (%s), %d transactions so far",
  19976. $cache_key, $self->{transaction_count});
  19977. } else {
  19978. do_log(3, "smtp session: setting up a new session");
  19979. $cache_key = undef;
  19980. $self = bless {
  19981. socket_specs => $socket_specs,
  19982. socketname => undef, protocol => undef, smtp_handle => undef,
  19983. deadline => $deadline, timeout => undef, in_xactn => 0,
  19984. transaction_count => 0, state => 'down', established_at_time => undef,
  19985. wildcard_implied_host => $wildcard_implied_host,
  19986. wildcard_implied_port => $wildcard_implied_port,
  19987. }, $class;
  19988. }
  19989. $self->establish_or_refresh;
  19990. if (!defined $cache_key) { # newly established session
  19991. $cache_key = sprintf("%s:%s", $self->protocol, $self->socketname);
  19992. $sessions_cache{$cache_key} = $self;
  19993. }
  19994. $self;
  19995. }
  19996. sub smtp_handle
  19997. { my $self = shift; !@_ ? $self->{handle} : ($self->{handle}=shift) }
  19998. sub socketname
  19999. { my $self = shift; !@_ ? $self->{socketname} :($self->{socketname}=shift) }
  20000. sub protocol
  20001. { my $self = shift; !@_ ? $self->{protocol} : ($self->{protocol}=shift) }
  20002. sub session_state
  20003. { my $self = shift; !@_ ? $self->{state} : ($self->{state}=shift) }
  20004. sub in_smtp_transaction
  20005. { my $self = shift; !@_ ? $self->{in_xactn} : ($self->{in_xactn}=shift) }
  20006. sub established_at_time
  20007. { my $self = shift; !@_ ? $self->{established_at_time}
  20008. : ($self->{established_at_time}=shift) }
  20009. sub transaction_begins {
  20010. my $self = $_[0];
  20011. !$self->in_smtp_transaction
  20012. or die "smtp session: transaction_begins, but already active";
  20013. $self->in_smtp_transaction(1);
  20014. }
  20015. sub transaction_begins_unconfirmed {
  20016. my $self = $_[0];
  20017. snmp_count('OutConnTransact'); $self->{transaction_count}++;
  20018. !$self->in_smtp_transaction
  20019. or die "smtp session: transaction_begins_unconfirmed, but already active";
  20020. $self->in_smtp_transaction(undef);
  20021. }
  20022. sub transaction_ends {
  20023. my $self = $_[0];
  20024. $self->in_smtp_transaction(0);
  20025. }
  20026. sub transaction_ends_unconfirmed {
  20027. my $self = $_[0];
  20028. # if already 0 then keep it, otherwise undefine
  20029. $self->in_smtp_transaction(undef) if $self->in_smtp_transaction;
  20030. }
  20031. sub timeout {
  20032. my $self = shift;
  20033. if (@_) {
  20034. my $timeout = shift;
  20035. $self->{timeout} = $timeout;
  20036. $self->{handle}->timeout($timeout) if defined $self->{handle};
  20037. # do_log(5, "smtp session, timeout set to %s", $timeout);
  20038. }
  20039. $self->{timeout};
  20040. }
  20041. sub supports {
  20042. my($self,$keyword) = @_;
  20043. defined $self->{handle} ? $self->{handle}->supports($keyword) : undef;
  20044. }
  20045. sub smtp_response {
  20046. my $self = $_[0];
  20047. defined $self->{handle} ? $self->{handle}->smtp_response : undef;
  20048. }
  20049. sub quit {
  20050. my $self = $_[0];
  20051. my $smtp_handle = $self->smtp_handle;
  20052. if (defined $smtp_handle) {
  20053. $self->session_state('quitsent');
  20054. snmp_count('OutConnQuit');
  20055. $smtp_handle->quit; #flush! QUIT
  20056. }
  20057. }
  20058. sub close {
  20059. my($self,$keep_connected) = @_;
  20060. my $msg; my $smtp_handle = $self->smtp_handle;
  20061. if (defined($smtp_handle) && $smtp_handle->eof) {
  20062. $msg = 'already disconnected'; $keep_connected = 0;
  20063. } else {
  20064. $msg = $keep_connected ? 'keeping connection' : 'disconnecting';
  20065. }
  20066. do_log(3, "Amavis::Out::SMTP::Session close, %s", $msg);
  20067. if (!$keep_connected) {
  20068. if (defined $smtp_handle) {
  20069. $smtp_handle->close
  20070. or do_log(1, "Error closing Amavis::Out::SMTP::Protocol obj");
  20071. $self->in_smtp_transaction(0); $self->established_at_time(undef);
  20072. $self->smtp_handle(undef); $self->session_state('down');
  20073. }
  20074. if (defined $self->socketname) {
  20075. my $cache_key = sprintf("%s:%s", $self->protocol, $self->socketname);
  20076. delete $sessions_cache{$cache_key} if exists $sessions_cache{$cache_key};
  20077. }
  20078. }
  20079. 1;
  20080. }
  20081. sub rundown_stale_sessions($) {
  20082. my($close_all) = @_;
  20083. my $num_sessions_closed = 0;
  20084. for my $cache_key (keys %sessions_cache) {
  20085. my $smtp_session = $sessions_cache{$cache_key};
  20086. my $smtp_handle = $smtp_session->smtp_handle;
  20087. my $established_at_time = $smtp_session->established_at_time;
  20088. my $last_event_time;
  20089. $last_event_time = $smtp_handle->last_io_event_timestamp if $smtp_handle;
  20090. my $now = Time::HiRes::time;
  20091. if ($close_all || !$smtp_connection_cache_enable ||
  20092. !defined($last_event_time) || $now - $last_event_time >= 30 ||
  20093. !defined($established_at_time) || $now - $established_at_time >= 60) {
  20094. ll(3) && do_log(3,"smtp session rundown%s%s%s, %s, state %s",
  20095. $close_all ? ' all sessions'
  20096. : $smtp_connection_cache_enable ? ' stale sessions'
  20097. : ', cache off',
  20098. !defined($last_event_time) ? ''
  20099. : sprintf(", idle %.1f s", $now - $last_event_time),
  20100. !defined($established_at_time) ? ''
  20101. : sprintf(", since %.1f s ago",
  20102. $now - $established_at_time),
  20103. $cache_key, $smtp_session->session_state);
  20104. if ($smtp_session->session_state ne 'down' &&
  20105. $smtp_session->session_state ne 'quitsent' &&
  20106. (!defined($last_event_time) || $now - $last_event_time <= 55)) {
  20107. do_log(3,"smtp session rundown, sending QUIT");
  20108. eval { $smtp_session->quit } or 1; #flush! QUIT (ignoring failures)
  20109. }
  20110. if ($smtp_session->session_state eq 'quitsent') { # collect response
  20111. $smtp_session->timeout(5);
  20112. my $smtp_resp = eval { $smtp_session->smtp_response };
  20113. if (!defined $smtp_resp) {
  20114. do_log(3,"No SMTP resp. to QUIT");
  20115. } elsif ($smtp_resp eq '') {
  20116. do_log(3,"Empty SMTP resp. to QUIT");
  20117. } elsif ($smtp_resp !~ /^2/) {
  20118. do_log(3,"Negative SMTP resp. to QUIT: %s", $smtp_resp);
  20119. } else { # success, $smtp_resp =~ /^2/
  20120. do_log(3,"smtp resp to QUIT: %s", $smtp_resp);
  20121. }
  20122. }
  20123. if ($smtp_session->session_state ne 'down') {
  20124. do_log(3,"smtp session rundown, closing session %s", $cache_key);
  20125. $smtp_session->close(0)
  20126. or do_log(-2, "Error closing smtp session %s", $cache_key);
  20127. $num_sessions_closed++;
  20128. }
  20129. }
  20130. }
  20131. $num_sessions_closed;
  20132. }
  20133. sub establish_or_refresh {
  20134. my($self) = @_;
  20135. # Timeout should be more than MTA normally takes to check DNS and RBL,
  20136. # which may take a minute or more in case of unreachable DNS server.
  20137. # Specifying shorter timeout will cause alarm to terminate the wait
  20138. # for SMTP status line prematurely, resulting in status code 000.
  20139. # RFC 5321 (ex RFC 2821) section 4.5.3.2 requires timeout to be
  20140. # at least 5 minutes
  20141. my $smtp_connect_timeout = 35; # seconds
  20142. my $smtp_helo_timeout = 300;
  20143. my $smtp_starttls_timeout = 300;
  20144. my $smtp_handle = $self->smtp_handle;
  20145. my $smtp_resp; my $last_event_time;
  20146. $last_event_time = $smtp_handle->last_io_event_timestamp if $smtp_handle;
  20147. my $now = Time::HiRes::time;
  20148. do_log(5,"establish_or_refresh, state: %s", $self->session_state);
  20149. die "panic, still in SMTP transaction" if $self->in_smtp_transaction;
  20150. if (defined($smtp_handle) &&
  20151. $self->session_state ne 'down' && $self->session_state ne 'quitsent') {
  20152. # if session has been idling for some time, check with a low-cost NOOP
  20153. # whether the session is still alive - reconnecting at this time is cheap;
  20154. # note that NOOP is non-pipelinable, MTA must respond immediately
  20155. if (defined($last_event_time) && $now - $last_event_time <= 18) {
  20156. snmp_count('OutConnReuseRecent');
  20157. do_log(3,"smtp session most likely still valid (short idle %.1f s)",
  20158. $now - $last_event_time);
  20159. } else { # Postfix default smtpd idle timeout is 60 s
  20160. eval {
  20161. $self->timeout(15);
  20162. $smtp_handle->noop; #flush!
  20163. $smtp_resp = $self->smtp_response; # fetch response to NOOP
  20164. do_log(3,"smtp resp to NOOP (idle %.1f s): %s",
  20165. $now - $last_event_time, $smtp_resp);
  20166. 1;
  20167. } or do {
  20168. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  20169. do_log(3,"smtp NOOP failed (idle %.1f s): %s",
  20170. $now - $last_event_time, $eval_stat);
  20171. $smtp_resp = '';
  20172. };
  20173. if ($smtp_resp =~ /^2/) {
  20174. snmp_count('OutConnReuseRefreshed');
  20175. } else {
  20176. snmp_count('OutConnReuseFail');
  20177. $self->close(0) or do_log(-1, "Error closing smtp session");
  20178. }
  20179. }
  20180. }
  20181. if ($self->session_state eq 'down' || $self->session_state eq 'quitsent') {
  20182. if (defined $smtp_handle) {
  20183. $smtp_handle->close
  20184. or do_log(-2, "Error closing Amavis::Out::SMTP::Protocol obj");
  20185. undef $smtp_handle;
  20186. }
  20187. my $localaddr = c('local_client_bind_address'); # IP assigned to socket
  20188. snmp_count('OutConnNew');
  20189. $smtp_handle = Amavis::Out::SMTP::Protocol->new(
  20190. $self->{socket_specs}, LocalAddr => $localaddr, Timeout => 35,
  20191. WildcardImpliedHost => $self->{wildcard_implied_host},
  20192. WildcardImpliedPort => $self->{wildcard_implied_port});
  20193. $self->smtp_handle($smtp_handle);
  20194. defined $smtp_handle # don't change die text, it is referred to elsewhere
  20195. or die sprintf("Can't connect to %s",
  20196. !ref $self->{socket_specs} ? $self->{socket_specs}
  20197. : join(", ",@$self->{socket_specs}) );
  20198. $self->socketname($smtp_handle->socketname);
  20199. $self->protocol($smtp_handle->protocol);
  20200. $self->session_state('connected');
  20201. $self->established_at_time(time);
  20202. $self->timeout($smtp_connect_timeout);
  20203. $smtp_resp = $self->smtp_response; # fetch greeting
  20204. if (!defined $smtp_resp || $smtp_resp eq '') {
  20205. die sprintf("%s greeting, dt: %.3f s\n",
  20206. !defined $smtp_resp ? 'No' : 'Empty',
  20207. time - $smtp_handle->last_io_event_tx_timestamp);
  20208. } elsif ($smtp_resp !~ /^2/) {
  20209. die "Negative greeting: $smtp_resp\n";
  20210. } else { # success, $smtp_resp =~ /^2/
  20211. do_log(3,"smtp greeting: %s, dt: %.1f ms", $smtp_resp,
  20212. 1000*(time-$smtp_handle->last_io_event_tx_timestamp));
  20213. }
  20214. }
  20215. if ($self->session_state eq 'connected') {
  20216. my $lmtp = lc($self->protocol) eq 'lmtp' ? 1 : 0; # RFC 2033
  20217. my $deadline = $self->{deadline};
  20218. my $tls_security_level = c('tls_security_level_out');
  20219. $tls_security_level = 0 if !defined($tls_security_level) ||
  20220. lc($tls_security_level) eq 'none';
  20221. my $heloname = c('localhost_name'); # host name used in EHLO/HELO/LHLO
  20222. $heloname = 'localhost' if $heloname eq '';
  20223. for (1..2) {
  20224. # send EHLO/LHLO/HELO
  20225. $self->timeout(max(60,min($smtp_helo_timeout,
  20226. $deadline - time)));
  20227. if ($lmtp) { $smtp_handle->lhlo($heloname) } #flush!
  20228. else { $smtp_handle->ehlo($heloname) } #flush!
  20229. $smtp_resp = $self->smtp_response; # fetch response to EHLO/LHLO
  20230. if (!defined $smtp_resp || $smtp_resp eq '') {
  20231. die sprintf("%s response to %s, dt: %.3f s\n",
  20232. !defined $smtp_resp ? 'No' : 'Empty',
  20233. $lmtp ? 'LHLO' : 'EHLO',
  20234. time - $smtp_handle->last_io_event_tx_timestamp);
  20235. } elsif ($smtp_resp =~ /^2/) { # success
  20236. do_log(3,"smtp resp to %s: %s", $lmtp?'LHLO':'EHLO', $smtp_resp);
  20237. } elsif ($lmtp) { # failure, no fallback possible
  20238. die "Negative SMTP resp. to LHLO: $smtp_resp\n";
  20239. } else { # failure, SMTP fallback to HELO
  20240. do_log(3,"Negative SMTP resp. to EHLO, will try HELO: %s", $smtp_resp);
  20241. $smtp_handle->helo($heloname); #flush!
  20242. $smtp_resp = $self->smtp_response; # fetch response to HELO
  20243. if (!defined $smtp_resp || $smtp_resp eq '') {
  20244. die sprintf("%s response to HELO, dt: %.3f s\n",
  20245. !defined $smtp_resp ? 'No' : 'Empty',
  20246. time - $smtp_handle->last_io_event_tx_timestamp);
  20247. } elsif ($smtp_resp !~ /^2/) {
  20248. die "Negative response to HELO: $smtp_resp\n";
  20249. } else { # success, $smtp_resp =~ /^2/
  20250. do_log(3,"smtp resp to HELO: %s", $smtp_resp);
  20251. }
  20252. }
  20253. $self->session_state('ehlo');
  20254. $smtp_handle->ehlo_response_parse($smtp_resp);
  20255. my $tls_capable = defined($self->supports('STARTTLS')); # RFC 3207
  20256. ll(5) && do_log(5, "tls active=%d, capable=%s, sec_level=%s",
  20257. $smtp_handle->ssl_active, $tls_capable, $tls_security_level);
  20258. if ($smtp_handle->ssl_active) {
  20259. last; # done
  20260. } elsif (!$tls_capable &&
  20261. $tls_security_level && lc($tls_security_level) ne 'may') {
  20262. die "MTA does not offer STARTTLS, ".
  20263. "but TLS is required: \"$tls_security_level\"";
  20264. } elsif (!$tls_capable || !$tls_security_level) {
  20265. last; # not offered and not mandated
  20266. } else {
  20267. $self->timeout(max(60,min($smtp_starttls_timeout,
  20268. $deadline - time)));
  20269. $smtp_handle->command('STARTTLS'); #flush!
  20270. $smtp_resp = $self->smtp_response; # fetch response to STARTTLS
  20271. $smtp_resp = '' if !defined $smtp_resp;
  20272. do_log(3,"smtp resp to STARTTLS: %s", $smtp_resp);
  20273. if ($smtp_resp !~ /^2/) {
  20274. (!$tls_security_level || lc($tls_security_level) eq 'may')
  20275. or die "Negative SMTP resp. to STARTTLS: $smtp_resp\n";
  20276. } else {
  20277. $smtp_handle->ssl_upgrade or die "Error upgrading socket to SSL";
  20278. $self->session_state('connected');
  20279. }
  20280. }
  20281. }
  20282. }
  20283. $self;
  20284. }
  20285. 1;
  20286. package Amavis::Out::SMTP;
  20287. use strict;
  20288. use re 'taint';
  20289. use warnings;
  20290. use warnings FATAL => qw(utf8 void);
  20291. no warnings 'uninitialized';
  20292. BEGIN {
  20293. require Exporter;
  20294. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  20295. $VERSION = '2.316';
  20296. @ISA = qw(Exporter);
  20297. @EXPORT = qw(&mail_via_smtp);
  20298. import Amavis::Conf qw(:platform c cr ca $smtp_connection_cache_enable);
  20299. import Amavis::Util qw(untaint min max minmax ll do_log snmp_count
  20300. xtext_encode xtext_decode prolong_timer
  20301. get_deadline collect_equal_delivery_recips);
  20302. import Amavis::Timing qw(section_time);
  20303. import Amavis::rfc2821_2822_Tools;
  20304. import Amavis::Lookup qw(lookup lookup2);
  20305. import Amavis::Out::EditHeader;
  20306. }
  20307. use Time::HiRes qw(time);
  20308. # use Authen::SASL;
  20309. # simple OO wrapper around Mail::DKIM::Signer to provide a method 'print'
  20310. # and to convert \n to CRLF
  20311. #
  20312. sub new_dkim_wrapper {
  20313. my($class, $handle,$strip_cr) = @_;
  20314. bless { handle => $handle, strip_cr => $strip_cr }, $class;
  20315. }
  20316. sub close { 1 }
  20317. sub print {
  20318. my $self = shift;
  20319. my $buff = @_ == 1 ? $_[0] : join('',@_);
  20320. $buff =~ tr/\r//d if $self->{strip_cr};
  20321. $buff =~ s{\n}{\015\012}gs;
  20322. $self->{handle}->PRINT($buff);
  20323. }
  20324. # Add a log_id to the SMTP status text, insert a fabricated RFC 3463 enhanced
  20325. # status code if missing in an MTA response.
  20326. #
  20327. sub enhance_smtp_response($$$$$) {
  20328. my($smtp_resp,$am_id,$mta_id,$dflt_enhcode,$cmd_name) = @_;
  20329. local($1,$2,$3,$4); my $resp_msg;
  20330. my($resp_code,$resp_more,$resp_enhcode) = ('451', ' ', '4.5.0');
  20331. if (!defined($smtp_resp) || $smtp_resp eq '') {
  20332. $smtp_resp = sprintf('No resp. to %s', $cmd_name);
  20333. } elsif ($smtp_resp !~ /^[245]\d{2}/) {
  20334. $smtp_resp = sprintf('Bad resp. to %s: %s', $cmd_name,$smtp_resp);
  20335. } elsif ($smtp_resp=~/^ (\d{3}) (\ |-|\z) [ \t]*
  20336. ([245] \. \d{1,3} \. \d{1,3})?
  20337. \s* (.*) \z/xs) {
  20338. ($resp_code, $resp_more, $resp_enhcode, $resp_msg) = ($1, $2, $3, $4);
  20339. if (!defined $resp_enhcode && $resp_code =~ /^[245]/) {
  20340. my $c = substr($resp_code,0,1);
  20341. $resp_enhcode = $dflt_enhcode; $resp_enhcode =~ s/^\d*/$c/;
  20342. }
  20343. }
  20344. sprintf("%s%s%s from MTA(%s): %s",
  20345. $resp_code, $resp_more, $resp_enhcode, $mta_id, $smtp_resp);
  20346. }
  20347. # Send mail using SMTP - single transaction
  20348. # (e.g. forwarding original mail or sending notification)
  20349. # May throw exception (die) if temporary failure (4xx) or other problem
  20350. #
  20351. # Multiple transactions may be necessary, either due to different delivery
  20352. # methods (IP address, port, SMTP vs. LMTP) or due to '452 Too many recipients'
  20353. #
  20354. sub mail_via_smtp(@) {
  20355. my($msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_;
  20356. #
  20357. # RFC 2033: LMTP protocol MUST NOT be used on the TCP port 25
  20358. #
  20359. # $initial_submission can be treated as a boolean, but for more detailed
  20360. # needs it can be any of: false: 0
  20361. # or true: 'Quar', 'Dsn', 'Notif', 'AV', 'Arf'
  20362. my $which_section = 'fwd_init';
  20363. my $logmsg = sprintf("%s from %s", $initial_submission?'SEND':'FWD',
  20364. $msginfo->sender_smtp);
  20365. my($per_recip_data_ref, $proto_sockname) =
  20366. collect_equal_delivery_recips($msginfo, $filter, qr/^(?:smtp|lmtp):/i);
  20367. if (!$per_recip_data_ref || !@$per_recip_data_ref) {
  20368. do_log(5, "%s, nothing to do", $logmsg); return 1;
  20369. }
  20370. my $proto_sockname_displ = !ref $proto_sockname ? $proto_sockname
  20371. : '(' . join(', ',@$proto_sockname) . ')';
  20372. my(@per_recip_data) = @$per_recip_data_ref; undef $per_recip_data_ref;
  20373. ll(4) && do_log(4, "about to connect to %s, %s -> %s",
  20374. $proto_sockname_displ, $logmsg,
  20375. join(',', qquote_rfc2821_local(
  20376. map($_->recip_final_addr, @per_recip_data)) ));
  20377. my $am_id = $msginfo->log_id;
  20378. my $dsn_envid = $msginfo->dsn_envid; my $dsn_ret = $msginfo->dsn_ret;
  20379. my($relayhost, $protocol, $lmtp, $mta_id, @snmp_vars);
  20380. my($smtp_session, $smtp_handle, $smtp_resp, $smtp_response);
  20381. my($any_valid_recips, $any_tempfail_recips, $pipelining,
  20382. $any_valid_recips_and_data_sent, $recips_done_by_early_fail,
  20383. $in_datasend_mode, $dsn_capable, $auth_capable) = (0) x 8;
  20384. my $mimetransport8bit_capable = 0; # RFC 1652
  20385. my(%from_options);
  20386. # RFC 5321 (ex RFC 2821), section 4.5.3.2. Timeouts
  20387. my $smtp_connect_timeout = 35;
  20388. my $smtp_helo_timeout = 300;
  20389. my $smtp_starttls_timeout = 300;
  20390. my $smtp_xforward_timeout = 300;
  20391. my $smtp_mail_timeout = 300;
  20392. my $smtp_rcpt_timeout = 300;
  20393. my $smtp_data_init_timeout = 120;
  20394. my $smtp_data_xfer_timeout = 180;
  20395. my $smtp_data_done_timeout = 600;
  20396. my $smtp_quit_timeout = 10; # 300
  20397. my $smtp_rset_timeout = 20;
  20398. # can appear anywhere in a pipelined command group:
  20399. # RSET, MAIL FROM, SEND FROM, SOML FROM, SAML FROM, RCPT TO, data
  20400. # can only appear as the last command in a pipelined group: --> flush
  20401. # EHLO, DATA, VRFY, EXPN, TURN, QUIT, NOOP,
  20402. # AUTH(RFC 4954), STARTTLS(RFC 3207), and all unknown commands
  20403. # needed to implement dynamic_destination: a '*' in place of a host or port
  20404. my($wildcard_implied_host, $wildcard_implied_port);
  20405. my $conn = $msginfo->conn_obj;
  20406. if ($conn) {
  20407. my $host = $conn->client_ip;
  20408. $wildcard_implied_host = $host if defined($host) && $host ne '';
  20409. my $port = $conn->socket_port;
  20410. $wildcard_implied_port = $port+1 if defined($port) && $port =~ /^\d+\z/;
  20411. }
  20412. my($remaining_time, $deadline) = get_deadline($which_section, 1, 0);
  20413. alarm(0); # stop the timer
  20414. my $err;
  20415. eval {
  20416. $which_section = 'fwd-connect';
  20417. $smtp_session = Amavis::Out::SMTP::Session->new($proto_sockname, $deadline,
  20418. $wildcard_implied_host, $wildcard_implied_port)
  20419. or die "Can't establish an SMTP/LMTP session with $proto_sockname_displ";
  20420. $smtp_handle = $smtp_session->smtp_handle;
  20421. if ($smtp_handle) {
  20422. $relayhost = $smtp_handle->socketname;
  20423. $protocol = $smtp_handle->protocol;
  20424. $lmtp = lc($protocol) eq 'lmtp' ? 1 : 0; # RFC 2033
  20425. $mta_id = sprintf("%s:%s", $protocol, $relayhost);
  20426. @snmp_vars = !$initial_submission ?
  20427. ('', 'Relay', 'Proto'.uc($protocol), 'Proto'.uc($protocol).'Relay')
  20428. : ('', 'Submit', 'Proto'.uc($protocol), 'Proto'.uc($protocol).'Submit',
  20429. 'Submit'.$initial_submission);
  20430. snmp_count('OutMsgs'.$_) for @snmp_vars;
  20431. }
  20432. $dsn_capable = c('propagate_dsn_if_possible') &&
  20433. defined($smtp_session->supports('DSN')); # RFC 3461
  20434. $mimetransport8bit_capable =
  20435. defined($smtp_session->supports('8BITMIME')); # RFC 1652
  20436. $pipelining = defined($smtp_session->supports('PIPELINING')); # RFC 2920
  20437. do_log(3,"No announced PIPELINING support by MTA?") if !$pipelining;
  20438. ll(5) && do_log(5,"Remote host presents itself as: %s%s%s",
  20439. $smtp_handle->domain,
  20440. $dsn_capable ? ', handles DSN' : '',
  20441. $pipelining ? ', handles PIPELINING' : '');
  20442. if ($lmtp && !$pipelining) # RFC 2033 requirements
  20443. { die "An LMTP server implementation MUST implement PIPELINING" }
  20444. if ($lmtp && !defined($smtp_session->supports('ENHANCEDSTATUSCODES')))
  20445. { die "An LMTP server implementation MUST implement ENHANCEDSTATUSCODES" }
  20446. section_time($which_section);
  20447. $which_section = 'fwd-xforward';
  20448. my $cl_ip = $msginfo->client_addr;
  20449. if (defined $cl_ip && $cl_ip ne '' &&
  20450. defined($smtp_session->supports('XFORWARD'))) {
  20451. $cl_ip = 'IPv6:'.$cl_ip if $cl_ip =~ /:.*:/ && $cl_ip !~ /^IPv6:/i;
  20452. my(%xfwd_supp_opt) = map((uc($_),1),
  20453. split(' ',$smtp_session->supports('XFORWARD')));
  20454. my(@params) = map
  20455. { my($n,$v) = @$_;
  20456. # Postfix since version 20060610 uses xtext-encoded (RFC 3461)
  20457. # strings in XCLIENT and XFORWARD attribute values, previous
  20458. # versions expected plain text with neutered special characters;
  20459. # see README_FILES/XFORWARD_README
  20460. if (defined $v && $v ne '') {
  20461. $v =~ s/[^\041-\176]/?/g; # isprint
  20462. $v =~ s/[<>()\\";\@]/?/g; # other chars that are special in hdrs
  20463. # postfix/src/smtpd/smtpd.c NEUTER_CHARACTERS
  20464. $v = xtext_encode($v);
  20465. $v = substr($v,0,255) if length($v) > 255; # chop xtext, not nice
  20466. }
  20467. !defined $v || $v eq '' || !$xfwd_supp_opt{$n} ? () : ("$n=$v") }
  20468. ( ['ADDR',$cl_ip], ['NAME',$msginfo->client_name],
  20469. ['PORT',$msginfo->client_port], ['PROTO',$msginfo->client_proto],
  20470. ['HELO',$msginfo->client_helo], ['SOURCE',$msginfo->client_source],
  20471. ['IDENT',$msginfo->queue_id] );
  20472. $smtp_session->timeout(
  20473. max(60,min($smtp_xforward_timeout,$deadline-time())));
  20474. $smtp_handle->command('XFORWARD',@params); #flush!
  20475. $smtp_resp = $smtp_session->smtp_response; # fetch response to XFORWARD
  20476. if (!defined $smtp_resp || $smtp_resp eq '') {
  20477. do_log(-1,"%s SMTP resp. to XFORWARD, dt: %.3f s",
  20478. !defined $smtp_resp ? 'No' : 'Empty',
  20479. time - $smtp_handle->last_io_event_tx_timestamp);
  20480. } elsif ($smtp_resp !~ /^2/) {
  20481. do_log(0,"Negative SMTP resp. to XFORWARD: %s", $smtp_resp);
  20482. } else { # success, $smtp_resp =~ /^2/
  20483. do_log(3,"smtp resp to XFORWARD: %s", $smtp_resp);
  20484. }
  20485. section_time($which_section);
  20486. }
  20487. $which_section = 'fwd-auth';
  20488. my $auth_user = $msginfo->auth_user;
  20489. my $mechanisms = $smtp_session->supports('AUTH');
  20490. if (!c('auth_required_out')) {
  20491. do_log(3,"AUTH not needed, user='%s', MTA offers '%s'",
  20492. $auth_user,$mechanisms);
  20493. } elsif ($mechanisms eq '') {
  20494. do_log(3,"INFO: MTA does not offer AUTH capability, user='%s'",
  20495. $auth_user);
  20496. } elsif (!defined $auth_user) {
  20497. do_log(0,"INFO: AUTH needed for submission but AUTH data not available");
  20498. } else {
  20499. do_log(3,"INFO: authenticating %s, server supports AUTH %s",
  20500. $auth_user,$mechanisms);
  20501. $auth_capable = 1;
  20502. # my $sasl = Authen::SASL->new(
  20503. # 'callback' => { 'user' => $auth_user, 'authname' => $auth_user,
  20504. # 'pass' => $msginfo->auth_pass });
  20505. # $smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n";#flush
  20506. do_log(0,"Sorry, AUTH not supported in this version of amavisd!");
  20507. section_time($which_section);
  20508. }
  20509. $which_section = 'fwd-mail-from';
  20510. $smtp_session->timeout(max(60,min($smtp_mail_timeout,$deadline-time())));
  20511. my $fetched_mail_resp = 0; my $fetched_rcpt_resp = 0;
  20512. my $data_command_accepted = 0;
  20513. if ($initial_submission && $dsn_capable && !defined($dsn_envid)) {
  20514. # ENVID identifies transaction, not a message
  20515. $dsn_envid = xtext_encode(sprintf("AM.%s.%s\@%s",
  20516. $msginfo->mail_id || $msginfo->log_id,
  20517. iso8601_utc_timestamp(time), c('myhostname')));
  20518. }
  20519. my $submitter = $msginfo->auth_submitter;
  20520. my $btype = $msginfo->body_type;
  20521. $from_options{'BODY'} = uc($btype) if $mimetransport8bit_capable
  20522. && defined($btype) && $btype ne '';
  20523. $from_options{'RET'} = $dsn_ret if $dsn_capable && defined $dsn_ret;
  20524. $from_options{'ENVID'} = $dsn_envid if $dsn_capable && defined $dsn_envid;
  20525. $from_options{'AUTH'} = xtext_encode($submitter) # RFC 4954 (ex RFC 2554)
  20526. if $auth_capable &&
  20527. defined($submitter) && $submitter ne '' && $submitter ne '<>';
  20528. my $faddr = $msginfo->sender_smtp;
  20529. $smtp_handle->mail($faddr, %from_options); # MAIL FROM
  20530. # consider the transaction state unknown until we see a response
  20531. $smtp_session->transaction_begins_unconfirmed; # also counts transactions
  20532. if (!$pipelining) {
  20533. $smtp_resp = $smtp_session->smtp_response; $fetched_mail_resp = 1;
  20534. if (!defined $smtp_resp || $smtp_resp eq '') {
  20535. die sprintf("%s response to MAIL, dt: %.3f s\n",
  20536. !defined $smtp_resp ? 'No' : 'Empty',
  20537. time - $smtp_handle->last_io_event_tx_timestamp);
  20538. } elsif ($smtp_resp =~ /^2/) {
  20539. do_log(3, "smtp resp to MAIL: %s", $smtp_resp);
  20540. $smtp_session->transaction_begins; # transaction is active
  20541. } else { # failure
  20542. do_log(1, "smtp resp to MAIL: %s", $smtp_resp);
  20543. # transaction state unchanged, consider it unknown
  20544. my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id,
  20545. '.1.0','MAIL FROM');
  20546. for my $r (@per_recip_data) {
  20547. next if $r->recip_done;
  20548. $r->recip_remote_mta($relayhost);
  20549. $r->recip_remote_mta_smtp_response($smtp_resp);
  20550. $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2);
  20551. }
  20552. $recips_done_by_early_fail = 1;
  20553. }
  20554. }
  20555. section_time($which_section) if !$pipelining; # otherwise it just shows 0
  20556. $which_section = 'fwd-rcpt-to';
  20557. $smtp_session->timeout(max(60,min($smtp_rcpt_timeout,$deadline-time())));
  20558. my($skipping_resp, @per_recip_data_rcpt_sent);
  20559. for my $r (@per_recip_data) { # send recipient addresses
  20560. next if $r->recip_done;
  20561. if (defined $skipping_resp) {
  20562. $r->recip_smtp_response($skipping_resp); $r->recip_done(2);
  20563. next;
  20564. }
  20565. # prepare to send a RCPT TO command
  20566. my $raddr = qquote_rfc2821_local($r->recip_final_addr);
  20567. if (!$dsn_capable) {
  20568. $smtp_handle->recipient($raddr); # a barebones RCPT TO command
  20569. } else { # include dsn options with a RCPT TO command
  20570. my(@dsn_notify); # implies a default when the list is empty
  20571. my $dn = $r->dsn_notify;
  20572. @dsn_notify = @$dn if $dn && $msginfo->sender ne ''; # if nondefault
  20573. if (c('terminate_dsn_on_notify_success')) {
  20574. # we want to handle option SUCCESS locally
  20575. if (grep($_ eq 'SUCCESS', @dsn_notify)) { # strip out SUCCESS
  20576. @dsn_notify = grep($_ ne 'SUCCESS', @dsn_notify);
  20577. @dsn_notify = ('NEVER') if !@dsn_notify;
  20578. do_log(3,"stripped out SUCCESS, result: NOTIFY=%s",
  20579. join(',',@dsn_notify));
  20580. }
  20581. }
  20582. my(%rcpt_options);
  20583. $rcpt_options{'NOTIFY'} =
  20584. join(',', map(uc($_),@dsn_notify)) if @dsn_notify;
  20585. $rcpt_options{'ORCPT'} = $r->dsn_orcpt if defined $r->dsn_orcpt;
  20586. $smtp_handle->recipient($raddr, %rcpt_options); # RCPT TO
  20587. }
  20588. push(@per_recip_data_rcpt_sent, $r); # remember which recips were sent
  20589. if (!$pipelining) { # must fetch responses to RCPT TO right away
  20590. $smtp_resp = $smtp_session->smtp_response; $fetched_rcpt_resp = 1;
  20591. if (defined $smtp_resp && $smtp_resp ne '') {
  20592. $r->recip_remote_mta($relayhost);
  20593. $r->recip_remote_mta_smtp_response($smtp_resp);
  20594. my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id,
  20595. '.1.0','RCPT TO');
  20596. $r->recip_smtp_response($smtp_resp_ext); # preliminary response
  20597. }
  20598. if (!defined $smtp_resp || $smtp_resp eq '') {
  20599. die sprintf("%s response to RCPT (%s), dt: %.3f s\n",
  20600. !defined $smtp_resp ? 'No' : 'Empty', $raddr,
  20601. time - $smtp_handle->last_io_event_tx_timestamp);
  20602. } elsif ($smtp_resp =~ /^2/) {
  20603. do_log(3, "smtp resp to RCPT (%s): %s", $raddr,$smtp_resp);
  20604. $any_valid_recips++;
  20605. } else { # failure
  20606. do_log(1, "smtp resp to RCPT (%s): %s", $raddr,$smtp_resp);
  20607. if ($smtp_resp =~ /^452/) { # too many recipients - see RFC 5321
  20608. do_log(-1, 'Only %d recips sent in one go: "%s"',
  20609. $any_valid_recips, $smtp_resp)
  20610. if !defined $skipping_resp;
  20611. $skipping_resp = enhance_smtp_response($smtp_resp,$am_id,$mta_id,
  20612. '.5.3','RCPT TO');
  20613. } elsif ($smtp_resp =~ /^4/) {
  20614. $any_tempfail_recips++;
  20615. }
  20616. $r->recip_done(2); # got a negative response to RCPT TO
  20617. }
  20618. }
  20619. }
  20620. section_time($which_section) if !$pipelining; # otherwise it just shows 0
  20621. my $what_cmd;
  20622. if (!@per_recip_data_rcpt_sent || # no recipients were sent
  20623. $fetched_rcpt_resp && !$any_valid_recips) { # no recipients accepted
  20624. # it is known there are no valid recipients, don't go into DATA section
  20625. do_log(0,"no valid recipients, skip data transfer");
  20626. $smtp_session->timeout($smtp_rset_timeout);
  20627. $what_cmd = 'RSET'; $smtp_handle->rset; # send a RSET
  20628. $smtp_session->transaction_ends_unconfirmed;
  20629. } elsif ($fetched_rcpt_resp && # no pipelining
  20630. $any_tempfail_recips && !$dsn_per_recip_capable) {
  20631. # we must not proceed if mail did not came in as LMTP,
  20632. # or we would generate mail duplicates on each delivery attempt
  20633. do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: %s",
  20634. $any_tempfail_recips);
  20635. $smtp_session->timeout($smtp_rset_timeout);
  20636. $what_cmd = 'RSET'; $smtp_handle->rset; # send a RSET
  20637. $smtp_session->transaction_ends_unconfirmed;
  20638. } else { # pipelining, or we know we got a clearance to proceed
  20639. $which_section = 'fwd-data-cmd';
  20640. # pipelining in effect, or we have at least one valid recipient, go DATA
  20641. $smtp_session->timeout(
  20642. max(60,min($smtp_data_init_timeout,$deadline-time())));
  20643. $smtp_handle->data; #flush! DATA
  20644. $in_datasend_mode = 1; # DATA command was sent (but not yet confirmed)
  20645. if (!$fetched_mail_resp) { # pipelining in effect, late response to MAIL
  20646. $which_section = 'fwd-mail-pip';
  20647. $smtp_session->timeout(
  20648. max(60,min($smtp_mail_timeout,$deadline-time())));
  20649. $smtp_resp = $smtp_session->smtp_response; $fetched_mail_resp = 1;
  20650. if (!defined $smtp_resp || $smtp_resp eq '') {
  20651. die sprintf("%s response to MAIL (pip), dt: %.3f s\n",
  20652. !defined $smtp_resp ? 'No' : 'Empty',
  20653. time - $smtp_handle->last_io_event_tx_timestamp);
  20654. } elsif ($smtp_resp =~ /^2/) {
  20655. do_log(3, "smtp resp to MAIL (pip): %s", $smtp_resp);
  20656. $smtp_session->transaction_begins; # transaction is active
  20657. } else { # failure
  20658. do_log(1, "smtp resp to MAIL (pip): %s", $smtp_resp);
  20659. # transaction state unchanged, consider it unknown
  20660. my $smtp_resp_ext = enhance_smtp_response($smtp_resp,
  20661. $am_id, $mta_id, '.1.0', 'MAIL FROM');
  20662. for my $r (@per_recip_data) {
  20663. next if $r->recip_done;
  20664. $r->recip_remote_mta($relayhost);
  20665. $r->recip_remote_mta_smtp_response($smtp_resp);
  20666. $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2);
  20667. }
  20668. $recips_done_by_early_fail = 1;
  20669. }
  20670. section_time($which_section);
  20671. }
  20672. if (!$fetched_rcpt_resp) { # pipelining in effect, late response to RCPT
  20673. $which_section = 'fwd-rcpt-pip';
  20674. $smtp_session->timeout(
  20675. max(60,min($smtp_rcpt_timeout,$deadline-time())));
  20676. for my $r (@per_recip_data_rcpt_sent) { # only for those actually sent
  20677. my $raddr = qquote_rfc2821_local($r->recip_final_addr);
  20678. $smtp_resp = $smtp_session->smtp_response; $fetched_rcpt_resp = 1;
  20679. if (defined $smtp_resp && $smtp_resp ne '') {
  20680. if ($r->recip_done) { # shouldn't happen, unless MAIL FROM failed
  20681. do_log(-1,"panic: recipient done, but got an ".
  20682. "smtp resp to RCPT (pip) (%s): %s",
  20683. $raddr,$smtp_resp) if !$recips_done_by_early_fail;
  20684. next; # do not overwrite previous diagnostics
  20685. }
  20686. $r->recip_remote_mta($relayhost);
  20687. $r->recip_remote_mta_smtp_response($smtp_resp);
  20688. my $smtp_resp_ext = enhance_smtp_response($smtp_resp,
  20689. $am_id, $mta_id, '.1.0', 'RCPT TO');
  20690. $r->recip_smtp_response($smtp_resp_ext); # preliminary response
  20691. }
  20692. if (!defined $smtp_resp || $smtp_resp eq '') {
  20693. die sprintf("%s response to RCPT (pip) (%s), dt: %.3f s\n",
  20694. !defined $smtp_resp ? 'No' : 'Empty', $raddr,
  20695. time - $smtp_handle->last_io_event_tx_timestamp);
  20696. } elsif ($smtp_resp =~ /^2/) {
  20697. do_log(3, "smtp resp to RCPT (pip) (%s): %s", $raddr,$smtp_resp);
  20698. $any_valid_recips++;
  20699. } else { # failure
  20700. do_log(1, "smtp resp to RCPT (pip) (%s): %s", $raddr,$smtp_resp);
  20701. if ($smtp_resp =~ /^452/) { # too many recipients - see RFC 5321
  20702. do_log(-1, 'Only %d recips sent in one go: "%s"',
  20703. $any_valid_recips, $smtp_resp)
  20704. if !defined $skipping_resp;
  20705. $skipping_resp = enhance_smtp_response($smtp_resp,$am_id,$mta_id,
  20706. '.5.3','RCPT TO');
  20707. } elsif ($smtp_resp =~ /^4/) {
  20708. $any_tempfail_recips++;
  20709. }
  20710. $r->recip_done(2); # got a negative response to RCPT TO
  20711. }
  20712. }
  20713. section_time($which_section);
  20714. }
  20715. $which_section = 'fwd-data-chkpnt' if $pipelining;
  20716. $smtp_session->timeout(
  20717. max(60,min($smtp_data_init_timeout,$deadline-time())));
  20718. $smtp_resp = $smtp_session->smtp_response; # fetch response to DATA
  20719. section_time($which_section);
  20720. $data_command_accepted = 0;
  20721. if (!defined $smtp_resp || $smtp_resp eq '') {
  20722. do_log(-1,"%s SMTP resp. to DATA, dt: %.3f s",
  20723. !defined $smtp_resp ? 'No' : 'Empty',
  20724. time - $smtp_handle->last_io_event_tx_timestamp);
  20725. } elsif ($smtp_resp !~ /^3/) {
  20726. do_log(0,"Negative SMTP resp. to DATA: %s", $smtp_resp);
  20727. } else { # success, $smtp_resp =~ /^3/
  20728. $data_command_accepted = 1;
  20729. do_log(3,"smtp resp to DATA: %s", $smtp_resp);
  20730. }
  20731. if (!$data_command_accepted) {
  20732. $in_datasend_mode = 0;
  20733. $smtp_session->timeout($smtp_rset_timeout);
  20734. $what_cmd = 'RSET'; $smtp_handle->rset; # send a RSET
  20735. $smtp_session->transaction_ends_unconfirmed;
  20736. } elsif (!$any_valid_recips) { # pipelining and no recipients, in DATA
  20737. do_log(2,"Too late, DATA accepted but no valid recips, send dummy");
  20738. $which_section = 'fwd-dummydata-end';
  20739. $smtp_session->timeout(
  20740. max(60,min($smtp_data_done_timeout,$deadline-time())));
  20741. $what_cmd = 'data-dot';
  20742. $smtp_handle->dataend; # .<CR><LF> as required by RFC 2920: if a DATA
  20743. # command was accepted the SMTP client should send a single dot
  20744. $in_datasend_mode = 0; $smtp_session->transaction_ends_unconfirmed;
  20745. } elsif ($any_tempfail_recips && !$dsn_per_recip_capable) { # pipelining
  20746. # we must not proceed if mail did not came in as LMTP,
  20747. # or we would generate mail duplicates on each delivery attempt
  20748. do_log(2,"Too late, DATA accepted but tempfailed recips, bail out");
  20749. die "Bail out, DATA accepted but tempfailed recips, not an LMTP input";
  20750. } else { # all ok so far, we are in a DATA state and must send contents
  20751. $which_section = 'fwd-data-hdr';
  20752. my $hdr_edits = $msginfo->header_edits;
  20753. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  20754. $smtp_session->timeout(
  20755. max(60,min($smtp_data_xfer_timeout,$deadline-time())));
  20756. my($received_cnt,$file_position) =
  20757. $hdr_edits->write_header($msginfo,$smtp_handle,!$initial_submission);
  20758. if ($received_cnt > 100) {
  20759. # loop detection required by RFC 5321 (ex RFC 2821) section 6.3
  20760. # Do not modify the signal text, it gets matched elsewhere!
  20761. die "Too many hops: $received_cnt 'Received:' header fields\n";
  20762. }
  20763. $which_section = 'fwd-data-contents';
  20764. # a file handle or a string ref or MIME::Entity object
  20765. my $msg = $msginfo->mail_text;
  20766. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  20767. $msg = $msg_str_ref if ref $msg_str_ref;
  20768. if (!defined $msg) {
  20769. # empty mail
  20770. } elsif (ref $msg eq 'SCALAR') {
  20771. # do it in chunks, saves memory, cache friendly
  20772. while ($file_position < length($$msg)) {
  20773. $smtp_handle->datasend(substr($$msg,$file_position,16384));
  20774. $file_position += 16384; # may overshoot, no problem
  20775. }
  20776. } elsif ($msg->isa('MIME::Entity')) {
  20777. $msg->print_body($smtp_handle);
  20778. } else {
  20779. my($nbytes,$buff);
  20780. while (($nbytes = $msg->read($buff,65536)) > 0) {
  20781. $smtp_handle->datasend($buff);
  20782. }
  20783. defined $nbytes or die "Error reading: $!";
  20784. }
  20785. section_time($which_section);
  20786. $which_section = 'fwd-data-end';
  20787. $smtp_session->timeout(
  20788. max(60,min($smtp_data_done_timeout,$deadline-time())));
  20789. $what_cmd = 'data-dot';
  20790. $smtp_handle->dataend; # .<CR><LF>
  20791. $in_datasend_mode = 0; $smtp_session->transaction_ends_unconfirmed;
  20792. $any_valid_recips_and_data_sent = 1;
  20793. section_time($which_section) if !$pipelining; # otherwise it shows 0
  20794. }
  20795. }
  20796. if ($pipelining && !$smtp_connection_cache_enable) {
  20797. do_log(5,"smtp connection_cache disabled, sending QUIT");
  20798. $smtp_session->quit; #flush! QUIT
  20799. # can't be sure until we see a response, consider uncertain just in case
  20800. $smtp_session->transaction_ends_unconfirmed;
  20801. }
  20802. $which_section = 'fwd-rundown-1';
  20803. $smtp_resp = undef;
  20804. if (!defined $what_cmd) {
  20805. # not expecting a response?
  20806. } elsif ($what_cmd ne 'data-dot') { # must be a response to a RSET
  20807. $smtp_resp = $smtp_session->smtp_response; # fetch a response
  20808. if (!defined $smtp_resp || $smtp_resp eq '') {
  20809. die sprintf("%s SMTP response to %s, dt: %.3f s",
  20810. !defined $smtp_resp ? 'No' : 'Empty', $what_cmd,
  20811. time - $smtp_handle->last_io_event_tx_timestamp);
  20812. } elsif ($smtp_resp !~ /^2/) {
  20813. die "Negative SMTP response to $what_cmd: $smtp_resp";
  20814. } else { # success, $smtp_resp =~ /^2/
  20815. do_log(3,"smtp resp to %s: %s", $what_cmd,$smtp_resp);
  20816. $smtp_session->transaction_ends if $what_cmd eq 'RSET';
  20817. }
  20818. } else { # get response(s) to data-dot
  20819. # replace success responses to RCPT TO commands with a final response
  20820. my $first = 1; my $anyfail = 0; my $anysucc = 0;
  20821. for my $r (@per_recip_data_rcpt_sent) { # only for those actually sent
  20822. if ($lmtp || $first) {
  20823. $first = 0; my $raddr = qquote_rfc2821_local($r->recip_final_addr);
  20824. $raddr .= ', etc.' if !$lmtp && @per_recip_data > 1;
  20825. $smtp_resp = $smtp_session->smtp_response; # resp to data-dot
  20826. if (!defined $smtp_resp || $smtp_resp eq '') {
  20827. $anyfail = 1;
  20828. do_log(0,"%s SMTP response to %s (%s), dt: %.3f s",
  20829. !defined $smtp_resp ? 'No' : 'Empty', $what_cmd, $raddr,
  20830. time - $smtp_handle->last_io_event_tx_timestamp);
  20831. } elsif ($smtp_resp !~ /^2/) {
  20832. $anyfail = 1;
  20833. do_log(0,"Negative SMTP response to %s (%s): %s, dt: %.1f ms",
  20834. $what_cmd, $raddr, $smtp_resp,
  20835. 1000*(time-$smtp_handle->last_io_event_tx_timestamp));
  20836. } else { # success, $smtp_resp =~ /^2/
  20837. $anysucc = 1;
  20838. ll(3) && do_log(3,"smtp resp to %s (%s): %s, dt: %.1f ms",
  20839. $what_cmd, $raddr, $smtp_resp,
  20840. 1000*(time-$smtp_handle->last_io_event_tx_timestamp));
  20841. }
  20842. }
  20843. next if $r->recip_done; # skip those that failed at earlier stages
  20844. $r->recip_remote_mta($relayhost);
  20845. $r->recip_remote_mta_smtp_response($smtp_resp);
  20846. my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id,
  20847. '.6.0','data-dot');
  20848. $smtp_response = $smtp_resp_ext if !defined $smtp_response;
  20849. $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2);
  20850. $r->recip_mbxname($r->recip_final_addr) if $smtp_resp =~ /^2/;
  20851. }
  20852. if ($first) { # fetch an uncollected response
  20853. # fetch unprocessed response if all recipients were rejected
  20854. # but we nevertheless somehow entered a data transfer mode
  20855. # (i.e. if an SMTP server failed to reject a DATA command).
  20856. # RFC 2033: when there have been no successful RCPT commands in the
  20857. # mail transaction, the DATA command MUST fail with a 503 reply code
  20858. $smtp_resp = $smtp_session->smtp_response; # resp to data-dot
  20859. $smtp_resp = '' if !defined $smtp_resp;
  20860. if ($smtp_resp =~ /^2/) { $anysucc = 1 } else { $anyfail = 1 }
  20861. do_log(3,"smtp resp to _dummy_ data %s: %s", $what_cmd,$smtp_resp);
  20862. }
  20863. if ($anysucc && !$anyfail) {
  20864. # we are certain all went fine and a transaction is definitely over
  20865. $smtp_session->transaction_ends;
  20866. }
  20867. }
  20868. # if ($pipelining) {} # QUIT was already sent
  20869. # elsif (!$smtp_connection_cache_enable) {
  20870. # $smtp_session->quit; #flush! QUIT
  20871. # # can't be sure until we see a response, consider uncertain just in case
  20872. # $smtp_session->transaction_ends_unconfirmed;
  20873. # }
  20874. # if ($smtp_session->session_state eq 'quitsent') {
  20875. # $smtp_session->timeout($smtp_quit_timeout);
  20876. # $smtp_resp = $smtp_session->smtp_response;
  20877. # $smtp_resp = '' if !defined $smtp_resp;
  20878. # do_log(3,"smtp resp to QUIT: %s", $smtp_resp);
  20879. # if ($smtp_resp =~ /^2/) {
  20880. # $smtp_session->transaction_ends;
  20881. # } else {
  20882. # $smtp_session->transaction_ends_unconfirmed;
  20883. # do_log(0,"Negative SMTP resp. to QUIT: %s", $smtp_resp);
  20884. # }
  20885. # }
  20886. my $keep_session = $smtp_session->session_state ne 'quitsent';
  20887. if ($keep_session && !defined($smtp_session->in_smtp_transaction)) {
  20888. do_log(2,"SMTP transaction state uncertain, closing just in case");
  20889. $keep_session = 0;
  20890. }
  20891. $smtp_session->close($keep_session)
  20892. or die "Error closing Amavis::Out::SMTP::Session";
  20893. undef $smtp_handle; undef $smtp_session;
  20894. 1;
  20895. # some unusual error conditions _are_ captured by eval, but fail to set $@
  20896. } or do { $err = $@ ne '' ? $@ : "errno=$!"; chomp $err };
  20897. my $saved_section_name = $which_section;
  20898. $which_section = 'fwd-end-chkpnt';
  20899. do_log(2,"mail_via_smtp: session failed: %s", $err) if defined $err;
  20900. prolong_timer($which_section); # restart timer
  20901. # terminate the SMTP session if still alive
  20902. if (!defined($smtp_session)) {
  20903. # already closed normally
  20904. } elsif ($in_datasend_mode) {
  20905. # We are aborting SMTP session. Data transfer mode must NOT be terminated
  20906. # with a dataend (dot), otherwise recipient will receive a chopped-off mail
  20907. # (and possibly be receiving it over and over again during each MTA retry.
  20908. do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, %s", $err);
  20909. $smtp_session->close(0); # abruptly terminate SMTP session, ignore status
  20910. } else {
  20911. do_log(5,"smtp session done, sending QUIT");
  20912. eval {
  20913. $smtp_session->timeout(1); # don't wait for too long
  20914. $smtp_session->quit; #flush! # send a QUIT regardless of success so far
  20915. $smtp_session->transaction_ends_unconfirmed;
  20916. for (my $cnt=0; ; $cnt++) { # curious if there are any pending responses
  20917. my $smtp_resp = $smtp_session->smtp_response;
  20918. last if !defined $smtp_resp;
  20919. do_log(0,"discarding unprocessed reply: %s", $smtp_resp);
  20920. if ($cnt > 20) { do_log(-1,"aborting, discarding many replies"); last }
  20921. }
  20922. } or do {
  20923. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  20924. do_log(-1, "mail_via_smtp: error during QUIT: %s", $eval_stat);
  20925. };
  20926. $smtp_session->close(0); # terminate SMTP session, ignore status
  20927. }
  20928. undef $smtp_handle; undef $smtp_session;
  20929. # prepare final smtp response and log abnormal events
  20930. for my $r (@per_recip_data) {
  20931. my $resp = $r->recip_smtp_response;
  20932. $smtp_response = $resp if !defined($smtp_response) ||
  20933. $resp =~ /^4/ && $smtp_response !~ /^4/ ||
  20934. $resp !~ /^2/ && $smtp_response !~ /^[45]/;
  20935. }
  20936. if (!defined $err) {
  20937. # no errors
  20938. } elsif ($err =~ /^timed out\b/ || $err =~ /: Timeout\z/) {
  20939. $smtp_response = sprintf("450 4.4.2 Timed out during %s, MTA(%s), id=%s",
  20940. $saved_section_name, $mta_id, $am_id);
  20941. } elsif ($err =~ /^Can't connect\b/) {
  20942. $smtp_response = sprintf("450 4.4.1 %s, MTA(%s), id=%s",
  20943. $err, $mta_id, $am_id);
  20944. } elsif ($err =~ /^Too many hops\b/) {
  20945. $smtp_response = sprintf("554 5.4.6 Reject: %s, id=%s", $err, $am_id);
  20946. } else {
  20947. $smtp_response = sprintf("451 4.5.0 From MTA(%s) during %s (%s): id=%s",
  20948. $mta_id, $saved_section_name, $err, $am_id);
  20949. }
  20950. # NOTE: $initial_submission argument is typically treated as a boolean
  20951. # but a value of 'AV' is supplied by av_smtp_client to allow a forwarding
  20952. # method to distinguish it from ordinary submissions
  20953. my $ll = ($smtp_response =~ /^2/ || $initial_submission eq 'AV') ? 1 : -1;
  20954. ll($ll) && do_log($ll, "%s -> %s,%s %s", $logmsg,
  20955. join(',', qquote_rfc2821_local(
  20956. map($_->recip_final_addr, @per_recip_data))),
  20957. join(' ', map { my $v=$from_options{$_}; defined($v)?"$_=$v":"$_" }
  20958. (keys %from_options)),
  20959. $smtp_response);
  20960. if (defined $smtp_response) {
  20961. $msginfo->dsn_passed_on($dsn_capable && $smtp_response=~/^2/ &&
  20962. !c('terminate_dsn_on_notify_success') ? 1 : 0);
  20963. for my $r (@per_recip_data) {
  20964. # attach an SMTP response to each recip that was not already processed
  20965. if (!$r->recip_done) { # mark it as done
  20966. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  20967. $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/;
  20968. } elsif ($any_valid_recips_and_data_sent &&
  20969. $r->recip_smtp_response =~ /^452/) {
  20970. # 'undo' the RCPT TO '452 Too many recipients' situation,
  20971. # mail needs to be transferred in more than one transaction
  20972. $r->recip_smtp_response(undef); $r->recip_done(undef);
  20973. }
  20974. }
  20975. if ($smtp_response =~ /^2/) {
  20976. snmp_count('OutMsgsDelivers');
  20977. my $size = $msginfo->msg_size;
  20978. snmp_count( ['OutMsgsSize'.$_, $size, 'C64'] ) for @snmp_vars;
  20979. } elsif ($smtp_response =~ /^4/) {
  20980. snmp_count('OutMsgsAttemptFails');
  20981. } elsif ($smtp_response =~ /^5/) {
  20982. snmp_count('OutMsgsRejects');
  20983. }
  20984. }
  20985. section_time($which_section);
  20986. die $err if defined($err) && $err =~ /^timed out\b/; # resignal timeout
  20987. 1;
  20988. }
  20989. 1;
  20990. __DATA__
  20991. #
  20992. package Amavis::Out::Pipe;
  20993. use strict;
  20994. use re 'taint';
  20995. use warnings;
  20996. use warnings FATAL => qw(utf8 void);
  20997. no warnings 'uninitialized';
  20998. BEGIN {
  20999. require Exporter;
  21000. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  21001. $VERSION = '2.316';
  21002. @ISA = qw(Exporter);
  21003. @EXPORT = qw(&mail_via_pipe);
  21004. import Amavis::Conf qw(:platform c cr ca);
  21005. import Amavis::Util qw(untaint min max minmax ll do_log snmp_count
  21006. collect_equal_delivery_recips);
  21007. import Amavis::ProcControl qw(exit_status_str proc_status_ok kill_proc
  21008. run_command_consumer);
  21009. import Amavis::Timing qw(section_time);
  21010. import Amavis::rfc2821_2822_Tools;
  21011. import Amavis::Out::EditHeader;
  21012. }
  21013. use Errno qw(ENOENT EACCES ESRCH);
  21014. use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
  21015. WEXITSTATUS WTERMSIG WSTOPSIG);
  21016. # Send mail using external mail submission program 'sendmail' or its lookalike
  21017. # (also available with Postfix and Exim) - used for forwarding original mail
  21018. # or sending notifications or quarantining. May throw exception (die) on
  21019. # temporary failure (4xx) or other problem.
  21020. #
  21021. sub mail_via_pipe(@) {
  21022. my($msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_;
  21023. my(@snmp_vars) = !$initial_submission ?
  21024. ('', 'Relay', 'ProtoPipe', 'ProtoPipeRelay')
  21025. : ('', 'Submit', 'ProtoPipe', 'ProtoPipeSubmit',
  21026. 'Submit'.$initial_submission);
  21027. snmp_count('OutMsgs'.$_) for @snmp_vars;
  21028. my $logmsg = sprintf("%s via PIPE: %s", ($initial_submission?'SEND':'FWD'),
  21029. $msginfo->sender_smtp);
  21030. my($per_recip_data_ref, $proto_sockname) =
  21031. collect_equal_delivery_recips($msginfo, $filter, qr/^pipe:/i);
  21032. if (!$per_recip_data_ref || !@$per_recip_data_ref) {
  21033. do_log(5, "%s, nothing to do", $logmsg); return 1;
  21034. }
  21035. $proto_sockname = $proto_sockname->[0] if ref $proto_sockname;
  21036. ll(1) && do_log(1, "delivering to %s, %s -> %s",
  21037. $proto_sockname, $logmsg,
  21038. join(',', qquote_rfc2821_local(
  21039. map($_->recip_final_addr, @$per_recip_data_ref)) ));
  21040. # just use the first one, ignoring failover alternatives
  21041. local($1);
  21042. $proto_sockname =~ /^pipe:(.*)\z/si
  21043. or die "Bad fwd method syntax: ".$proto_sockname;
  21044. my $pipe_args = $1;
  21045. $pipe_args =~ s/^flags=\S*\s*//i; # flags are currently ignored, q implied
  21046. $pipe_args =~ s/^argv=//i;
  21047. my(@pipe_args) = split(' ',$pipe_args); my(@command) = shift(@pipe_args);
  21048. my $dsn_capable = c('propagate_dsn_if_possible'); # assume, unless disabled
  21049. $dsn_capable = 0 if $command[0] !~ /sendmail/; # a hack, don't use -N or -V
  21050. if ($dsn_capable) { # DSN is supported since Postfix 2.3
  21051. # notify options are per-recipient, yet a command option -N applies to all
  21052. my $common_list; my $not_all_the_same = 0;
  21053. for my $r (@{$msginfo->per_recip_data}) {
  21054. my $dsn_notify = $r->dsn_notify;
  21055. my $d;
  21056. if ($msginfo->sender eq '') {
  21057. $d = 'NEVER';
  21058. } elsif (!$dsn_notify) {
  21059. $d = 'DELAY,FAILURE'; # sorted
  21060. } else {
  21061. $d = uc(join(',', sort @$dsn_notify)); # normalize order
  21062. }
  21063. if (!defined($common_list)) { $common_list = $d }
  21064. elsif ($d ne $common_list) { $not_all_the_same = 1 }
  21065. }
  21066. if ($common_list=~/\bSUCCESS\b/ && c('terminate_dsn_on_notify_success')) {
  21067. # strip out option SUCCESS, we want to handle it locally
  21068. my(@dsn_notify) = grep($_ ne 'SUCCESS', split(/,/,$common_list));
  21069. @dsn_notify = ('NEVER') if !@dsn_notify;
  21070. $common_list = join(',',@dsn_notify);
  21071. do_log(3,"stripped out SUCCESS, result: NOTIFY=%s", $common_list);
  21072. }
  21073. if ($not_all_the_same || $msginfo->sender eq '') {} # leave at default
  21074. elsif ($common_list eq 'DELAY,FAILURE') {} # leave at default
  21075. else { unshift(@pipe_args, '-N', $common_list) }
  21076. unshift(@pipe_args,
  21077. '-V', $msginfo->dsn_envid) if defined $msginfo->dsn_envid;
  21078. # but there is no mechanism to specify ORCPT or RET
  21079. }
  21080. for (@pipe_args) {
  21081. # The sendmail command line expects addresses quoted as per RFC 822.
  21082. # "funny user"@some.domain
  21083. # For compatibility with Sendmail, the Postfix sendmail command line also
  21084. # accepts address formats that are legal in RFC 822 mail header section:
  21085. # Funny Dude <"funny user"@some.domain>
  21086. # Although addresses passed as args to sendmail submission program
  21087. # should not be <...> bracketed, for some reason original sendmail
  21088. # issues a warning on null reverse-path, but gladly accepts <>.
  21089. # As this is not strictly wrong, we comply to make it happy.
  21090. # NOTE: the -fsender is not allowed, -f and sender must be separate args!
  21091. my $null_ret_path = '<>'; # some sendmail lookalikes want '<>', others ''
  21092. # Courier sendmail accepts '' but not '<>' for null reverse path
  21093. $null_ret_path = '' if $Amavis::extra_code_in_courier;
  21094. if (/^\$\{sender\}\z/i) {
  21095. push(@command, $msginfo->sender eq '' ? $null_ret_path
  21096. : do { my $s = $msginfo->sender_smtp;
  21097. $s =~ s/^<//; $s =~ s/>\z//; untaint($s) });
  21098. } elsif (/^\$\{recipient\}\z/i) {
  21099. push(@command,
  21100. map { $_ eq '' ? $null_ret_path : untaint(quote_rfc2821_local($_)) }
  21101. map($_->recip_final_addr, @$per_recip_data_ref));
  21102. } else {
  21103. push(@command, $_);
  21104. }
  21105. }
  21106. ll(5) && do_log(5, "mail_via_pipe running command: %s", join(' ',@command));
  21107. local $SIG{CHLD} = 'DEFAULT';
  21108. local $SIG{PIPE} = 'IGNORE'; # don't signal on a write to a widowed pipe
  21109. my($proc_fh,$pid) = run_command_consumer(undef,undef,@command);
  21110. my $hdr_edits = $msginfo->header_edits;
  21111. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  21112. my($received_cnt,$file_position) =
  21113. $hdr_edits->write_header($msginfo,$proc_fh,!$initial_submission);
  21114. my $msg = $msginfo->mail_text;
  21115. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  21116. $msg = $msg_str_ref if ref $msg_str_ref;
  21117. if ($received_cnt > 100) { # loop detection required by RFC 5321 section 6.3
  21118. # deal with it later, for now just skip the body
  21119. } elsif (!defined $msg) {
  21120. # empty mail
  21121. } elsif (ref $msg eq 'SCALAR') {
  21122. # do it in chunks, saves memory, cache friendly
  21123. while ($file_position < length($$msg)) {
  21124. $proc_fh->print(substr($$msg,$file_position,16384))
  21125. or die "writing mail text to a pipe failed: $!";
  21126. $file_position += 16384; # may overshoot, no problem
  21127. }
  21128. } elsif ($msg->isa('MIME::Entity')) {
  21129. $msg->print_body($proc_fh);
  21130. } else {
  21131. my($nbytes,$buff);
  21132. while (($nbytes = $msg->read($buff,32768)) > 0) {
  21133. $proc_fh->print($buff)
  21134. or die "writing mail text to a pipe failed: $!";
  21135. }
  21136. defined $nbytes or die "Error reading: $!";
  21137. }
  21138. $proc_fh->flush or die "Can't flush pipe to a mail submission program: $!";
  21139. my $smtp_response;
  21140. if ($received_cnt > 100) { # loop detection required by RFC 5321 section 6.3
  21141. do_log(-2, "Too many hops: %d 'Received:' header fields", $received_cnt);
  21142. kill_proc($pid,$command[0],10,$proc_fh,'too many hops') if defined $pid;
  21143. $proc_fh->close; undef $proc_fh; undef $pid; # and ignore status
  21144. $smtp_response = "554 5.4.6 Reject: " .
  21145. "Too many hops: $received_cnt 'Received:' header fields";
  21146. } else {
  21147. my $err = 0; $proc_fh->close or $err=$!;
  21148. my $child_stat = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  21149. undef $proc_fh; undef $pid;
  21150. # sendmail program (Postfix variant) can return the following exit codes:
  21151. # EX_OK(0), EX_DATAERR, EX_SOFTWARE, EX_TEMPFAIL, EX_NOUSER, EX_UNAVAILABLE
  21152. if (proc_status_ok($child_stat,$err, EX_OK)) {
  21153. $smtp_response = "250 2.6.0 Ok"; # submitted to MTA
  21154. snmp_count('OutMsgsDelivers');
  21155. my $size = $msginfo->msg_size;
  21156. snmp_count( ['OutMsgsSize'.$_, $size, 'C64'] ) for @snmp_vars;
  21157. } elsif (proc_status_ok($child_stat,$err, EX_TEMPFAIL)) {
  21158. $smtp_response = "450 4.5.0 Temporary failure submitting message";
  21159. snmp_count('OutMsgsAttemptFails');
  21160. } elsif (proc_status_ok($child_stat,$err, EX_NOUSER)) {
  21161. $smtp_response = "554 5.1.1 Recipient unknown";
  21162. snmp_count('OutMsgsRejects');
  21163. } elsif (proc_status_ok($child_stat,$err, EX_UNAVAILABLE)) {
  21164. $smtp_response = "554 5.5.0 Mail submission service unavailable";
  21165. snmp_count('OutMsgsRejects');
  21166. } else {
  21167. $smtp_response = "451 4.5.0 Failed to submit a message: ".
  21168. exit_status_str($child_stat,$err);
  21169. snmp_count('OutMsgsAttemptFails');
  21170. }
  21171. ll(3) && do_log(3,"mail_via_pipe %s, %s, %s", $command[0],
  21172. exit_status_str($child_stat,$err), $smtp_response);
  21173. }
  21174. $smtp_response .= ", id=" . $msginfo->log_id;
  21175. for my $r (@$per_recip_data_ref) {
  21176. next if $r->recip_done;
  21177. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  21178. $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/;
  21179. }
  21180. $msginfo->dsn_passed_on($dsn_capable && $smtp_response=~/^2/ &&
  21181. !c('terminate_dsn_on_notify_success') ? 1 : 0);
  21182. section_time('fwd-pipe');
  21183. 1;
  21184. }
  21185. 1;
  21186. __DATA__
  21187. #
  21188. package Amavis::Out::BSMTP;
  21189. use strict;
  21190. use re 'taint';
  21191. use warnings;
  21192. use warnings FATAL => qw(utf8 void);
  21193. no warnings 'uninitialized';
  21194. BEGIN {
  21195. require Exporter;
  21196. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  21197. $VERSION = '2.316';
  21198. @ISA = qw(Exporter);
  21199. @EXPORT = qw(&mail_via_bsmtp);
  21200. import Amavis::Conf qw(:platform $QUARANTINEDIR c cr ca);
  21201. import Amavis::Util qw(untaint min max minmax ll do_log snmp_count
  21202. collect_equal_delivery_recips);
  21203. import Amavis::Timing qw(section_time);
  21204. import Amavis::rfc2821_2822_Tools;
  21205. import Amavis::Out::EditHeader;
  21206. }
  21207. use Errno qw(ENOENT EACCES);
  21208. use IO::File qw(O_CREAT O_EXCL O_WRONLY);
  21209. # store message in a BSMTP format
  21210. #
  21211. # RFC 2442: Application/batch-SMTP material is generated by a specially
  21212. # modified SMTP client operating without a corresponding SMTP server.
  21213. # The client simply assumes a successful response to all commands it issues.
  21214. # The resulting content then consists of the collected output from the SMTP
  21215. # client.
  21216. #
  21217. sub mail_via_bsmtp(@) {
  21218. my($msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_;
  21219. my(@snmp_vars) = !$initial_submission ?
  21220. ('', 'Relay', 'ProtoBSMTP', 'ProtoBSMTPRelay')
  21221. : ('', 'Submit', 'ProtoBSMTP', 'ProtoBSMTPSubmit',
  21222. 'Submit'.$initial_submission);
  21223. snmp_count('OutMsgs'.$_) for @snmp_vars;
  21224. my $logmsg = sprintf("%s via BSMTP: %s", ($initial_submission?'SEND':'FWD'),
  21225. $msginfo->sender_smtp);
  21226. my($per_recip_data_ref, $proto_sockname) =
  21227. collect_equal_delivery_recips($msginfo, $filter, qr/^bsmtp:/i);
  21228. if (!$per_recip_data_ref || !@$per_recip_data_ref) {
  21229. do_log(5, "%s, nothing to do", $logmsg); return 1;
  21230. }
  21231. $proto_sockname = $proto_sockname->[0] if ref $proto_sockname;
  21232. ll(1) && do_log(1, "delivering to %s, %s -> %s",
  21233. $proto_sockname, $logmsg,
  21234. join(',', qquote_rfc2821_local(
  21235. map($_->recip_final_addr, @$per_recip_data_ref)) ));
  21236. # just use the first one, ignoring failover alternatives
  21237. local($1);
  21238. $proto_sockname =~ /^bsmtp:(.*)\z/si
  21239. or die "Bad fwd method syntax: ".$proto_sockname;
  21240. my $bsmtp_file_final = $1; my $mbxname;
  21241. my $s = $msginfo->sender; # sanitized sender name for use in a filename
  21242. $s =~ tr/a-zA-Z0-9@._+-/=/c;
  21243. $s = substr($s,0,100)."..." if length($s) > 100+3;
  21244. $s =~ s/\@/_at_/g; $s =~ s/^(\.{0,2})\z/_$1/;
  21245. $bsmtp_file_final =~ s{%(.)}
  21246. { $1 eq 'b' ? $msginfo->body_digest
  21247. : $1 eq 'P' ? $msginfo->partition_tag
  21248. : $1 eq 'm' ? $msginfo->mail_id||''
  21249. : $1 eq 'n' ? $msginfo->log_id
  21250. : $1 eq 's' ? untaint($s) # a hack, avoid using %s
  21251. : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1) #,'-')
  21252. : $1 eq '%' ? '%' : '%'.$1 }egs;
  21253. # prepend directory if not specified
  21254. my $bsmtp_file_final_to_show = $bsmtp_file_final;
  21255. $bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final
  21256. if $QUARANTINEDIR ne '' && $bsmtp_file_final !~ m{^/};
  21257. my $bsmtp_file_tmp = $bsmtp_file_final . ".tmp";
  21258. my $mp; my $err;
  21259. eval {
  21260. my $errn = lstat($bsmtp_file_tmp) ? 0 : 0+$!;
  21261. if ($errn == ENOENT) {} # good, no file, as expected
  21262. elsif ($errn==0 && (-f _ || -l _))
  21263. { die "File $bsmtp_file_tmp already exists, refuse to overwrite" }
  21264. else
  21265. { die "File $bsmtp_file_tmp exists??? Refuse to overwrite it, $!" }
  21266. $mp = IO::File->new;
  21267. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  21268. $mp->open($bsmtp_file_tmp, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640)
  21269. or die "Can't create BSMTP file $bsmtp_file_tmp: $!";
  21270. binmode($mp,':bytes') or die "Can't set :bytes, $!";
  21271. # RFC 2442: Since no SMTP server is present the client must be prepared
  21272. # to make certain assumptions about which SMTP extensions can be used.
  21273. # The generator MAY assume that ESMTP [RFC 1869 (obsoleted by RFC 5321)]
  21274. # facilities are available, that is, it is acceptable to use the EHLO
  21275. # command and additional parameters on MAIL FROM and RCPT TO. If EHLO
  21276. # is used MAY assume that the 8bitMIME [RFC 1652], SIZE [RFC 1870], and
  21277. # NOTARY [RFC 1891] extensions are available. In particular, NOTARY
  21278. # SHOULD be used. (nowadays called DSN)
  21279. $mp->printf("EHLO %s\n", c('localhost_name'))
  21280. or die "print failed (EHLO): $!";
  21281. my $btype = $msginfo->body_type; # RFC 1652: need "8bit Data"? (RFC 2045)
  21282. $btype = '' if !defined $btype;
  21283. my $dsn_envid = $msginfo->dsn_envid; my $dsn_ret = $msginfo->dsn_ret;
  21284. $mp->printf("MAIL FROM:%s\n", join(' ',
  21285. $msginfo->sender_smtp,
  21286. $btype ne '' ? ('BODY='.uc($btype)) : (),
  21287. defined $dsn_ret ? ('RET='.$dsn_ret) : (),
  21288. defined $dsn_envid ? ('ENVID='.$dsn_envid) : () ),
  21289. ) or die "print failed (MAIL FROM): $!";
  21290. for my $r (@$per_recip_data_ref) {
  21291. my(@dsn_notify); # implies a default when the list is empty
  21292. my $dn = $r->dsn_notify;
  21293. @dsn_notify = @$dn if $dn && $msginfo->sender ne ''; # if nondefault
  21294. if (@dsn_notify && c('terminate_dsn_on_notify_success')) {
  21295. # we want to handle option SUCCESS locally
  21296. if (grep($_ eq 'SUCCESS', @dsn_notify)) { # strip out SUCCESS
  21297. @dsn_notify = grep($_ ne 'SUCCESS', @dsn_notify);
  21298. @dsn_notify = ('NEVER') if !@dsn_notify;
  21299. do_log(3,"stripped out SUCCESS, result: NOTIFY=%s",
  21300. join(',',@dsn_notify));
  21301. }
  21302. }
  21303. $mp->printf("RCPT TO:%s\n", join(' ',
  21304. qquote_rfc2821_local($r->recip_final_addr),
  21305. @dsn_notify ? ('NOTIFY='.join(',',@dsn_notify)) : (),
  21306. defined $r->dsn_orcpt ? ('ORCPT='.$r->dsn_orcpt) : () ),
  21307. ) or die "print failed (RCPT TO): $!";
  21308. }
  21309. $mp->print("DATA\n") or die "print failed (DATA): $!";
  21310. my $hdr_edits = $msginfo->header_edits;
  21311. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  21312. my($received_cnt,$file_position) =
  21313. $hdr_edits->write_header($msginfo,$mp,!$initial_submission);
  21314. my $msg = $msginfo->mail_text;
  21315. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  21316. $msg = $msg_str_ref if ref $msg_str_ref;
  21317. if ($received_cnt > 100) { # loop detection required by RFC 5321 sect. 6.3
  21318. die "Too many hops: $received_cnt 'Received:' header fields";
  21319. } elsif (!defined $msg) {
  21320. # empty mail
  21321. } elsif (ref $msg eq 'SCALAR') {
  21322. my $buff = substr($$msg,$file_position);
  21323. $buff =~ s/^\./../gm;
  21324. $mp->print($buff) or die "print failed - data: $!";
  21325. } elsif ($msg->isa('MIME::Entity')) {
  21326. $msg->print_body($mp);
  21327. } else {
  21328. my $ln;
  21329. for ($! = 0; defined($ln=$msg->getline); $! = 0) {
  21330. $mp->print($ln=~/^\./ ? (".",$ln) : $ln)
  21331. or die "print failed - data: $!";
  21332. }
  21333. defined $ln || $! == 0 or die "Error reading: $!";
  21334. }
  21335. $mp->print(".\n") or die "print failed (final dot): $!";
  21336. # $mp->print("QUIT\n") or die "print failed (QUIT): $!";
  21337. $mp->close or die "Error closing BSMTP file $bsmtp_file_tmp: $!";
  21338. undef $mp;
  21339. rename($bsmtp_file_tmp, $bsmtp_file_final)
  21340. or die "Can't rename BSMTP file to $bsmtp_file_final: $!";
  21341. $mbxname = $bsmtp_file_final;
  21342. 1;
  21343. } or do { $err = $@ ne '' ? $@ : "errno=$!" };
  21344. my $smtp_response;
  21345. if ($err eq '') {
  21346. $smtp_response = "250 2.6.0 Ok, queued as BSMTP $bsmtp_file_final_to_show";
  21347. snmp_count('OutMsgsDelivers');
  21348. my $size = $msginfo->msg_size;
  21349. snmp_count( ['OutMsgsSize'.$_, $size, 'C64'] ) for @snmp_vars;
  21350. } else {
  21351. chomp $err;
  21352. unlink($bsmtp_file_tmp)
  21353. or do_log(-2,"Can't delete half-finished BSMTP file %s: %s",
  21354. $bsmtp_file_tmp, $!);
  21355. $mp->close if defined $mp; # ignore status
  21356. if ($err =~ /too many hops\b/i) {
  21357. $smtp_response = "554 5.4.6 Reject: $err";
  21358. snmp_count('OutMsgsRejects');
  21359. } else {
  21360. $smtp_response = "451 4.5.0 Writing $bsmtp_file_tmp failed: $err";
  21361. snmp_count('OutMsgsAttemptFails');
  21362. }
  21363. die $err if $err =~ /^timed out\b/; # resignal timeout
  21364. }
  21365. $smtp_response .= ", id=" . $msginfo->log_id;
  21366. $msginfo->dsn_passed_on($smtp_response=~/^2/ &&
  21367. !c('terminate_dsn_on_notify_success') ? 1 : 0);
  21368. for my $r (@$per_recip_data_ref) {
  21369. next if $r->recip_done;
  21370. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  21371. $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/;
  21372. }
  21373. section_time('fwd-bsmtp');
  21374. 1;
  21375. }
  21376. 1;
  21377. __DATA__
  21378. #
  21379. package Amavis::Out::Local;
  21380. use strict;
  21381. use re 'taint';
  21382. use warnings;
  21383. use warnings FATAL => qw(utf8 void);
  21384. no warnings 'uninitialized';
  21385. BEGIN {
  21386. require Exporter;
  21387. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  21388. $VERSION = '2.316';
  21389. @ISA = qw(Exporter);
  21390. @EXPORT_OK = qw(&mail_to_local_mailbox);
  21391. import Amavis::Conf qw(:platform c cr ca
  21392. $QUARANTINEDIR $quarantine_subdir_levels);
  21393. import Amavis::Util qw(snmp_count ll do_log untaint unique_list
  21394. collect_equal_delivery_recips);
  21395. import Amavis::Timing qw(section_time);
  21396. import Amavis::rfc2821_2822_Tools;
  21397. import Amavis::Out::EditHeader;
  21398. }
  21399. use Errno qw(ENOENT EACCES);
  21400. use Fcntl qw(:flock);
  21401. #use File::Spec;
  21402. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  21403. use subs @EXPORT_OK;
  21404. # Deliver to local mailboxes only, ignore the rest: either to directory
  21405. # (maildir style), or file (Unix mbox). (normally used as a quarantine method)
  21406. #
  21407. sub mail_to_local_mailbox(@) {
  21408. my($msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_;
  21409. # note that recipients of a message being delivered to a quarantine
  21410. # are typically not the original envelope recipients, but are pseudo
  21411. # address provided to do_quarantine() based on @quarantine_to_maps;
  21412. # nevertheless, we do the usual collect_equal_delivery_recips() ritual
  21413. # here too for consistency
  21414. #
  21415. my $logmsg = sprintf("%s via LOCAL: %s", ($initial_submission?'SEND':'FWD'),
  21416. $msginfo->sender_smtp);
  21417. my($per_recip_data_ref, $proto_sockname) =
  21418. collect_equal_delivery_recips($msginfo, $filter, qr/^local:/i);
  21419. if (!$per_recip_data_ref || !@$per_recip_data_ref) {
  21420. do_log(5, "%s, nothing to do", $logmsg); return 1;
  21421. }
  21422. my(@per_recip_data) = @$per_recip_data_ref; undef $per_recip_data_ref;
  21423. $proto_sockname = $proto_sockname->[0] if ref $proto_sockname;
  21424. ll(4) && do_log(4, "delivering to %s, %s -> %s",
  21425. $proto_sockname, $logmsg,
  21426. join(',', qquote_rfc2821_local(
  21427. map($_->recip_final_addr, @per_recip_data)) ));
  21428. # just use the first one, ignoring failover alternatives
  21429. local($1);
  21430. $proto_sockname =~ /^local:(.*)\z/si
  21431. or die "Bad local method syntax: ".$proto_sockname;
  21432. my $via_arg = $1;
  21433. my(@snmp_vars) = !$initial_submission ?
  21434. ('', 'Relay', 'ProtoLocal', 'ProtoLocalRelay')
  21435. : ('', 'Submit','ProtoLocal', 'ProtoLocalSubmit',
  21436. 'Submit'.$initial_submission);
  21437. snmp_count('OutMsgs'.$_) for @snmp_vars;
  21438. my $sender = $msginfo->sender;
  21439. for my $r (@per_recip_data) { # determine a mailbox file for each recipient
  21440. # each recipient gets his own copy; these are not the original message's
  21441. # recipients but are mailbox addresses, typically telling where a message
  21442. # to be quarantined is to be stored
  21443. my $recip = $r->recip_final_addr;
  21444. # %local_delivery_aliases emulates aliases map - this would otherwise
  21445. # be done by MTA's local delivery agent if we gave the message to MTA.
  21446. # This way we keep interface compatible with other mail delivery
  21447. # methods. The hash value may be a ref to a pair of fixed strings,
  21448. # or a subroutine ref (which must return such pair) to allow delayed
  21449. # (lazy) evaluation when some part of the pair is not yet known
  21450. # at initialization time.
  21451. # If no matching entry is found quarantining is skipped.
  21452. my($mbxname, $suggested_filename);
  21453. my($localpart,$domain) = split_address($recip);
  21454. my $ldar = cr('local_delivery_aliases'); # a ref to a hash
  21455. my $alias = $ldar->{$localpart};
  21456. if (ref($alias) eq 'ARRAY') {
  21457. ($mbxname, $suggested_filename) = @$alias;
  21458. } elsif (ref($alias) eq 'CODE') { # lazy (delayed) evaluation
  21459. ($mbxname, $suggested_filename) = &$alias;
  21460. } elsif ($alias ne '') {
  21461. ($mbxname, $suggested_filename) = ($alias, undef);
  21462. } elsif (!exists $ldar->{$localpart}) {
  21463. do_log(3, "no key '%s' in %s, using a default",
  21464. $localpart, '%local_delivery_aliases');
  21465. ($mbxname, $suggested_filename) = ($QUARANTINEDIR, undef);
  21466. }
  21467. if (!defined($mbxname) || $mbxname eq '' || $recip eq '') {
  21468. my $why = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3;
  21469. do_log(2, "skip local delivery(%s): <%s> -> <%s>", $why,$sender,$recip);
  21470. my $smtp_response = "250 2.6.0 Ok, skip local delivery($why)";
  21471. $smtp_response .= ", id=" . $msginfo->log_id;
  21472. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  21473. next;
  21474. }
  21475. my $ux; # is it a UNIX-style mailbox?
  21476. my $errn = stat($mbxname) ? 0 : 0+$!;
  21477. if ($errn == ENOENT) {
  21478. $ux = 1; # $mbxname is a UNIX-style mailbox (new file)
  21479. } elsif ($errn != 0) {
  21480. die "Can't access a mailbox file or directory $mbxname: $!";
  21481. } elsif (-f _) {
  21482. $ux = 1; # $mbxname is a UNIX-style mailbox (existing file)
  21483. } elsif (!-d _) {
  21484. die "Mailbox is neither a file nor a directory: $mbxname";
  21485. } else { # a directory
  21486. $ux = 0; # $mbxname is a directory (amavis/maildir style mailbox)
  21487. my $explicitly_suggested_filename = $suggested_filename ne '';
  21488. if ($suggested_filename eq '')
  21489. { $suggested_filename = $via_arg ne '' ? $via_arg : '%m' }
  21490. my $mail_id = $msginfo->mail_id;
  21491. if (!defined($mail_id)) {
  21492. do_log(-1, "mail_to_local_mailbox: mail_id still undefined!?");
  21493. $mail_id = '';
  21494. }
  21495. $suggested_filename =~ s{%(.)}
  21496. { $1 eq 'b' ? $msginfo->body_digest
  21497. : $1 eq 'P' ? $msginfo->partition_tag
  21498. : $1 eq 'm' ? $mail_id
  21499. : $1 eq 'n' ? $msginfo->log_id
  21500. : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1) #,'-')
  21501. : $1 eq '%' ? '%' : '%'.$1 }egs;
  21502. # $mbxname = File::Spec->catfile($mbxname, $suggested_filename);
  21503. $mbxname = "$mbxname/$suggested_filename";
  21504. if ($quarantine_subdir_levels>=1 && !$explicitly_suggested_filename) {
  21505. # using a subdirectory structure to disperse quarantine files
  21506. local($1,$2); my $subdir = substr($mail_id, 0, 1);
  21507. $subdir=~/^[A-Z0-9]\z/i or die "Unexpected first char: $subdir";
  21508. $mbxname =~ m{^ (.*/)? ([^/]+) \z}sx; my($path,$fname) = ($1,$2);
  21509. # $mbxname = File::Spec->catfile($path, $subdir, $fname);
  21510. $mbxname = "$path$subdir/$fname"; # resulting full filename
  21511. my $errn = stat("$path$subdir") ? 0 : 0+$!;
  21512. # only test for ENOENT, other errors will be detected later on access
  21513. if ($errn == ENOENT) { # check/prepare a set of subdirectories
  21514. do_log(2, "checking/creating quarantine subdirs under %s", $path);
  21515. for my $d ('A'..'Z','a'..'z','0'..'9') {
  21516. $errn = stat("$path$d") ? 0 : 0+$!;
  21517. if ($errn == ENOENT) {
  21518. mkdir("$path$d", 0750) or die "Can't create dir $path$d: $!";
  21519. }
  21520. }
  21521. }
  21522. }
  21523. }
  21524. # save location where mail should be stored, prepend a mailbox style
  21525. $r->recip_mbxname(($ux ? 'mbox' : 'maildir') . ':' . $mbxname);
  21526. }
  21527. #
  21528. # now go ahead and store a message to predetermined files in recip_mbxname;
  21529. # iterate by groups of recipients with the same mailbox name
  21530. #
  21531. @per_recip_data = grep(!$_->recip_done, @per_recip_data);
  21532. while (@per_recip_data) {
  21533. my $mbxname = $per_recip_data[0]->recip_mbxname; # first mailbox name
  21534. # collect all recipient which have the same mailbox file as the first one
  21535. my(@recips_with_same_mbx) =
  21536. grep($_->recip_mbxname eq $mbxname, @per_recip_data);
  21537. @per_recip_data = grep($_->recip_mbxname ne $mbxname, @per_recip_data);
  21538. # retrieve mailbox style and a filename
  21539. local($1,$2); $mbxname =~ /^([^:]*):(.*)\z/;
  21540. my $ux = $1 eq 'mbox' ? 1 : 0; $mbxname = $2;
  21541. my(@recips) = map($_->recip_final_addr, @recips_with_same_mbx);
  21542. @recips = unique_list(\@recips);
  21543. my $smtp_response;
  21544. { # a block is used as a 'switch' statement - 'last' will exit from it
  21545. do_log(1,"local delivery: %s -> %s, mbx=%s",
  21546. $msginfo->sender_smtp, join(", ",@recips), $mbxname);
  21547. my $eval_stat; my($mp,$pos);
  21548. my $errn = stat($mbxname) ? 0 : 0+$!;
  21549. section_time('stat-mbx');
  21550. local $SIG{CHLD} = 'DEFAULT';
  21551. local $SIG{PIPE} = 'IGNORE'; # don't signal on a write to a widowed pipe
  21552. eval { # try to open the mailbox file for writing
  21553. if (!$ux) { # one mail per file, will create specified file
  21554. if ($errn == ENOENT) {
  21555. # good, no file, as expected
  21556. } elsif ($errn != 0) {
  21557. die "File $mbxname not accessible, refuse to write: $!";
  21558. } elsif (!-f _) {
  21559. die "File $mbxname is not a regular file, refuse to write";
  21560. } else {
  21561. die "File $mbxname already exists, refuse to overwrite";
  21562. }
  21563. if ($mbxname =~ /\.gz\z/) {
  21564. $mp = Amavis::IO::Zlib->new; # ?how to request an exclusive access?
  21565. $mp->open($mbxname,'wb')
  21566. or die "Can't create gzip file $mbxname: $!";
  21567. } else {
  21568. $mp = IO::File->new;
  21569. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  21570. $mp->open($mbxname, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640)
  21571. or die "Can't create file $mbxname: $!";
  21572. binmode($mp,':bytes') or die "Can't cancel :utf8 mode: $!";
  21573. }
  21574. } else { # append to a UNIX-style mailbox
  21575. # deliver only to non-executable regular files
  21576. if ($errn == ENOENT) {
  21577. # if two processes try creating the same new UNIX-style mailbox
  21578. # file at the same time, one will tempfail at this point, with
  21579. # its mail delivery to be retried later by MTA
  21580. $mp = IO::File->new;
  21581. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  21582. $mp->open($mbxname, untaint(O_CREAT|O_EXCL|O_APPEND|O_WRONLY),0640)
  21583. or die "Can't create file $mbxname: $!";
  21584. } elsif ($errn==0 && !-f _) {
  21585. die "Mailbox $mbxname is not a regular file, refuse to deliver";
  21586. } elsif (-x _ || -X _) {
  21587. die "Mailbox file $mbxname is executable, refuse to deliver";
  21588. } else {
  21589. $mp = IO::File->new;
  21590. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  21591. $mp->open($mbxname, untaint(O_APPEND|O_WRONLY), 0640)
  21592. or die "Can't append to $mbxname: $!";
  21593. }
  21594. binmode($mp,':bytes') or die "Can't cancel :utf8 mode: $!";
  21595. flock($mp,LOCK_EX) or die "Can't lock mailbox file $mbxname: $!";
  21596. $mp->seek(0,2) or die "Can't position mailbox file to its tail: $!";
  21597. $pos = $mp->tell; # remember where we started
  21598. }
  21599. section_time('open-mbx');
  21600. 1;
  21601. } or do {
  21602. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  21603. $smtp_response =
  21604. $eval_stat =~ /^timed out\b/ ? "450 4.4.2" : "451 4.5.0";
  21605. $smtp_response .= " Local delivery(1) to $mbxname failed: $eval_stat";
  21606. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  21607. };
  21608. last if defined $eval_stat; # exit block, not the loop
  21609. my $failed = 0; $eval_stat = undef;
  21610. eval { # if things fail from here on, try to restore mailbox state
  21611. if ($ux) {
  21612. # a null return path may not appear in the 'From ' delimiter line
  21613. my $snd = $sender eq '' ? 'MAILER-DAEMON' # as in sendmail & Postfix
  21614. : $msginfo->sender_smtp;
  21615. # if the envelope sender contains spaces, tabs, or newlines,
  21616. # the program (like qmail-local) replaces them with hyphens
  21617. $snd =~ s/[ \t\n]/-/sg;
  21618. # date/time in asctime (ctime) format, English month names!
  21619. # RFC 4155 and qmail-local require UTC time, no timezone name
  21620. $mp->printf("From %s %s\n", $snd, scalar gmtime($msginfo->rx_time) )
  21621. or die "Can't write mbox separator line to $mbxname: $!";
  21622. }
  21623. my $hdr_edits = $msginfo->header_edits;
  21624. if (!$hdr_edits) {
  21625. $hdr_edits = Amavis::Out::EditHeader->new;
  21626. $msginfo->header_edits($hdr_edits);
  21627. }
  21628. $hdr_edits->delete_header('Return-Path');
  21629. $hdr_edits->prepend_header('Delivered-To', join(', ',@recips));
  21630. $hdr_edits->prepend_header('Return-Path', $msginfo->sender_smtp);
  21631. my($received_cnt,$file_position) =
  21632. $hdr_edits->write_header($msginfo,$mp,!$initial_submission);
  21633. if ($received_cnt > 110) {
  21634. # loop detection required by RFC 5321 (ex RFC 2821) section 6.3
  21635. # Do not modify the signal text, it gets matched elsewhere!
  21636. die "Too many hops: $received_cnt 'Received:' header fields\n";
  21637. }
  21638. my $msg = $msginfo->mail_text;
  21639. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  21640. $msg = $msg_str_ref if ref $msg_str_ref;
  21641. if (!$ux) { # do it in blocks for speed if we can
  21642. if (!defined $msg) {
  21643. # empty mail
  21644. } elsif (ref $msg eq 'SCALAR') {
  21645. $mp->print(substr($$msg,$file_position))
  21646. or die "Can't write to $mbxname: $!";
  21647. } elsif ($msg->isa('MIME::Entity')) {
  21648. die "quarantining a MIME::Entity object is not implemented";
  21649. } else {
  21650. my($nbytes,$buff);
  21651. while (($nbytes = $msg->read($buff,32768)) > 0) {
  21652. $mp->print($buff) or die "Can't write to $mbxname: $!";
  21653. }
  21654. defined $nbytes or die "Error reading: $!";
  21655. }
  21656. } else { # for UNIX-style mailbox file delivery: escape 'From '
  21657. # mail(1) and elm(1) recognize /^From / as a message delimiter
  21658. # only after a blank line, which is correct. Other MUAs like mutt,
  21659. # thunderbird, kmail and pine need all /^From / lines escaped.
  21660. # See also http://en.wikipedia.org/wiki/Mbox and RFC 4155.
  21661. if (!defined $msg) {
  21662. # empty mail
  21663. } elsif (ref $msg eq 'SCALAR') {
  21664. my $buff = substr($$msg,$file_position);
  21665. local $1;
  21666. # $buff =~ s/^From />From /gm; # mboxo format
  21667. $buff =~ s/^(>*From )/>$1/gm; # mboxrd format
  21668. $mp->print($buff) or die "Can't write to $mbxname: $!";
  21669. } elsif ($msg->isa('MIME::Entity')) {
  21670. die "quarantining a MIME::Entity object is not implemented";
  21671. } else {
  21672. my $ln; my $blank_line = 1;
  21673. # need to copy line-by-line, slow
  21674. for ($! = 0; defined($ln=$msg->getline); $! = 0) {
  21675. # see wikipedia and RFC 4155 for "From " escaping conventions
  21676. $mp->print('>') or die "Can't write to $mbxname: $!"
  21677. if $ln =~ /^(?:>*)From /; # escape all "From " lines
  21678. # if $blank_line && $ln =~ /^(?:>*)From /; # only after blankline
  21679. $mp->print($ln) or die "Can't write to $mbxname: $!";
  21680. $blank_line = $ln eq "\n";
  21681. }
  21682. defined $ln || $! == 0 or die "Error reading: $!";
  21683. }
  21684. }
  21685. # must append an empty line for a Unix mailbox format
  21686. $mp->print("\n") or die "Can't write to $mbxname: $!" if $ux;
  21687. 1;
  21688. } or do { # trouble
  21689. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  21690. if ($ux && defined($pos)) {
  21691. $mp->flush or die "Can't flush file $mbxname: $!";
  21692. $can_truncate or
  21693. do_log(-1, "Truncating a mailbox file will most likely fail");
  21694. # try to restore UNIX-style mailbox to previous size;
  21695. # Produces a fatal error if truncate isn't implemented on the system
  21696. $mp->truncate($pos) or die "Can't truncate file $mbxname: $!";
  21697. }
  21698. $failed = 1;
  21699. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  21700. };
  21701. if ($ux) {
  21702. $mp->flush or die "Can't flush mailbox file $mbxname: $!";
  21703. flock($mp,LOCK_UN) or die "Can't unlock mailbox $mbxname: $!";
  21704. }
  21705. $mp->close or die "Error closing $mbxname: $!";
  21706. undef $mp;
  21707. if (!$failed) {
  21708. $smtp_response = "250 2.6.0 Ok, delivered to $mbxname";
  21709. snmp_count('OutMsgsDelivers');
  21710. my $size = $msginfo->msg_size;
  21711. snmp_count( ['OutMsgsSize'.$_, $size, 'C64'] ) for @snmp_vars;
  21712. } elsif ($@ =~ /^timed out\b/) {
  21713. $smtp_response = "450 4.4.2 Local delivery to $mbxname timed out";
  21714. snmp_count('OutMsgsAttemptFails');
  21715. } elsif ($@ =~ /too many hops\b/i) {
  21716. $smtp_response = "554 5.4.6 Rejected delivery to mailbox $mbxname: $@";
  21717. snmp_count('OutMsgsRejects');
  21718. } else {
  21719. $smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname ".
  21720. "failed: $@";
  21721. snmp_count('OutMsgsAttemptFails');
  21722. }
  21723. } # end of block, 'last' within the block brings us here
  21724. do_log(-1, "%s", $smtp_response) if $smtp_response !~ /^2/;
  21725. $smtp_response .= ", id=" . $msginfo->log_id;
  21726. for my $r (@recips_with_same_mbx) {
  21727. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  21728. $r->recip_mbxname($smtp_response =~ /^2/ ? $mbxname : undef);
  21729. }
  21730. }
  21731. section_time('save-to-local-mailbox');
  21732. }
  21733. 1;
  21734. __DATA__
  21735. #
  21736. package Amavis::OS_Fingerprint;
  21737. use strict;
  21738. use re 'taint';
  21739. use warnings;
  21740. use warnings FATAL => qw(utf8 void);
  21741. no warnings 'uninitialized';
  21742. BEGIN {
  21743. require Exporter;
  21744. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  21745. $VERSION = '2.316';
  21746. @ISA = qw(Exporter);
  21747. import Amavis::Conf qw(:platform);
  21748. import Amavis::Util qw(ll do_log);
  21749. }
  21750. use Errno qw(EINTR EAGAIN);
  21751. use Socket;
  21752. use IO::Socket::UNIX;
  21753. #use IO::Socket::INET;
  21754. use Time::HiRes ();
  21755. sub new {
  21756. my($class, $service_method,$timeout,
  21757. $src_ip,$src_port, $dst_ip,$dst_port, $nonce) = @_;
  21758. local($1,$2,$3); my($type,$service_host,$service_port,$service_path);
  21759. if ($service_method =~
  21760. m{^p0f: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) }six) {
  21761. $type = "p0f-analyzer"; ($service_host, $service_port) = ($1.$2, $3);
  21762. } elsif ($service_method =~
  21763. m{^p0f: ( / [^ ]+ ) \z}six) { # looks like a unix socket
  21764. $type = "p0f"; $service_path = $1;
  21765. } else { die "Bad p0f method syntax: $service_method" }
  21766. $dst_ip = '0.0.0.0' if !defined $dst_ip; # our MTA's IP address
  21767. $dst_port = defined $dst_port ? 0+$dst_port : 0; # our MTA port, usually 25
  21768. $src_port = defined $src_port ? 0+$src_port : 0; # remote client's port no.
  21769. do_log(4,"Fingerprint query: [%s]:%s %s (%s) %s",
  21770. $src_ip,$src_port,$nonce,$type,$service_method);
  21771. my $sock; my $query; my $query_sent = 0;
  21772. if ($type eq "p0f-analyzer") { # send a UDP query to p0f-analyzer
  21773. $query = '['.$src_ip.']' . ($src_port==0 ? '' : ':'.$src_port);
  21774. $have_inet4
  21775. or die "Can't connect to p0f, protocol family INET not available";
  21776. # IO::Socket::IP 0.08 does not handle unconnected sockets yet
  21777. $sock = IO::Socket::INET->new(Type=>SOCK_DGRAM, Proto=>'udp');
  21778. $sock or die "Can't create inet socket: $!";
  21779. my $hisiaddr;
  21780. $hisiaddr = inet_aton($service_host)
  21781. or die "Fingerprint bad IP address: $service_host";
  21782. my $hispaddr = scalar(sockaddr_in($service_port, $hisiaddr));
  21783. # bypass send method in IO::Socket to be able to retrieve
  21784. # status/errno directly from 'send', not from 'getpeername':
  21785. defined send($sock, "$query $nonce", 0, $hispaddr)
  21786. or die "Fingerprint - send error: $!";
  21787. $query_sent = 1;
  21788. } elsif ($type eq "p0f") { # contact p0f directly
  21789. if ($src_ip !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/s) {
  21790. do_log(5,"Fingerprint - SRC addr not an IPv4: %s", $src_ip);
  21791. } elsif ($dst_ip !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/s) {
  21792. do_log(5,"Fingerprint - DST addr not an IPv4: %s", $dst_ip);
  21793. } else { # connect to a Unix socket where p0f is listening
  21794. $sock = IO::Socket::UNIX->new(Type=>SOCK_STREAM, Peer=>$service_path);
  21795. if (!$sock) {
  21796. do_log(-1, "Can't connect to p0f socket %s: %s", $service_path,$!);
  21797. } else { # send a query directly to a p0f service
  21798. my $QUERY_MAGIC = 0x0defaced; my $QTYPE_FINGERPRINT = 1;
  21799. my($src_n,$dst_n);
  21800. $src_n = inet_aton($src_ip) or die "Fingerprint bad IP addr: $src_ip";
  21801. $dst_n = inet_aton($dst_ip) or die "Fingerprint bad IP addr: $dst_ip";
  21802. my $j=0; $j = ($j*7 ^ ord($_)) & 0xffffffff for split(//,$nonce);
  21803. $nonce = $j; # convert a string into a 32-bit integer
  21804. $query = pack("LLLa4a4SS", $QUERY_MAGIC, $QTYPE_FINGERPRINT, $nonce,
  21805. $src_n, $dst_n, $src_port, $dst_port);
  21806. my $nwrite = syswrite($sock,$query);
  21807. if (defined $nwrite && $nwrite==length($query)) { $query_sent = 1 }
  21808. else { do_log(-1, "Error writing to p0f %s: %s", $service_path,$!) }
  21809. }
  21810. }
  21811. }
  21812. return if !$query_sent;
  21813. bless { sock=>$sock, wait_until=>(Time::HiRes::time + $timeout),
  21814. query=>$query, nonce=>$nonce, type=>$type }, $class;
  21815. }
  21816. sub collect_response {
  21817. my($self) = @_;
  21818. my $timeout = $self->{wait_until} - Time::HiRes::time;
  21819. if ($timeout < 0) { $timeout = 0 };
  21820. my $type = $self->{type};
  21821. my $sock = $self->{sock};
  21822. my($resp,$nfound,$inbuf);
  21823. my($rin,$rout); $rin = ''; vec($rin,fileno($sock),1) = 1;
  21824. for (;;) {
  21825. $nfound = select($rout=$rin, undef, undef, $timeout);
  21826. last if !$nfound || $nfound < 0;
  21827. my $rv = $type eq "p0f-analyzer" ? $sock->recv($inbuf,1024,0)
  21828. : $sock->sysread($inbuf,1024);
  21829. if (!defined $rv) {
  21830. if ($! == EAGAIN || $! == EINTR) {
  21831. Time::HiRes::sleep(0.1); # slow down, just in case
  21832. } else {
  21833. die "Fingerprint - error reading from socket: $!";
  21834. }
  21835. } elsif ($type eq "p0f" && $rv < 1) { # sysread returns 0 at eof
  21836. last;
  21837. } elsif ($type eq "p0f-analyzer") {
  21838. local($1,$2,$3);
  21839. if ($inbuf =~ /^([^ ]*) ([^ ]*) (.*)\015\012\z/) {
  21840. my($r_query,$r_nonce,$r_resp) = ($1,$2,$3);
  21841. if ($r_query eq $self->{query} && $r_nonce eq $self->{nonce})
  21842. { $resp = $r_resp };
  21843. }
  21844. do_log(4,"Fingerprint collect: max_wait=%.3f, %.35s... => %s",
  21845. $timeout,$inbuf,$resp);
  21846. $timeout = 0;
  21847. } elsif ($type eq "p0f") {
  21848. # # default struct alignments
  21849. # my($magic, $id, $r_status, $genre, $detail, $dist, $link, $tos,
  21850. # $fw, $nat, $real, $dmy1, $masq_score, $masq_flags, $dmy2, $uptime) =
  21851. # unpack ("L L C Z20 Z40 c Z30 Z30 C C C C s S S l", $inbuf);
  21852. # properly packed struct
  21853. my($magic, $id, $r_status, $genre, $detail, $dist, $link, $tos,
  21854. $fw, $nat, $real, $masq_score, $masq_flags, $uptime) =
  21855. unpack ("L L C Z20 Z40 c Z30 Z30 C C C s S l", $inbuf);
  21856. my $QUERY_MAGIC = 0x0defaced;
  21857. $magic == $QUERY_MAGIC or die "Bad response magic";
  21858. if ($r_status == 1) {
  21859. do_log(-1, "Fingerprint - malformed query");
  21860. } elsif ($r_status == 0 && $id != $self->{nonce}) {
  21861. do_log(-1, "Fingerprint - nonce mismatch: %s", $id);
  21862. } elsif ($r_status == 2) {
  21863. do_log(1, "Fingerprint - no matching entry in the p0f cache");
  21864. } elsif ($r_status == 0) {
  21865. $resp = sprintf("%s%s%s%s%s%s, (%s%s)",
  21866. ($genre eq '' ? 'UNKNOWN' : $genre),
  21867. ($detail eq '' ? '' : " $detail"),
  21868. (!$fw ? '' : " (firewall!)"),
  21869. (!$nat ? '' : $nat==1 ? " (NAT!)" : " (NAT$nat!)"),
  21870. ($tos eq '' ? '' : " [tos $tos]"),
  21871. $uptime == -1 ? '' : " (up: $uptime hrs)",
  21872. ($dist == -1 ? '' : "distance $dist, "),
  21873. ($link eq '' ? '' : "link: $link"));
  21874. } else {
  21875. do_log(-1, "Fingerprint - invalid reply type: %s", $r_status);
  21876. }
  21877. do_log(4,"Fingerprint collect: max_wait=%.3f => %s", $timeout,$resp);
  21878. $timeout = 0;
  21879. }
  21880. }
  21881. defined $nfound && $nfound >= 0
  21882. or die "Fingerprint - select on socket failed: $!";
  21883. if ($type eq "p0f") { $sock->close or die "Error closing socket: $!" }
  21884. $resp;
  21885. }
  21886. 1;
  21887. __DATA__
  21888. #^L
  21889. package Amavis::Out::SQL::Connection;
  21890. use strict;
  21891. use re 'taint';
  21892. use warnings;
  21893. use warnings FATAL => qw(utf8 void);
  21894. no warnings 'uninitialized';
  21895. BEGIN {
  21896. require Exporter;
  21897. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  21898. $VERSION = '2.316';
  21899. @ISA = qw(Exporter);
  21900. import Amavis::Conf qw(:platform c cr ca);
  21901. import Amavis::Util qw(ll do_log do_log_safe);
  21902. import Amavis::Timing qw(section_time);
  21903. }
  21904. use DBI qw(:sql_types);
  21905. # one object per connection (normally exactly one) to a database server;
  21906. # connection need not exist at all times, stores info on how to connect;
  21907. # when connected it holds a database handle
  21908. #
  21909. sub new {
  21910. my($class, @dsns) = @_; # a list of DSNs to try connecting to sequentially
  21911. bless { dbh=>undef, sth=>undef, incarnation=>1, in_transaction=>0,
  21912. dsn_list=>\@dsns, dsn_current=>undef }, $class;
  21913. }
  21914. sub dsn_current { # get/set information on currently connected data set name
  21915. my $self = shift; !@_ ? $self->{dsn_current} : ($self->{dsn_current}=shift);
  21916. }
  21917. sub dbh { # get/set database handle
  21918. my $self = shift; !@_ ? $self->{dbh} : ($self->{dbh}=shift);
  21919. }
  21920. sub sth { # get/set statement handle
  21921. my $self = shift; my $clause = shift;
  21922. !@_ ? $self->{sth}{$clause} : ($self->{sth}{$clause}=shift);
  21923. }
  21924. sub dbh_inactive { # get/set dbh "InactiveDestroy" attribute
  21925. my $self = shift;
  21926. my $dbh = $self->dbh;
  21927. return if !$dbh;
  21928. !@_ ? $dbh->{'InactiveDestroy'} : ($dbh->{'InactiveDestroy'}=shift);
  21929. }
  21930. sub DESTROY {
  21931. my $self = shift; local($@,$!,$_);
  21932. do_log_safe(5,"Amavis::Out::SQL::Connection DESTROY called");
  21933. # ignore failures, make perlcritic happy
  21934. eval { $self->disconnect_from_sql } or 1;
  21935. }
  21936. # returns current connection version; works like cache versioning/invalidation:
  21937. # SQL statement handles need to be rebuilt and caches cleared when SQL
  21938. # connection is re-established and a new database handle provided
  21939. #
  21940. sub incarnation { my $self = shift; $self->{incarnation} }
  21941. sub in_transaction {
  21942. my $self = shift;
  21943. !@_ ? $self->{in_transaction} : ($self->{in_transaction}=shift)
  21944. }
  21945. # returns DBD driver name such as 'Pg', 'mysql'; or undef if unknown
  21946. #
  21947. sub driver_name {
  21948. my $self = shift; my $dbh = $self->dbh;
  21949. $dbh or die "sql driver_name: dbh not available";
  21950. !$dbh->{Driver} ? undef : $dbh->{Driver}->{Name};
  21951. }
  21952. # DBI method wrappers:
  21953. #
  21954. sub begin_work {
  21955. my $self = shift; do_log(5,"sql begin transaction");
  21956. # DBD::mysql man page: if you detect an error while changing
  21957. # the AutoCommit mode, you should no longer use the database handle.
  21958. # In other words, you should disconnect and reconnect again
  21959. $self->dbh or $self->connect_to_sql;
  21960. my $stat; my $eval_stat;
  21961. eval {
  21962. $stat = $self->dbh->begin_work(@_); 1;
  21963. } or do {
  21964. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  21965. };
  21966. if (defined $eval_stat || !$stat) {
  21967. do_log(-1,"sql begin transaction failed, ".
  21968. "probably disconnected by server, reconnecting (%s)", $eval_stat);
  21969. $self->disconnect_from_sql;
  21970. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  21971. $self->connect_to_sql;
  21972. $stat = $self->dbh->begin_work(@_);
  21973. }
  21974. $self->in_transaction(1);
  21975. $stat;
  21976. };
  21977. sub begin_work_nontransaction {
  21978. my $self = shift; do_log(5,"sql begin, nontransaction");
  21979. $self->dbh or $self->connect_to_sql;
  21980. };
  21981. sub commit {
  21982. my $self = shift; do_log(5,"sql commit");
  21983. $self->in_transaction(0);
  21984. my $dbh = $self->dbh;
  21985. $dbh or die "commit: dbh not available";
  21986. $dbh->commit(@_); my($rv_err,$rv_str) = ($dbh->err, $dbh->errstr);
  21987. do_log(2,"sql commit status: err=%s, errstr=%s",
  21988. $rv_err,$rv_str) if defined $rv_err;
  21989. ($rv_err,$rv_str); # potentially useful to see non-fatal errors
  21990. };
  21991. sub rollback {
  21992. my $self = shift; do_log(5,"sql rollback");
  21993. $self->in_transaction(0);
  21994. $self->dbh or die "rollback: dbh not available";
  21995. eval {
  21996. $self->dbh->rollback(@_); 1;
  21997. } or do {
  21998. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  21999. do_log(-1,"sql rollback error, reconnecting (%s)", $eval_stat);
  22000. $self->disconnect_from_sql;
  22001. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22002. $self->connect_to_sql;
  22003. # $self->dbh->rollback(@_); # too late now, hopefully implied in disconnect
  22004. };
  22005. };
  22006. sub fetchrow_arrayref {
  22007. my($self,$clause,@args) = @_;
  22008. $self->dbh or die "fetchrow_arrayref: dbh not available";
  22009. my $sth = $self->sth($clause);
  22010. $sth or die "fetchrow_arrayref: statement handle not available";
  22011. $sth->fetchrow_arrayref(@args);
  22012. };
  22013. sub finish {
  22014. my($self,$clause,@args) = @_;
  22015. $self->dbh or die "finish: dbh not available";
  22016. my $sth = $self->sth($clause);
  22017. $sth or die "finish: statement handle not available";
  22018. $sth->finish(@args);
  22019. };
  22020. sub execute {
  22021. my($self,$clause,@args) = @_;
  22022. $self->dbh or die "sql execute: dbh not available";
  22023. my $sth = $self->sth($clause); # fetch cached st. handle or prepare new
  22024. if ($sth) {
  22025. ll(5) && do_log(5, "sql: executing clause (%d args): %s",
  22026. scalar(@args), $clause);
  22027. } else {
  22028. ll(4) && do_log(4,"sql: preparing and executing (%d args): %s",
  22029. scalar(@args), $clause);
  22030. $sth = $self->dbh->prepare($clause); $self->sth($clause,$sth);
  22031. $sth or die "sql: prepare failed: ".$DBI::errstr;
  22032. }
  22033. my($rv_err,$rv_str);
  22034. eval {
  22035. for my $j (0..$#args) { # arg can be a scalar or [val,type] or [val,\%attr]
  22036. my $arg = $args[$j];
  22037. $sth->bind_param($j+1, !ref($arg) ? $arg : @$arg);
  22038. # ll(5) && do_log(5, "sql: bind %d: %s",
  22039. # $j+1, !ref($arg) ? $arg : '['.join(',',@$arg).']' );
  22040. }
  22041. $sth->execute; $rv_err = $sth->err; $rv_str = $sth->errstr; 1;
  22042. } or do {
  22043. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22044. # man DBI: ->err code is typically an integer but you should not assume so
  22045. # $DBI::errstr is normally already contained in $eval_stat
  22046. my $sqlerr = $sth ? $sth->err : $DBI::err;
  22047. my $sqlstate = $sth ? $sth->state : $DBI::state;
  22048. my $msg = sprintf("err=%s, %s, %s", $sqlerr, $sqlstate, $eval_stat);
  22049. if (!$sth) {
  22050. die "sql execute (no handle): ".$msg;
  22051. } elsif (! ($sqlerr eq '2006' || $sqlerr eq '2013' || # MySQL
  22052. ($sqlerr == -1 && $sqlstate eq 'S1000') || # PostgreSQL 7
  22053. ($sqlerr == 7 && $sqlstate =~ /^(S8|08|57)...\z/i) )) { #PgSQL
  22054. # libpq-fe.h: ExecStatusType PGRES_FATAL_ERROR=7
  22055. # ignore failures, make perlcritic happy
  22056. eval { $self->disconnect_from_sql } or 1; # better safe than sorry
  22057. die "sql exec: $msg\n";
  22058. } else { # Server has gone away; Lost connection to...
  22059. # MySQL: 2006, 2013; PostgreSQL: 7
  22060. if ($self->in_transaction) {
  22061. # ignore failures, make perlcritic happy
  22062. eval { $self->disconnect_from_sql } or 1;
  22063. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22064. die "sql execute failed within transaction, $msg";
  22065. } else { # try one more time
  22066. do_log(0,"NOTICE: reconnecting in response to: %s", $msg);
  22067. # ignore failures, make perlcritic happy
  22068. eval { $self->disconnect_from_sql } or 1;
  22069. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22070. $self->connect_to_sql;
  22071. $self->dbh or die "sql execute: reconnect failed";
  22072. do_log(4,"sql: preparing and executing (again): %s", $clause);
  22073. $sth = $self->dbh->prepare($clause); $self->sth($clause,$sth);
  22074. $sth or die "sql: prepare (reconnected) failed: ".$DBI::errstr;
  22075. $rv_err = $rv_str = undef;
  22076. eval {
  22077. for my $j (0..$#args) { # a scalar or [val,type] or [val,\%attr]
  22078. $sth->bind_param($j+1, !ref($args[$j]) ? $args[$j] : @{$args[$j]});
  22079. }
  22080. $sth->execute; $rv_err = $sth->err; $rv_str = $sth->errstr; 1;
  22081. } or do {
  22082. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22083. $msg = sprintf("err=%s, %s, %s", $DBI::err,$DBI::state,$eval_stat);
  22084. $self->disconnect_from_sql;
  22085. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22086. die "sql execute failed again, $msg";
  22087. };
  22088. }
  22089. }
  22090. };
  22091. # $rv_err: undef indicates success, "" indicates an 'information',
  22092. # "0" indicates a 'warning', true indicates an error
  22093. do_log(2,"sql execute status: err=%s, errstr=%s",
  22094. $rv_err,$rv_str) if defined $rv_err;
  22095. ($rv_err,$rv_str); # potentially useful to see non-fatal errors
  22096. }
  22097. # Connect to a database. Take a list of database connection
  22098. # parameters and try each until one succeeds.
  22099. # -- based on code from Ben Ransford <amavis@uce.ransford.org> 2002-09-22
  22100. #
  22101. sub connect_to_sql {
  22102. my $self = shift; # a list of DSNs to try connecting to sequentially
  22103. my $dbh; my(@dsns) = @{$self->{dsn_list}};
  22104. do_log(3,"Connecting to SQL database server");
  22105. for my $tmpdsn (@dsns) {
  22106. my($dsn, $username, $password) = @$tmpdsn;
  22107. do_log(4,"connect_to_sql: trying '%s'", $dsn);
  22108. $dbh = DBI->connect($dsn, $username, $password,
  22109. {PrintError => 0, RaiseError => 0, Taint => 1, AutoCommit => 1} );
  22110. if ($dbh) {
  22111. $self->dsn_current($dsn);
  22112. do_log(3,"connect_to_sql: '%s' succeeded", $dsn);
  22113. last;
  22114. }
  22115. do_log(-1,"connect_to_sql: unable to connect to DSN '%s': %s",
  22116. $dsn, $DBI::errstr);
  22117. }
  22118. $self->dbh($dbh); delete($self->{sth});
  22119. $self->in_transaction(0); $self->{incarnation}++;
  22120. $dbh or die "connect_to_sql: unable to connect to any dataset";
  22121. $dbh->{'RaiseError'} = 1;
  22122. # $dbh->{mysql_auto_reconnect} = 1; # questionable benefit
  22123. # $dbh->func(30000,'busy_timeout'); # milliseconds (SQLite)
  22124. eval {
  22125. $dbh->do("SET NAMES 'utf8'"); 1;
  22126. } or do {
  22127. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22128. do_log(2,"connect_to_sql: SET NAMES 'utf8' failed: %s", $eval_stat);
  22129. };
  22130. section_time('sql-connect');
  22131. $self;
  22132. }
  22133. sub disconnect_from_sql($) {
  22134. my $self = shift;
  22135. my $did_disconnect;
  22136. $self->in_transaction(0);
  22137. if ($self->dbh) {
  22138. do_log(4,"disconnecting from SQL");
  22139. $self->dbh->disconnect; $self->dbh(undef);
  22140. $did_disconnect = 1;
  22141. }
  22142. delete $self->{sth}; $self->dsn_current(undef);
  22143. $did_disconnect;
  22144. }
  22145. 1;
  22146. __DATA__
  22147. #^L
  22148. package Amavis::Out::SQL::Log;
  22149. use strict;
  22150. use re 'taint';
  22151. use warnings;
  22152. use warnings FATAL => qw(utf8 void);
  22153. no warnings 'uninitialized';
  22154. BEGIN {
  22155. require Exporter;
  22156. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  22157. $VERSION = '2.316';
  22158. @ISA = qw(Exporter);
  22159. import Amavis::Conf qw(:platform :confvars c cr ca);
  22160. import Amavis::rfc2821_2822_Tools;
  22161. import Amavis::Util qw(ll do_log do_log_safe min max minmax
  22162. untaint untaint_inplace
  22163. add_entropy sanitize_str safe_decode
  22164. safe_encode safe_encode_ascii safe_encode_utf8
  22165. snmp_count orcpt_decode ccat_split ccat_maj);
  22166. import Amavis::Lookup qw(lookup lookup2);
  22167. import Amavis::Out::SQL::Connection ();
  22168. }
  22169. use Encode; # Perl 5.8 UTF-8 support
  22170. use DBI qw(:sql_types);
  22171. sub new {
  22172. my($class,$conn_h) = @_; bless { conn_h=>$conn_h, incarnation=>0 }, $class;
  22173. }
  22174. sub DESTROY {
  22175. my $self = shift; local($@,$!,$_);
  22176. do_log_safe(5,"Amavis::Out::SQL::Log DESTROY called");
  22177. }
  22178. # find an existing e-mail address record or insert one, returning its id;
  22179. # may return undef if 'sel_adr' or 'ins_adr' SQL clauses are not defined;
  22180. #
  22181. sub find_or_save_addr {
  22182. my($self,$addr,$partition_tag,$keep_localpart_case) = @_;
  22183. my $id; my $existed = 0; my($localpart,$domain);
  22184. my $naddr = untaint($addr);
  22185. if ($naddr ne '') { # normalize address (lowercase, 7-bit, max 255 ch...)
  22186. ($localpart,$domain) = split_address($naddr);
  22187. $domain =~ s/[^\040-\176]/?/gs; $domain = lc $domain;
  22188. if (!$keep_localpart_case && !c('localpart_is_case_sensitive')) {
  22189. $localpart = lc($localpart);
  22190. }
  22191. local($1);
  22192. $domain = $1 if $domain=~/^\@?(.*?)\.*\z/s; # chop leading @ and tr. dots
  22193. $naddr = $localpart.'@'.$domain;
  22194. $naddr = substr($naddr,0,255) if length($naddr) > 255;
  22195. # avoid UTF-8 SQL trouble, legitimate RFC 5321 addresses only need 7 bits
  22196. $naddr =~ s/[^\040-\176]/?/g if !$sql_allow_8bit_address;
  22197. # SQL character strings disallow zero octets, and also disallow any other
  22198. # octet values and sequences of octet values that are invalid according to
  22199. # the database's selected character set encoding
  22200. }
  22201. my $conn_h = $self->{conn_h}; my $sql_cl_r = cr('sql_clause');
  22202. my $sel_adr = $sql_cl_r->{'sel_adr'};
  22203. my $ins_adr = $sql_cl_r->{'ins_adr'};
  22204. if (!defined($sel_adr) || $sel_adr eq '') {
  22205. # no way to query a database, behave as if no record was found
  22206. do_log(5,"find_or_save_addr: sel_adr query disabled, %s", $naddr);
  22207. } else {
  22208. $conn_h->begin_work_nontransaction; #(re)connect if necessary, autocommit
  22209. my $datatype = SQL_VARCHAR;
  22210. if ($sql_allow_8bit_address) {
  22211. my $driver = $conn_h->driver_name; # only available when connected
  22212. $datatype = $driver eq 'Pg' ? { pg_type => DBD::Pg::PG_BYTEA() }
  22213. : SQL_VARBINARY;
  22214. }
  22215. $conn_h->execute($sel_adr, $partition_tag, [$naddr,$datatype]);
  22216. my($a_ref,$a2_ref);
  22217. if (defined($a_ref=$conn_h->fetchrow_arrayref($sel_adr))) { # exists?
  22218. $id = $a_ref->[0]; $conn_h->finish($sel_adr);
  22219. $existed = 1;
  22220. } elsif (!defined($ins_adr) || $ins_adr eq '') {
  22221. # record does not exist, insertion is not allowed
  22222. do_log(5,"find_or_save_addr: ins_adr insertion disabled, %s", $naddr);
  22223. } else { # does not exist, attempt to insert a new e-mail address record
  22224. my $invdomain; # domain with reversed fields, chopped to 255 characters
  22225. $invdomain = join('.', reverse split(/\./,$domain,-1));
  22226. $invdomain = substr($invdomain,0,255) if length($invdomain) > 255;
  22227. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  22228. my $eval_stat;
  22229. eval { $conn_h->execute($ins_adr, $partition_tag,
  22230. [$naddr,$datatype], $invdomain); 1 }
  22231. or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  22232. # INSERT may have failed because of race condition with other processes;
  22233. # try the SELECT again, it will most likely succeed this time;
  22234. # SELECT after INSERT also avoids the need for a working last_insert_id()
  22235. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  22236. # try select again, regardless of the success of INSERT
  22237. $conn_h->execute($sel_adr, $partition_tag, [$naddr,$datatype]);
  22238. if ( defined($a2_ref=$conn_h->fetchrow_arrayref($sel_adr)) ) {
  22239. $id = $a2_ref->[0]; $conn_h->finish($sel_adr);
  22240. add_entropy($id);
  22241. if (!defined($eval_stat)) { # status of the INSERT
  22242. do_log(5,"find_or_save_addr: record inserted, id=%s, %s",
  22243. $id,$naddr);
  22244. } else {
  22245. $existed = 1; chomp $eval_stat;
  22246. do_log(5,"find_or_save_addr: found on a second attempt, ".
  22247. "id=%s, %s, (first attempt: %s)", $id,$naddr,$eval_stat);
  22248. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22249. }
  22250. } else { # still does not exist
  22251. $id = $existed = undef;
  22252. if (defined $eval_stat) { # status of the INSERT
  22253. chomp $eval_stat;
  22254. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22255. };
  22256. die "find_or_save_addr: failed to insert addr $naddr: $eval_stat";
  22257. }
  22258. }
  22259. }
  22260. ($id, $existed);
  22261. }
  22262. # find a penpals record which proves that a local user sid really sent a
  22263. # mail to a recipient rid some time ago. Returns an interval time in seconds
  22264. # since the last such mail was sent by our local user to a specified recipient
  22265. # (or undef if information is not available). If @$message_id_list is a
  22266. # nonempty list of Message-IDs as found in References header field, the query
  22267. # also provides previous outgoing messages with a matching Message-ID but
  22268. # possibly to recipients different from what the mail was originally sent to.
  22269. #
  22270. sub penpals_find {
  22271. my($self, $sid,$rid,$message_id_list, $now) = @_;
  22272. my($a_ref,$found,$age,$send_time,$ref_mail_id,$ref_subj,$ref_mid,$ref_rid);
  22273. my $conn_h = $self->{conn_h}; my $sql_cl_r = cr('sql_clause');
  22274. my $sel_penpals = $sql_cl_r->{'sel_penpals'};
  22275. my $sel_penpals_msgid = $sql_cl_r->{'sel_penpals_msgid'};
  22276. if (defined($sel_penpals_msgid) && @$message_id_list && defined($sid)) {
  22277. # list of refs to Message-ID is nonempty, try reference or recipient match
  22278. my $n = scalar(@$message_id_list); # number of keys
  22279. my(@args) = ($sid,$rid); my(@pos_args); local($1);
  22280. my $sel_taint = substr($sel_penpals_msgid,0,0); # taintedness
  22281. $sel_penpals_msgid =~
  22282. s{ ( %m | \? ) } # substitute %m for keys and ? for next arg
  22283. { push(@pos_args,
  22284. $1 eq '%m' ? (map { my $s=$_; $s=~s/[^\040-\176]/?/gs; $s }
  22285. @$message_id_list)
  22286. : shift @args),
  22287. $1 eq '%m' ? join(',', ('?') x $n) : '?' }gxe;
  22288. # keep original clause taintedness
  22289. $sel_penpals_msgid = untaint($sel_penpals_msgid) . $sel_taint;
  22290. untaint_inplace($_) for @pos_args; # untaint arguments
  22291. do_log(4, "penpals: query args: %s", join(', ',@pos_args));
  22292. do_log(4, "penpals: %s", $sel_penpals_msgid);
  22293. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  22294. $conn_h->execute($sel_penpals_msgid,@pos_args);
  22295. snmp_count('PenPalsAttempts'); snmp_count('PenPalsAttemptsMid');
  22296. if (!defined($a_ref=$conn_h->fetchrow_arrayref($sel_penpals_msgid))) {
  22297. snmp_count('PenPalsMisses');
  22298. } else {
  22299. ($send_time, $ref_mail_id, $ref_subj, $ref_mid, $ref_rid) = @$a_ref;
  22300. $found = 1; $conn_h->finish($sel_penpals_msgid);
  22301. my $rid_match = defined $ref_rid && defined $rid && $rid eq $ref_rid;
  22302. my $mid_match = grep($ref_mid eq $_, @$message_id_list);
  22303. my $t = $mid_match && $rid_match ? 'MidRid' :
  22304. # $mid_match && !defined($rid) ? 'MidNullRPath' :
  22305. $mid_match ? 'Mid' : $rid_match ? 'Rid' : 'none';
  22306. snmp_count('PenPalsHits'.$t); snmp_count('PenPalsHits');
  22307. ll(4) && do_log(4, "penpals: MATCH ON %s: %s",
  22308. $t, join(", ",@$a_ref));
  22309. }
  22310. }
  22311. if (!$found && defined($sel_penpals) && defined($rid) && defined($sid)) {
  22312. # list of Message-ID references not given, try matching on recipient only
  22313. $conn_h->begin_work_nontransaction; # (re)connect if not connected
  22314. $conn_h->execute($sel_penpals, untaint($sid), untaint($rid));
  22315. snmp_count('PenPalsAttempts'); snmp_count('PenPalsAttemptsRid');
  22316. if (!defined($a_ref=$conn_h->fetchrow_arrayref($sel_penpals))) { # exists?
  22317. snmp_count('PenPalsMisses');
  22318. } else {
  22319. ($send_time, $ref_mail_id, $ref_subj) = @$a_ref;
  22320. $found = 1; $conn_h->finish($sel_penpals);
  22321. snmp_count('PenPalsHitsRid'); snmp_count('PenPalsHits');
  22322. ll(4) && do_log(4, "penpals: MATCH ON RID(%s): %s",
  22323. $rid, join(", ",@$a_ref));
  22324. }
  22325. }
  22326. if (!$found) {
  22327. ll(4) && do_log(4, "penpals: (%s,%s) not found%s", $sid,$rid,
  22328. !@$message_id_list ? '' : ' refs: '.join(", ",@$message_id_list));
  22329. } else {
  22330. $age = max(0, $now - $send_time);
  22331. do_log(3, "penpals: (%s,%s) %s age %.3f days",
  22332. $sid,$rid, $ref_mail_id, $age/(24*60*60));
  22333. }
  22334. ($age, $ref_mail_id, $ref_subj);
  22335. }
  22336. sub save_info_preliminary {
  22337. my($self, $msginfo) = @_;
  22338. my $mail_id = $msginfo->mail_id;
  22339. defined $mail_id or die "save_info_preliminary: mail_id still undefined";
  22340. my $partition_tag = $msginfo->partition_tag;
  22341. my($sid,$existed,$sender_smtp); local($1);
  22342. $sender_smtp = $msginfo->sender_smtp; $sender_smtp =~ s/^<(.*)>\z/$1/s;
  22343. # find an existing e-mail address record for sender, or insert a new one
  22344. ($sid,$existed) = $self->find_or_save_addr($sender_smtp,$partition_tag);
  22345. if (defined $sid) {
  22346. $msginfo->sender_maddr_id($sid);
  22347. # there is perhaps 30-50% chance the sender address is already in the db
  22348. snmp_count('SqlAddrSenderAttempts');
  22349. snmp_count($existed ? 'SqlAddrSenderHits' : 'SqlAddrSenderMisses');
  22350. do_log(4,"save_info_preliminary %s, sender id: %s, %s, %s",
  22351. $mail_id, $sid, $sender_smtp, $existed ? 'exists' : 'new' );
  22352. }
  22353. # find existing address records for recipients, or insert them
  22354. for my $r (@{$msginfo->per_recip_data}) {
  22355. my $addr_smtp = $r->recip_addr_smtp;
  22356. if (defined $addr_smtp) {
  22357. $addr_smtp =~ s/^<(.*)>\z/$1/s;
  22358. $addr_smtp =~ s/(\@[^@]+)\z/lc $1/se; # lowercase just a domain part
  22359. }
  22360. my $orig_addr = $r->dsn_orcpt; # RCPT command ORCPT option, RFC 3461
  22361. if (defined $orig_addr) {
  22362. $orig_addr = orcpt_decode($orig_addr);
  22363. $orig_addr =~ s/(\@[^@]+)\z/lc $1/se; # lowercase just a domain part
  22364. } else {
  22365. $orig_addr = $addr_smtp;
  22366. }
  22367. my($rid, $o_rid, $existed);
  22368. if ($addr_smtp ne '') {
  22369. ($rid,$existed) = $self->find_or_save_addr($addr_smtp,$partition_tag);
  22370. # there is perhaps 90-100% chance the recipient addr is already in the db
  22371. if (defined $rid) {
  22372. $r->recip_maddr_id($rid);
  22373. snmp_count('SqlAddrRecipAttempts');
  22374. snmp_count($existed ? 'SqlAddrRecipHits' : 'SqlAddrRecipMisses');
  22375. do_log(4,"save_info_preliminary %s, recip id: %s, %s%s, %s",
  22376. $mail_id, $rid, $addr_smtp,
  22377. $orig_addr eq $addr_smtp ? '' : " (ORCPT $orig_addr)",
  22378. $existed ? 'exists' : 'new');
  22379. }
  22380. }
  22381. ## currently disabled, probably not worth saving into SQL, rarely useful
  22382. # if ($orig_addr ne '' && lc($orig_addr) ne lc($addr_smtp)) {
  22383. # # don't bother saving as a separate record for just a case change
  22384. # ($o_rid,$existed) = $self->find_or_save_addr($orig_addr,$partition_tag,1);
  22385. # if (defined $o_rid) {
  22386. # $r->recip_maddr_id_orig($o_rid);
  22387. # snmp_count('SqlAddrRecipAttempts');
  22388. # snmp_count($existed ? 'SqlAddrRecipHits' : 'SqlAddrRecipMisses');
  22389. # do_log(4,"save_info_preliminary %s, o_recip id: %s, %s, %s",
  22390. # $mail_id, $o_rid, $orig_addr, $existed ? 'exists' : 'new');
  22391. # }
  22392. # }
  22393. }
  22394. my $conn_h = $self->{conn_h}; my $sql_cl_r = cr('sql_clause');
  22395. my $ins_msg = $sql_cl_r->{'ins_msg'};
  22396. if (!defined($ins_msg) || $ins_msg eq '') {
  22397. do_log(4,"save_info_preliminary: ins_msg undef, not saving");
  22398. } elsif (!defined($sid)) {
  22399. do_log(4,"save_info_preliminary: sid undef, not saving");
  22400. } else {
  22401. $conn_h->begin_work; # SQL transaction starts
  22402. eval {
  22403. # MySQL does not like a standard iso8601 delimiter 'T' or a timezone
  22404. # when data type of msgs.time_iso is TIMESTAMP (instead of a string)
  22405. my $time_iso = $timestamp_fmt_mysql && $conn_h->driver_name eq 'mysql'
  22406. ? iso8601_utc_timestamp($msginfo->rx_time,1,'')
  22407. : iso8601_utc_timestamp($msginfo->rx_time);
  22408. # insert a placeholder msgs record with sender information
  22409. $conn_h->execute($ins_msg,
  22410. $partition_tag, $msginfo->mail_id, $msginfo->secret_id,
  22411. $msginfo->log_id, int($msginfo->rx_time), $time_iso,
  22412. untaint($sid), c('policy_bank_path'), untaint($msginfo->client_addr),
  22413. 0+untaint($msginfo->msg_size), untaint(substr(c('myhostname'),0,255)));
  22414. $conn_h->commit; 1;
  22415. } or do {
  22416. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22417. if ($conn_h->in_transaction) {
  22418. eval {
  22419. $conn_h->rollback;
  22420. do_log(1,"save_info_preliminary: rollback done"); 1;
  22421. } or do {
  22422. $@ = "errno=$!" if $@ eq ''; chomp $@;
  22423. do_log(1,"save_info_preliminary: rollback %s", $@);
  22424. die $@ if $@ =~ /^timed out\b/; # resignal timeout
  22425. };
  22426. }
  22427. do_log(-1, "WARN save_info_preliminary: %s", $eval_stat);
  22428. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22429. return 0;
  22430. };
  22431. }
  22432. 1;
  22433. }
  22434. sub save_info_final {
  22435. my($self, $msginfo,$dsn_sent) = @_;
  22436. my $mail_id = $msginfo->mail_id;
  22437. defined $mail_id or die "save_info_final: mail_id still undefined";
  22438. my $sid = $msginfo->sender_maddr_id;
  22439. my $conn_h = $self->{conn_h}; my($sql_cl_r) = cr('sql_clause');
  22440. my $ins_msg = $sql_cl_r->{'ins_msg'};
  22441. my $upd_msg = $sql_cl_r->{'upd_msg'};
  22442. my $ins_rcp = $sql_cl_r->{'ins_rcp'};
  22443. if ($ins_msg eq '' || $upd_msg eq '' || $ins_rcp eq '') {
  22444. # updates disabled
  22445. } elsif (!defined($sid)) {
  22446. # sender not in table maddr, msgs record was not inserted by preliminary
  22447. } else {
  22448. $conn_h->begin_work; # SQL transaction starts
  22449. eval {
  22450. my(%content_short_name) = ( # as written to a SQL record
  22451. CC_VIRUS,'V', CC_BANNED,'B', CC_UNCHECKED,'U',
  22452. CC_SPAM,'S', CC_SPAMMY,'Y', CC_BADH.",2",'M', CC_BADH,'H',
  22453. CC_OVERSIZED,'O', CC_MTA,'T', CC_CLEAN,'C', CC_CATCHALL,'?');
  22454. my($min_spam_level, $max_spam_level) =
  22455. minmax(map($_->spam_level, @{$msginfo->per_recip_data}));
  22456. # insert per-recipient records into table msgrcpt
  22457. my $r_seq_num = 0; # can serve as a component of a primary key
  22458. for my $r (@{$msginfo->per_recip_data}) {
  22459. $r_seq_num++;
  22460. my $rid = $r->recip_maddr_id;
  22461. next if !defined $rid; # e.g. always_bcc, or table 'maddr' is disabled
  22462. my $o_rid = $r->recip_maddr_id_orig; # may be undef
  22463. my $spam_level = $r->spam_level;
  22464. my($dest,$resp) = ($r->recip_destiny, $r->recip_smtp_response);
  22465. my $d = $resp=~/^4/ ? 'TEMPFAIL'
  22466. : ($dest==D_BOUNCE && $resp=~/^5/) ? 'BOUNCE'
  22467. : ($dest!=D_BOUNCE && $resp=~/^5/) ? 'REJECT'
  22468. : ($dest==D_PASS && ($resp=~/^2/ || !$r->recip_done)) ? 'PASS'
  22469. : ($dest==D_DISCARD) ? 'DISCARD' : '?';
  22470. my $r_content_type =
  22471. $r->setting_by_contents_category(\%content_short_name);
  22472. for ($r_content_type) { $_ = ' ' if !defined $_ || /^ *\z/ }
  22473. $resp = substr($resp,0,255) if length($resp) > 255;
  22474. $resp =~ s/[^\040-\176]/?/gs; # just in case, only need 7 bit printbl
  22475. # avoid op '?:' on tainted operand in args list, see PR [perl #81028]
  22476. my $recip_local_yn = $r->recip_is_local ? 'Y' : 'N';
  22477. my $blacklisted_yn = $r->recip_blacklisted_sender ? 'Y' : 'N';
  22478. my $whitelisted_yn = $r->recip_whitelisted_sender ? 'Y' : 'N';
  22479. $conn_h->execute($ins_rcp,
  22480. $msginfo->partition_tag, $mail_id,
  22481. $sql_schema_version < 2.007000 ? untaint($rid)
  22482. : ($r_seq_num, untaint($rid), $recip_local_yn, $r_content_type),
  22483. substr($d,0,1), ' ',
  22484. $blacklisted_yn, $whitelisted_yn, 0+untaint($spam_level),
  22485. untaint($resp),
  22486. );
  22487. # untaint(defined $o_rid ? $o_rid : $rid),
  22488. # int($msginfo->rx_time),
  22489. # untaint($r->user_policy_id),
  22490. }
  22491. my $q_to = $msginfo->quarantined_to; # ref to a list of quar. locations
  22492. if (!defined($q_to) || !@$q_to) { $q_to = undef }
  22493. else {
  22494. $q_to = $q_to->[0]; # keep only the first quarantine location
  22495. $q_to =~ s{^\Q$QUARANTINEDIR\E/}{}; # strip directory name
  22496. }
  22497. my $m_id = $msginfo->get_header_field_body('message-id');
  22498. $m_id = join(' ',parse_message_id($m_id)) if $m_id ne ''; # strip CFWS
  22499. my $subj = $msginfo->get_header_field_body('subject');
  22500. my $from = $msginfo->get_header_field_body('from'); # raw full field
  22501. my $rfc2822_from = $msginfo->rfc2822_from; # undef, scalar or listref
  22502. my $rfc2822_sender = $msginfo->rfc2822_sender; # undef or scalar
  22503. $rfc2822_from = join(', ',@$rfc2822_from) if ref $rfc2822_from;
  22504. my $os_fp = $msginfo->client_os_fingerprint;
  22505. $_ = !defined($_) ? '' :untaint($_) for ($subj,$from,$m_id,$q_to,$os_fp);
  22506. for ($subj,$from) { # character set decoding, sanitation
  22507. chomp; s/\n(?=[ \t])//gs; s/^[ \t]+//s; s/[ \t]+\z//s; # unfold, trim
  22508. eval { # convert to UTF-8 octets, truncate to 255 bytes
  22509. local($1);
  22510. my $chars = safe_decode('MIME-Header',$_); # logical characters
  22511. my $octets = safe_encode_utf8($chars); # bytes, UTF-8 encoded
  22512. if (length($octets) > 255 &&
  22513. $octets =~ /^ (.{0,255}) (?= [\x00-\x7F\xC0-\xFF] | \z )/xs) {
  22514. $octets = $1; # cleanly chop a UTF-8 byte sequence, RFC 3629
  22515. $chars = safe_decode('UTF-8',$octets); # convert back to chars
  22516. }
  22517. # man DBI: Drivers should accept [unicode and non-unicode] strings
  22518. # and, if required, convert them to the character set of the
  22519. # database being used. Similarly, when fetching from the database
  22520. # character data that isn't iso-8859-1 the driver should convert
  22521. # it into UTF-8.
  22522. # $_ = $chars; 1; # pass logical characters to SQL
  22523. $_ = $octets; 1; # pass bytes to SQL, works better
  22524. } or do {
  22525. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22526. do_log(1,"save_info_final INFO: header field ".
  22527. "not decodable, keeping raw bytes: %s", $eval_stat);
  22528. $_ = substr($_,0,255) if length($_) > 255;
  22529. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22530. };
  22531. }
  22532. for ($m_id,$q_to,$os_fp) { # truncate to 255 ch, ensure 7-bit characters
  22533. $_ = substr($_,0,255) if length($_) > 255;
  22534. s/[^\040-\176]/?/gs; # only use 7 bit printable, compatible with UTF-8
  22535. }
  22536. my $content_type =
  22537. $msginfo->setting_by_contents_category(\%content_short_name);
  22538. my $checks_performed = $msginfo->checks_performed;
  22539. $checks_performed = !ref $checks_performed ? ''
  22540. : join('', grep($checks_performed->{$_}, qw(V S H B F P D)));
  22541. my $q_type = $msginfo->quar_type;
  22542. # only keep the first quarantine type used (e.g. ignore archival quar.)
  22543. $q_type = $q_type->[0] if ref $q_type;
  22544. for ($q_type,$content_type) { $_ = ' ' if !defined $_ || /^ *\z/ }
  22545. $min_spam_level = 0 if !defined $min_spam_level;
  22546. $max_spam_level = 0 if !defined $max_spam_level;
  22547. my $orig = $msginfo->originating ? 'Y' : 'N';
  22548. ll(4) && do_log(4,"save_info_final %s, orig=%s, chks=%s, cont.ty=%s, ".
  22549. "q.type=%s, q.to=%s, dsn=%s, score=%s, ".
  22550. "Message-ID: %s, From: '%s', Subject: '%s'",
  22551. $mail_id, $orig, $checks_performed, $content_type,
  22552. $q_type, $q_to, $dsn_sent, $min_spam_level,
  22553. $m_id, sanitize_str($from), sanitize_str($subj));
  22554. # update message record with additional information
  22555. $conn_h->execute($upd_msg,
  22556. $content_type, $q_type, $q_to, $dsn_sent,
  22557. 0+untaint($min_spam_level), $m_id, $from, $subj,
  22558. untaint($msginfo->client_addr), # we may have a better info now
  22559. $sql_schema_version < 2.007000 ? () : $orig,
  22560. $msginfo->partition_tag, $mail_id);
  22561. # $os_fp, $rfc2822_sender, $rfc2822_from, $checks_performed, ...
  22562. # SQL_CHAR, SQL_VARCHAR, SQL_VARBINARY, SQL_BLOB, SQL_INTEGER, SQL_FLOAT,
  22563. # SQL_TIMESTAMP, SQL_TYPE_TIMESTAMP_WITH_TIMEZONE, ...
  22564. $conn_h->commit; 1;
  22565. } or do {
  22566. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22567. if ($conn_h->in_transaction) {
  22568. eval {
  22569. $conn_h->rollback;
  22570. do_log(1,"save_info_final: rollback done"); 1;
  22571. } or do {
  22572. $@ = "errno=$!" if $@ eq ''; chomp $@;
  22573. do_log(1,"save_info_final: rollback %s", $@);
  22574. die $@ if $@ =~ /^timed out\b/; # resignal timeout
  22575. };
  22576. }
  22577. do_log(-1, "WARN save_info_final: %s", $eval_stat);
  22578. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22579. return 0;
  22580. }
  22581. }
  22582. 1;
  22583. }
  22584. 1;
  22585. __DATA__
  22586. #
  22587. package Amavis::IO::SQL;
  22588. # an IO wrapper around SQL for inserting/retrieving mail text
  22589. # to/from a database
  22590. use strict;
  22591. use re 'taint';
  22592. use warnings;
  22593. use warnings FATAL => qw(utf8 void);
  22594. no warnings 'uninitialized';
  22595. BEGIN {
  22596. require Exporter;
  22597. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  22598. $VERSION = '2.316';
  22599. @ISA = qw(Exporter);
  22600. import Amavis::Util qw(ll do_log untaint min max minmax);
  22601. }
  22602. use Errno qw(ENOENT EACCES EIO);
  22603. use DBI qw(:sql_types);
  22604. # use DBD::Pg;
  22605. sub new {
  22606. my $class = shift; my $self = bless {}, $class;
  22607. if (@_) { $self->open(@_) or return }
  22608. $self;
  22609. }
  22610. sub open {
  22611. my $self = shift;
  22612. if (exists $self->{conn_h}) {
  22613. eval { $self->close } or 1; # ignore failure, make perlcritic happy
  22614. }
  22615. @$self{qw(conn_h clause dbkey mode partition_tag maxbuf rx_time)} = @_;
  22616. my $conn_h = $self->{conn_h}; $self->{buf} = '';
  22617. $self->{chunk_ind} = $self->{pos} = $self->{bufpos} = $self->{eof} = 0;
  22618. my $driver; my $eval_stat;
  22619. eval { $driver = $conn_h->driver_name; 1 }
  22620. or do { $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat };
  22621. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  22622. if ($self->{mode} eq 'w') { # open for write access
  22623. ll(4) && do_log(4,"Amavis::IO::SQL::open %s drv=%s (%s); key=%s, p_tag=%s",
  22624. $self->{mode}, $driver, $self->{clause},
  22625. $self->{dbkey}, $self->{partition_tag});
  22626. } else { # open for read access
  22627. $eval_stat = undef;
  22628. eval {
  22629. $conn_h->execute($self->{clause}, $self->{partition_tag},$self->{dbkey});
  22630. 1;
  22631. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat };
  22632. my $ll = $eval_stat ne '' ? -1 : 4;
  22633. do_log($ll,"Amavis::IO::SQL::open %s drv=%s (%s); key=%s, p_tag=%s, s: %s",
  22634. $self->{mode}, $driver, $self->{clause},
  22635. $self->{dbkey}, $self->{partition_tag}, $eval_stat) if ll($ll);
  22636. if ($eval_stat ne '') {
  22637. if ($eval_stat =~ /^timed out\b/) { die $eval_stat } # resignal timeout
  22638. else { die "Amavis::IO::SQL::open $driver SELECT error: $eval_stat" }
  22639. $! = EIO; return; # not reached
  22640. }
  22641. $eval_stat = undef;
  22642. eval { # fetch the first chunk; if missing treat it as a file-not-found
  22643. my $a_ref = $conn_h->fetchrow_arrayref($self->{clause});
  22644. if (!defined($a_ref)) { $self->{eof} = 1 }
  22645. else { $self->{buf} = $a_ref->[0]; $self->{chunk_ind}++ }
  22646. 1;
  22647. } or do {
  22648. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22649. if ($eval_stat =~ /^timed out\b/) { die $eval_stat } # resignal timeout
  22650. else { die "Amavis::IO::SQL::open $driver read error: $eval_stat" }
  22651. $! = EIO; return; # not reached
  22652. };
  22653. if ($self->{eof}) { # no records, make it look like a missing file
  22654. do_log(0,"Amavis::IO::SQL::open key=%s, p_tag=%s: no such record",
  22655. $self->{dbkey}, $self->{partition_tag});
  22656. $! = ENOENT; # No such file or directory
  22657. return;
  22658. }
  22659. }
  22660. $self;
  22661. }
  22662. sub DESTROY {
  22663. my $self = shift;
  22664. local($@,$!,$_); my $myactualpid = $$;
  22665. if (ref $self && $self->{conn_h}) {
  22666. eval {
  22667. $self->close or die "Error closing: $!"; 1;
  22668. } or do {
  22669. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22670. warn "[$myactualpid] Amavis::IO::SQL::close error: $eval_stat";
  22671. };
  22672. delete $self->{conn_h};
  22673. }
  22674. }
  22675. sub close {
  22676. my $self = shift;
  22677. my $eval_stat;
  22678. eval {
  22679. if ($self->{mode} eq 'w') {
  22680. $self->flush or die "Can't flush: $!";
  22681. } elsif ($self->{conn_h} && $self->{clause} && !$self->{eof}) {
  22682. # reading, closing before eof was reached
  22683. $self->{conn_h}->finish($self->{clause}) or die "Can't finish: $!";
  22684. };
  22685. 1;
  22686. } or do {
  22687. $eval_stat = $@ ne '' ? $@ : "errno=$!";
  22688. };
  22689. delete @$self{
  22690. qw(conn_h clause dbkey mode maxbuf rx_time buf chunk_ind pos bufpos eof) };
  22691. if (defined $eval_stat) {
  22692. chomp $eval_stat;
  22693. if ($eval_stat =~ /^timed out\b/) { die $eval_stat } # resignal timeout
  22694. else { die "Error closing, $eval_stat" }
  22695. $! = EIO; return; # not reached
  22696. }
  22697. 1;
  22698. }
  22699. sub seek {
  22700. my($self,$pos,$whence) = @_;
  22701. $whence == 0 or die "Only absolute seek is supported on sql i/o";
  22702. $pos >= 0 or die "Can't seek to a negative absolute position on sql i/o";
  22703. ll(5) && do_log(5, "Amavis::IO::SQL::seek mode=%s, pos=%s",
  22704. $self->{mode}, $pos);
  22705. $self->{mode} ne 'w'
  22706. or die "Seek to $whence,$pos on sql i/o only supported for read mode";
  22707. if ($pos < $self->{pos}) {
  22708. if (!$self->{eof} && $self->{chunk_ind} <= 1) {
  22709. # still in the first chunk, just reset pos
  22710. $self->{pos} = $self->{bufpos} = 0; # reset
  22711. } else { # beyond the first chunk, restart the query from the beginning
  22712. my($con,$clause,$key,$mode,$partition_tag,$maxb,$rx_time) =
  22713. @$self{qw(conn_h clause dbkey mode partition_tag maxbuf rx_time)};
  22714. $self->close or die "seek: error closing, $!";
  22715. $self->open($con,$clause,$key,$mode,$partition_tag,$maxb,$rx_time)
  22716. or die "seek: reopen failed: $!";
  22717. }
  22718. }
  22719. my $skip = $pos - $self->{pos};
  22720. if ($skip > 0) {
  22721. my $s; my $nbytes = $self->read($s,$skip); # acceptable for small skips
  22722. defined $nbytes or die "seek: error skipping $skip bytes on sql i/o: $!";
  22723. }
  22724. 1; # seek is supposed to return 1 upon success, 0 otherwise
  22725. }
  22726. sub read { # SCALAR,LENGTH,OFFSET
  22727. my $self = shift; my $req_len = $_[1]; my $offset = $_[2];
  22728. my $conn_h = $self->{conn_h}; my $a_ref;
  22729. ll(5) && do_log(5, "Amavis::IO::SQL::read, %d, %d",
  22730. $self->{chunk_ind}, $self->{bufpos});
  22731. eval {
  22732. while (!$self->{eof} && length($self->{buf})-$self->{bufpos} < $req_len) {
  22733. $a_ref = $conn_h->fetchrow_arrayref($self->{clause});
  22734. if (!defined($a_ref)) { $self->{eof} = 1 }
  22735. else { $self->{buf} .= $a_ref->[0]; $self->{chunk_ind}++ }
  22736. }
  22737. 1;
  22738. } or do {
  22739. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22740. # we can't stash an arbitrary error message string into $!,
  22741. # which forces us to use 'die' to properly report an error
  22742. if ($eval_stat =~ /^timed out\b/) { die $eval_stat } # resignal timeout
  22743. else { die "read: sql select failed, $eval_stat" }
  22744. $! = EIO; return; # not reached
  22745. };
  22746. my $nbytes;
  22747. if (!defined($offset) || $offset == 0) {
  22748. $_[0] = substr($self->{buf}, $self->{bufpos}, $req_len);
  22749. $nbytes = length($_[0]);
  22750. } else {
  22751. my $buff = substr($self->{buf}, $self->{bufpos}, $req_len);
  22752. substr($_[0],$offset) = $buff; $nbytes = length($buff);
  22753. }
  22754. $self->{bufpos} += $nbytes; $self->{pos} += $nbytes;
  22755. if ($self->{bufpos} > 0 && $self->{chunk_ind} > 1) {
  22756. # discard used-up part of the buf unless at ch.1, which may still be useful
  22757. ll(5) && do_log(5,"read: moving on by %d chars", $self->{bufpos});
  22758. $self->{buf} = substr($self->{buf},$self->{bufpos}); $self->{bufpos} = 0;
  22759. }
  22760. $nbytes; # eof: 0, error: undef
  22761. }
  22762. sub getline {
  22763. my $self = shift; my $conn_h = $self->{conn_h};
  22764. ll(5) && do_log(5, "Amavis::IO::SQL::getline, chunk %d, pos %d",
  22765. $self->{chunk_ind}, $self->{bufpos});
  22766. my($a_ref,$line); my $ind = -1;
  22767. eval {
  22768. while (!$self->{eof} &&
  22769. ($ind=index($self->{buf},"\n",$self->{bufpos})) < 0) {
  22770. $a_ref = $conn_h->fetchrow_arrayref($self->{clause});
  22771. if (!defined($a_ref)) { $self->{eof} = 1 }
  22772. else { $self->{buf} .= $a_ref->[0]; $self->{chunk_ind}++ }
  22773. }
  22774. 1;
  22775. } or do {
  22776. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22777. if ($eval_stat =~ /^timed out\b/) { die $eval_stat } # resignal timeout
  22778. else { die "getline: reading sql select results failed, $eval_stat" }
  22779. $! = EIO; return; # not reached
  22780. };
  22781. if ($ind < 0 && $self->{eof}) # imply a NL before eof if missing
  22782. { $self->{buf} .= "\n"; $ind = index($self->{buf}, "\n", $self->{bufpos}) }
  22783. $ind >= 0 or die "Programming error, NL not found";
  22784. if (length($self->{buf}) > $self->{bufpos}) { # nonempty buffer?
  22785. $line = substr($self->{buf}, $self->{bufpos}, $ind+1-$self->{bufpos});
  22786. my $nbytes = length($line);
  22787. $self->{bufpos} += $nbytes; $self->{pos} += $nbytes;
  22788. if ($self->{bufpos} > 0 && $self->{chunk_ind} > 1) {
  22789. # discard used part of the buf unless at ch.1, which may still be useful
  22790. ll(5) && do_log(5,"getline: moving on by %d chars", $self->{bufpos});
  22791. $self->{buf} = substr($self->{buf},$self->{bufpos}); $self->{bufpos} = 0;
  22792. }
  22793. }
  22794. # eof: undef, $! zero; error: undef, $! nonzero
  22795. $! = 0; $line eq '' ? undef : $line;
  22796. }
  22797. sub flush {
  22798. my $self = shift;
  22799. return if $self->{mode} ne 'w';
  22800. my $msg; my $conn_h = $self->{conn_h};
  22801. while (length($self->{buf}) > 0) {
  22802. my $ind = $self->{chunk_ind} + 1;
  22803. ll(4) && do_log(4, "sql flush: key: (%s, %d), p_tag=%s, rx_t=%d, size=%d",
  22804. $self->{dbkey}, $ind, $self->{partition_tag}, $self->{rx_time},
  22805. min(length($self->{buf}),$self->{maxbuf}));
  22806. eval {
  22807. my $driver = $conn_h->driver_name;
  22808. $conn_h->execute($self->{clause},
  22809. $self->{partition_tag}, $self->{dbkey}, $ind,
  22810. # int($self->{rx_time}),
  22811. [ untaint(substr($self->{buf},0,$self->{maxbuf})),
  22812. $driver eq 'Pg' ? { pg_type => DBD::Pg::PG_BYTEA() }
  22813. : SQL_BLOB ] );
  22814. 1;
  22815. } or do {
  22816. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22817. $msg = $eval_stat;
  22818. };
  22819. last if defined $msg;
  22820. substr($self->{buf},0,$self->{maxbuf}) = ''; $self->{chunk_ind} = $ind;
  22821. }
  22822. if (defined $msg) {
  22823. chomp $msg;
  22824. if ($msg =~ /^timed out\b/) { die $msg } # resignal timeout
  22825. else {
  22826. $msg = "flush: sql inserting text failed, $msg";
  22827. die $msg; # we can't stash an arbitrary error message string into $!,
  22828. # which forces us to use 'die' to properly report an error
  22829. }
  22830. $! = EIO; return; # not reached
  22831. }
  22832. 1;
  22833. }
  22834. sub print {
  22835. my $self = shift;
  22836. $self->{mode} eq 'w' or die "Can't print, not opened for writing";
  22837. my $buff_ref = @_ == 1 ? \$_[0] : \join('',@_);
  22838. my $len = length($$buff_ref);
  22839. my $nbytes; my $conn_h = $self->{conn_h};
  22840. if ($len <= 0) { $nbytes = "0 but true" }
  22841. else {
  22842. $self->{buf} .= $$buff_ref; $self->{pos} += $len; $nbytes = $len;
  22843. while (length($self->{buf}) >= $self->{maxbuf}) {
  22844. my $ind = $self->{chunk_ind} + 1;
  22845. ll(4) && do_log(4, "sql print: key: (%s, %d), p_tag=%s, size=%d",
  22846. $self->{dbkey}, $ind,
  22847. $self->{partition_tag}, $self->{maxbuf});
  22848. eval {
  22849. my $driver = $conn_h->driver_name;
  22850. $conn_h->execute($self->{clause},
  22851. $self->{partition_tag}, $self->{dbkey}, $ind,
  22852. # int($self->{rx_time}),
  22853. [ untaint(substr($self->{buf},0,$self->{maxbuf})),
  22854. $driver eq 'Pg' ? { pg_type => DBD::Pg::PG_BYTEA() }
  22855. : SQL_BLOB ] );
  22856. 1;
  22857. } or do {
  22858. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  22859. # we can't stash an arbitrary error message string into $!,
  22860. # which forces us to use 'die' to properly report an error
  22861. if ($eval_stat =~ /^timed out\b/) { die $eval_stat } # resignal timeout
  22862. else { die "print: sql inserting mail text failed, $eval_stat" }
  22863. $! = EIO; return; # not reached
  22864. };
  22865. substr($self->{buf},0,$self->{maxbuf}) = ''; $self->{chunk_ind} = $ind;
  22866. }
  22867. }
  22868. $nbytes;
  22869. }
  22870. sub printf { shift->print(sprintf(shift,@_)) }
  22871. 1;
  22872. #^L
  22873. package Amavis::Out::SQL::Quarantine;
  22874. use strict;
  22875. use re 'taint';
  22876. use warnings;
  22877. use warnings FATAL => qw(utf8 void);
  22878. no warnings 'uninitialized';
  22879. BEGIN {
  22880. require Exporter;
  22881. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  22882. $VERSION = '2.316';
  22883. @ISA = qw(Exporter);
  22884. @EXPORT = qw(&mail_via_sql);
  22885. import Amavis::Conf qw(:platform c cr ca $sql_quarantine_chunksize_max);
  22886. import Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local);
  22887. import Amavis::Util qw(ll do_log snmp_count collect_equal_delivery_recips);
  22888. import Amavis::Timing qw(section_time);
  22889. import Amavis::Out::SQL::Connection ();
  22890. }
  22891. use subs @EXPORT;
  22892. use DBI qw(:sql_types);
  22893. sub mail_via_sql {
  22894. my($conn_h,
  22895. $msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_;
  22896. my(@snmp_vars) = !$initial_submission ?
  22897. ('', 'Relay', 'ProtoSQL', 'ProtoSQLRelay')
  22898. : ('', 'Submit', 'ProtoSQL', 'ProtoSQLSubmit',
  22899. 'Submit'.$initial_submission);
  22900. snmp_count('OutMsgs'.$_) for @snmp_vars;
  22901. my $logmsg =
  22902. sprintf("%s via SQL (%s): %s", ($initial_submission?'SEND':'FWD'),
  22903. $conn_h->dsn_current, $msginfo->sender_smtp);
  22904. my($per_recip_data_ref, $proto_sockname) =
  22905. collect_equal_delivery_recips($msginfo, $filter, qr/^sql:/i);
  22906. if (!$per_recip_data_ref || !@$per_recip_data_ref) {
  22907. do_log(5, "%s, nothing to do", $logmsg); return 1;
  22908. }
  22909. my $mail_id = $msginfo->mail_id;
  22910. defined $mail_id or die "mail_via_sql: mail_id still undefined";
  22911. $proto_sockname = $proto_sockname->[0] if ref $proto_sockname;
  22912. ll(1) && do_log(1, "delivering to %s, %s -> %s, mail_id %s",
  22913. $proto_sockname, $logmsg,
  22914. join(',', qquote_rfc2821_local(
  22915. map($_->recip_final_addr, @$per_recip_data_ref)) ),
  22916. $mail_id);
  22917. my $msg = $msginfo->mail_text; # a scalar reference, or a file handle
  22918. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  22919. $msg = $msg_str_ref if ref $msg_str_ref;
  22920. my($err,$smtp_response);
  22921. eval {
  22922. my $sql_cl_r = cr('sql_clause');
  22923. $conn_h->begin_work; # SQL transaction starts
  22924. eval {
  22925. my $mp = Amavis::IO::SQL->new;
  22926. $mp->open($conn_h, $sql_cl_r->{'ins_quar'}, $msginfo->mail_id, 'w',
  22927. $msginfo->partition_tag, $sql_quarantine_chunksize_max,
  22928. $msginfo->rx_time)
  22929. or die "Can't open Amavis::IO::SQL object: $!";
  22930. my $hdr_edits = $msginfo->header_edits;
  22931. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  22932. my($received_cnt,$file_position) =
  22933. $hdr_edits->write_header($msginfo,$mp,!$initial_submission);
  22934. if ($received_cnt > 100) { # loop detection required by RFC 5321 sect 6.2
  22935. die "Too many hops: $received_cnt 'Received:' header fields";
  22936. } elsif (!defined $msg) {
  22937. # empty mail
  22938. } elsif (ref $msg eq 'SCALAR') {
  22939. $mp->print(substr($$msg,$file_position))
  22940. or die "Can't write to SQL storage: $!";
  22941. } elsif ($msg->isa('MIME::Entity')) {
  22942. $msg->print_body($mp);
  22943. } else {
  22944. my($nbytes,$buff);
  22945. while (($nbytes = $msg->read($buff,32768)) > 0) {
  22946. $mp->print($buff) or die "Can't write to SQL storage: $!";
  22947. }
  22948. defined $nbytes or die "Error reading: $!";
  22949. }
  22950. $mp->close or die "Error closing Amavis::IO::SQL object: $!";
  22951. $conn_h->commit; 1;
  22952. } or do {
  22953. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err; my $msg = $err;
  22954. $msg = "writing mail text to SQL failed: $msg"; do_log(0,"%s",$msg);
  22955. if ($conn_h->in_transaction) {
  22956. eval {
  22957. $conn_h->rollback;
  22958. do_log(1,"mail_via_sql: rollback done"); 1;
  22959. } or do {
  22960. $@ = "errno=$!" if $@ eq ''; chomp $@;
  22961. do_log(1,"mail_via_sql: rollback %s", $@);
  22962. die $@ if $@ =~ /^timed out\b/; # resignal timeout
  22963. };
  22964. }
  22965. die $err if $err =~ /^timed out\b/; # resignal timeout
  22966. die $msg;
  22967. };
  22968. 1;
  22969. } or do { $err = $@ ne '' ? $@ : "errno=$!" };
  22970. if ($err eq '') {
  22971. $smtp_response = "250 2.6.0 Ok, Stored to sql db as mail_id $mail_id";
  22972. snmp_count('OutMsgsDelivers');
  22973. my $size = $msginfo->msg_size;
  22974. snmp_count( ['OutMsgsSize'.$_, $size, 'C64'] ) for @snmp_vars;
  22975. } else {
  22976. chomp $err;
  22977. if ($err =~ /too many hops\b/i) {
  22978. $smtp_response = "554 5.4.6 Reject: $err";
  22979. snmp_count('OutMsgsRejects');
  22980. } else {
  22981. $smtp_response =
  22982. "451 4.5.0 Storing to sql db as mail_id $mail_id failed: $err";
  22983. snmp_count('OutMsgsAttemptFails');
  22984. }
  22985. die $err if $err =~ /^timed out\b/; # resignal timeout
  22986. }
  22987. $smtp_response .= ", id=" . $msginfo->log_id;
  22988. for my $r (@$per_recip_data_ref) {
  22989. next if $r->recip_done;
  22990. $r->recip_smtp_response($smtp_response); $r->recip_done(2);
  22991. if ($smtp_response =~ /^2/) {
  22992. my $mbxname = $mail_id;
  22993. my $p_tag = $msginfo->partition_tag;
  22994. $mbxname .= '[' . $p_tag . ']'
  22995. if defined($p_tag) && $p_tag ne '' && $p_tag ne '0';
  22996. $r->recip_mbxname($mbxname);
  22997. }
  22998. }
  22999. section_time('fwd-sql');
  23000. 1;
  23001. }
  23002. 1;
  23003. __DATA__
  23004. #
  23005. package Amavis::AV;
  23006. use strict;
  23007. use re 'taint';
  23008. use warnings;
  23009. use warnings FATAL => qw(utf8 void);
  23010. no warnings 'uninitialized';
  23011. BEGIN {
  23012. require Exporter;
  23013. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  23014. $VERSION = '2.316';
  23015. @ISA = qw(Exporter);
  23016. import Amavis::Conf qw(:platform :confvars c cr ca);
  23017. import Amavis::Util qw(ll untaint min max minmax unique_list do_log
  23018. add_entropy proto_decode rmdir_recursively
  23019. prolong_timer get_deadline);
  23020. import Amavis::ProcControl qw(exit_status_str proc_status_ok
  23021. run_command run_as_subprocess
  23022. collect_results collect_results_structured);
  23023. import Amavis::Lookup qw(lookup lookup2);
  23024. import Amavis::Timing qw(section_time);
  23025. import Amavis::Out qw(mail_dispatch);
  23026. import Amavis::rfc2821_2822_Tools qw(one_response_for_all);
  23027. }
  23028. use subs @EXPORT_OK;
  23029. use vars @EXPORT;
  23030. use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
  23031. WEXITSTATUS WTERMSIG WSTOPSIG);
  23032. use Errno qw(EPIPE ENOTCONN ENOENT EACCES EINTR EAGAIN ECONNRESET);
  23033. use Time::HiRes ();
  23034. use vars qw(%st_socket_created %st_sock); # keep persistent state (per-socket)
  23035. sub clamav_module_init($) {
  23036. my($av_name) = @_;
  23037. # each child should reinitialize clamav module to reload databases
  23038. my $clamav_version = Mail::ClamAV->VERSION;
  23039. my $dbdir = Mail::ClamAV::retdbdir();
  23040. my $clamav_obj = Mail::ClamAV->new($dbdir);
  23041. ref $clamav_obj
  23042. or die "$av_name: Can't load db from $dbdir: $Mail::ClamAV::Error";
  23043. $clamav_obj->buildtrie;
  23044. $clamav_obj->maxreclevel($MAXLEVELS) if $MAXLEVELS > 0;
  23045. $clamav_obj->maxfiles($MAXFILES) if $MAXFILES > 0;
  23046. $clamav_obj->maxfilesize($MAX_EXPANSION_QUOTA || 30*1024*1024);
  23047. if ($clamav_version >= 0.12) {
  23048. $clamav_obj->maxratio($MAX_EXPANSION_FACTOR);
  23049. # $clamav_obj->archivememlim(0); # limit memory usage for bzip2 (0/1)
  23050. }
  23051. do_log(3,"clamav_module_init: %s init", $av_name);
  23052. section_time('clamav_module_init');
  23053. ($clamav_obj,$clamav_version);
  23054. }
  23055. # called from sub ask_clamav or ask_daemon, should not run as a subprocess
  23056. #
  23057. use vars qw($clamav_obj $clamav_version);
  23058. sub clamav_module_internal_pre($) {
  23059. my($av_name) = @_;
  23060. if (!defined $clamav_obj) {
  23061. ($clamav_obj,$clamav_version) = clamav_module_init($av_name); # first time
  23062. } elsif ($clamav_obj->statchkdir) { # db reload needed?
  23063. do_log(2, "%s: reloading virus database", $av_name);
  23064. ($clamav_obj,$clamav_version) = clamav_module_init($av_name);
  23065. }
  23066. }
  23067. # called from sub ask_clamav or ask_daemon, may be called directly
  23068. # or in a subprocess
  23069. #
  23070. sub clamav_module_internal($@) {
  23071. my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name) = @_;
  23072. $query = join(' ',@$query) if ref $query;
  23073. my $fname = "$tempdir/parts/$query"; # file to be checked
  23074. my $part = $names_to_parts->{$query}; # get corresponding parts object
  23075. my $options = 0; # bitfield of options to Mail::ClamAV::scan
  23076. my($opt_archive,$opt_mail);
  23077. if ($clamav_version < 0.12) {
  23078. $opt_archive = &Mail::ClamAV::CL_ARCHIVE;
  23079. $opt_mail = &Mail::ClamAV::CL_MAIL;
  23080. } else { # >= 0.12, reflects renamed flags in libclamav 0.80
  23081. $opt_archive = &Mail::ClamAV::CL_SCAN_ARCHIVE;
  23082. $opt_mail = &Mail::ClamAV::CL_SCAN_MAIL;
  23083. }
  23084. # see clamav.h for standard options enabled by CL_SCAN_STDOPT
  23085. $options |= &Mail::ClamAV::CL_SCAN_STDOPT if $clamav_version >= 0.13;
  23086. $options |= $opt_archive; # turn on ARCHIVE
  23087. $options &= ~$opt_mail; # turn off MAIL
  23088. if (ref($part) && ($part->type_short eq 'MAIL' ||
  23089. lc($part->type_declared) eq 'message/rfc822')) {
  23090. do_log(2, "%s: $query - enabling option CL_MAIL", $av_name);
  23091. $options |= $opt_mail; # turn on MAIL
  23092. }
  23093. my $ret = $clamav_obj->scan(untaint($fname), $options);
  23094. my($output,$status);
  23095. if ($ret->virus) { $status = 1; $output = "INFECTED: $ret" }
  23096. elsif ($ret->clean) { $status = 0; $output = "CLEAN" }
  23097. else { $status = 2; $output = $ret->error.", errno=".$ret->errno }
  23098. ($status,$output); # return synthesised status and a result string
  23099. }
  23100. # subroutine available for calling from @av_scanners list entries;
  23101. # it has the same args and returns as run_av() below
  23102. #
  23103. sub ask_clamav {
  23104. my($bare_fnames,$names_to_parts,$tempdir, $av_name) = @_;
  23105. clamav_module_internal_pre($av_name); # must not run as a subprocess
  23106. # my(@results) = ask_av(\&clamav_module_internal, @_); # invoke directly
  23107. my($proc_fh,$pid) = run_as_subprocess(\&ask_av, \&clamav_module_internal,@_);
  23108. my($results_ref,$child_stat) =
  23109. collect_results_structured($proc_fh,$pid,$av_name,200*1024);
  23110. !$results_ref ? () : @$results_ref;
  23111. }
  23112. my $savi_obj;
  23113. sub sophos_savi_init {
  23114. my($av_name, $command) = @_;
  23115. my(@savi_bool_options) = qw(
  23116. GrpArchiveUnpack GrpSelfExtract GrpExecutable GrpInternet GrpMSOffice
  23117. GrpMisc !GrpDisinfect !GrpClean EnableAutoStop FullSweep FullPdf Xml
  23118. );
  23119. $savi_obj = SAVI->new;
  23120. ref $savi_obj or die "$av_name: Can't create SAVI object, err=$savi_obj";
  23121. my $status = $savi_obj->load_data;
  23122. !defined($status) or die "$av_name: Failed to load SAVI virus data " .
  23123. $savi_obj->error_string($status) . " ($status)";
  23124. my $version = $savi_obj->version;
  23125. ref $version or die "$av_name: Can't get SAVI version, err=$version";
  23126. do_log(2,"%s init: Version %s (engine %d.%d) recognizing %d viruses",
  23127. $av_name, $version->string, $version->major, $version->minor,
  23128. $version->count);
  23129. my $error;
  23130. if ($MAXLEVELS > 0) {
  23131. $error = $savi_obj->set('MaxRecursionDepth', $MAXLEVELS);
  23132. !defined $error
  23133. or die "$av_name: error setting MaxRecursionDepth: err=$error";
  23134. }
  23135. $error = $savi_obj->set('NamespaceSupport', 3); # new with Sophos 3.67
  23136. !defined $error
  23137. or do_log(-1,"%s: error setting NamespaceSupport: err=%s",$av_name,$error);
  23138. for (@savi_bool_options) {
  23139. my $value = /^!/ ? 0 : 1; s/^!+//;
  23140. $error = $savi_obj->set($_, $value);
  23141. !defined $error or die "$av_name: Error setting $_: err=$error";
  23142. }
  23143. section_time('sophos_savi_init');
  23144. 1;
  23145. }
  23146. sub sophos_savi_stale {
  23147. defined $savi_obj && $savi_obj->stale;
  23148. }
  23149. # run by a master(!) process, invoked from a hook run_n_children_hook
  23150. #
  23151. sub sophos_savi_reload {
  23152. if (defined $savi_obj) {
  23153. do_log(3,"sophos_savi_reload: about to reload SAVI data");
  23154. eval {
  23155. my $status = $savi_obj->load_data;
  23156. do_log(-1,"sophos_savi_reload: failed to load SAVI virus data %s (%s)",
  23157. $savi_obj->error_string($status), $status) if defined $status;
  23158. 1;
  23159. } or do {
  23160. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  23161. do_log(-1,"sophos_savi_reload failed: %s", $eval_stat);
  23162. };
  23163. my $version = $savi_obj->version;
  23164. if (!ref($version)) {
  23165. do_log(-1,"sophos_savi_reload: Can't get SAVI version: %s", $version);
  23166. } else {
  23167. do_log(2,"Updated SAVI data: Version %s (engine %d.%d) ".
  23168. "recognizing %d viruses", $version->string,
  23169. $version->major, $version->minor, $version->count);
  23170. }
  23171. }
  23172. }
  23173. # to be called from sub sophos_savi
  23174. #
  23175. sub sophos_savi_internal {
  23176. my($query,
  23177. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
  23178. $query = join(' ',@$query) if ref $query;
  23179. my $fname = "$tempdir/parts/$query"; # file to be checked
  23180. if (!c('bypass_decode_parts')) {
  23181. my $part = $names_to_parts->{$query}; # get corresponding parts object
  23182. my $mime_option_value = 0;
  23183. if (ref($part) && ($part->type_short eq 'MAIL' ||
  23184. lc($part->type_declared) eq 'message/rfc822')) {
  23185. do_log(2, "%s: $query - enabling option Mime", $av_name);
  23186. $mime_option_value = 1;
  23187. }
  23188. my $error = $savi_obj->set('Mime', $mime_option_value);
  23189. !defined $error or die sprintf("%s: Error %s option Mime: err=%s",
  23190. $av_name, $mime_option_value ? 'setting' : 'clearing', $error);
  23191. }
  23192. my($output,$status); $!=0; my $result = $savi_obj->scan($fname);
  23193. if (!ref($result)) { # error
  23194. my $msg = "error scanning file $fname, " .
  23195. $savi_obj->error_string($result) . " ($result)"; # ignore $! ?
  23196. if ( !grep($result == $_, (514,527,530,538,549)) ) {
  23197. $status = 2; $output = "ERROR $query: $msg";
  23198. } else { # don't panic on non-fatal (encrypted, corrupted, partial)
  23199. $status = 0; $output = "CLEAN $query: $msg";
  23200. }
  23201. do_log(5,"%s: %s", $av_name,$output);
  23202. } elsif ($result->infected) {
  23203. $status = 1; $output = join(", ", $result->viruses) . " FOUND";
  23204. } else {
  23205. $status = 0; $output = "CLEAN $query";
  23206. }
  23207. ($status,$output); # return synthesised status and a result string
  23208. }
  23209. # implements client side of the Sophos SSSP protocol
  23210. #
  23211. sub sophos_sssp_internal {
  23212. my($query,
  23213. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
  23214. my($query_template, $socket_specs) = !$args ? () : @$args;
  23215. # short timeout for connect and sending a request
  23216. prolong_timer('sophos_sssp_connect', undef, undef, 10);
  23217. my($remaining_time, $deadline) = get_deadline('sophos_sssp_internal');
  23218. # section_time('sssp-pre');
  23219. my $sssp_handle =
  23220. Amavis::IO::RW->new($socket_specs, Eol => "\015\012", Timeout => 10);
  23221. defined $sssp_handle or die "Can't connect to savdid";
  23222. # section_time('sssp-conn');
  23223. my $ln; local($1);
  23224. $ln = $sssp_handle->get_response_line; # greeting
  23225. defined $ln && $ln ne '' or die "sssp no greeting";
  23226. do_log(5,"sssp greeting %s", $ln);
  23227. $ln =~ m{^OK\s+SSSP/(\d+.*)\015\012\z}s or die "sssp bad greeting '$ln'";
  23228. # section_time('sssp-greet');
  23229. # # Use the SSSP OPTIONS request only if necessary, it is cheaper to have the
  23230. # # options set in the configuration file. If a client has needs different
  23231. # # from other clients, create another channel tailored for that client.
  23232. # #
  23233. # $sssp_handle->print("SSSP/1.0 OPTIONS\015\012".
  23234. # "savists:zipdecompression 1\015\012".
  23235. # "output: brief\015\012\015\012")
  23236. # or die "Error writing to sssp socket";
  23237. # $sssp_handle->flush or die "Error flushing sssp socket";
  23238. # $ln = $sssp_handle->get_response_line;
  23239. # defined $ln && $ln ne '' or die "sssp no response to OPTIONS";
  23240. # do_log(5,"sssp response to OPTIONS: %s", $ln);
  23241. # $ln =~ /^ACC\s+(\S*)/ or die "sssp OPTIONS request not accepted";
  23242. # while (defined($ln = $sssp_handle->get_response_line)) {
  23243. # last if $ln eq "\015\012";
  23244. # do_log(5,"sssp result of OPTIONS: %s", $ln);
  23245. # }
  23246. # # section_time('sssp-opts');
  23247. my $output = '';
  23248. # normal timeout for reading a response
  23249. prolong_timer('sophos_sssp_scan');
  23250. $sssp_handle->timeout(max(2, $deadline - Time::HiRes::time));
  23251. for my $fname (!ref($query) ? $query : @$query) {
  23252. my $fname_enc = $fname;
  23253. $fname_enc =~ s/([%\000-\040\177\377])/sprintf("%%%02X",ord($1))/egs;
  23254. $sssp_handle->print("SSSP/1.0 SCANDIRR $fname_enc\015\012")
  23255. or die "Error writing to sssp socket";
  23256. $sssp_handle->flush or die "Error flushing sssp socket";
  23257. $ln = $sssp_handle->get_response_line;
  23258. defined $ln && $ln ne '' or die "sssp no response to SCANDIRR";
  23259. do_log(5,"sssp response to SCANDIRR: %s", $ln);
  23260. # section_time('sssp-scan-ack');
  23261. $ln =~ /^ACC\s+(\S*)/ or die "sssp SCANDIRR request not accepted";
  23262. while (defined($ln = $sssp_handle->get_response_line)) {
  23263. last if $ln eq "\015\012";
  23264. do_log(3,"sssp result: %s", $ln);
  23265. $output .= $ln if length($output) < 10000;
  23266. }
  23267. }
  23268. $output = proto_decode($output);
  23269. # section_time('sssp-scan-result');
  23270. $sssp_handle->print("BYE\015\012") or die "Error writing to sssp socket";
  23271. $sssp_handle->flush or die "Error flushing sssp socket";
  23272. $sssp_handle->timeout(max(2, $deadline - Time::HiRes::time));
  23273. while (defined($ln = $sssp_handle->get_response_line)) {
  23274. do_log(5,"sssp response to BYE: %s", $ln);
  23275. last if $ln eq "\015\012" || $ln =~ /^BYE/;
  23276. }
  23277. # section_time('sssp-bye');
  23278. $sssp_handle->close or do_log(-1, "sssp - error closing session: $!");
  23279. # section_time('sssp-close');
  23280. (0,$output); # return synthesised status and a result string
  23281. }
  23282. # implements client side of the AVIRA SAVAPI3 protocol
  23283. #
  23284. sub avira_savapi_internal {
  23285. my($query,
  23286. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
  23287. my($query_template, $socket_specs, $product_id) = !$args ? () : @$args;
  23288. # short timeout for connect and sending a request
  23289. prolong_timer('avira_savapi_connect', undef, undef, 10);
  23290. my($remaining_time, $deadline) = get_deadline('avira_savapi_internal');
  23291. # section_time('savapi-pre');
  23292. my $savapi_handle =
  23293. Amavis::IO::RW->new($socket_specs, Eol => "\012", Timeout => 10);
  23294. defined $savapi_handle or die "Can't connect to savapi daemon";
  23295. # section_time('savapi-conn');
  23296. my $ln; local($1);
  23297. $ln = $savapi_handle->get_response_line; # greeting
  23298. defined $ln && $ln ne '' or die "savapi no greeting";
  23299. do_log(5,"savapi greeting %s", $ln);
  23300. $ln =~ m{^100 SAVAPI:(\d+.*)\012\z}s or die "savapi bad greeting '$ln'";
  23301. # section_time('savapi-greet');
  23302. $remaining_time = int(max(2, $deadline - Time::HiRes::time + 0.5));
  23303. for my $cmd ("SET PRODUCT $product_id",
  23304. "SET SCAN_TIMEOUT $remaining_time",
  23305. "SET CWD $tempdir/parts",
  23306. ) {
  23307. # consider: "SET MAILBOX_SCAN 1", "SET ARCHIVE_SCAN 1", "SET HEUR_LEVEL 2"
  23308. $savapi_handle->print($cmd."\012") or die "Error writing '$cmd' to socket";
  23309. $savapi_handle->flush or die "Error flushing socket";
  23310. $ln = $savapi_handle->get_response_line;
  23311. defined $ln && $ln ne '' or die "savapi: no response to $cmd";
  23312. do_log(5,"savapi response to '%s': %s", $cmd,$ln);
  23313. $ln =~ /^100/ or die "savapi: $cmd request not accepted: $ln";
  23314. }
  23315. # section_time('savapi-settings');
  23316. # set a normal timeout for reading a response
  23317. prolong_timer('avira_savapi_scan');
  23318. $savapi_handle->timeout(max(2, $deadline - Time::HiRes::time));
  23319. my $keep_one_success; my $output = '';
  23320. for my $fname (!ref($query) ? $query : @$query) {
  23321. my $cmd = "SCAN $fname"; # files only, no directories
  23322. $savapi_handle->print($cmd."\012") or die "Error writing '$cmd' to socket";
  23323. $savapi_handle->flush or die "Error flushing socket";
  23324. while (defined($ln = $savapi_handle->get_response_line)) {
  23325. do_log(5,"savapi response to '%s': %s", $cmd,$ln);
  23326. if ($ln =~ /^200/) { # clean
  23327. $keep_one_success = $ln if !defined $keep_one_success;
  23328. } else {
  23329. $output .= $ln if length($output) < 10000; # sanity limit
  23330. }
  23331. last if $ln =~ /^([0125-9]\d\d|300|319).*\012/; # terminal status
  23332. # last if $ln =~ !/^(310|420|421|422|430).*\012/; # nonterminal status
  23333. }
  23334. }
  23335. $output = $keep_one_success if $output eq '' && defined $keep_one_success;
  23336. do_log(5,"savapi result: %s", $output);
  23337. # section_time('savapi-scan-result');
  23338. $savapi_handle->print("QUIT\012")
  23339. or do_log(-1, "savapi - error writing QUIT to socket");
  23340. $savapi_handle->flush
  23341. or do_log(-1, "savapi - error flushing socket after QUIT");
  23342. $savapi_handle->close
  23343. or do_log(-1, "savapi - error closing session: $!");
  23344. # section_time('savapi-close');
  23345. (0,$output); # return synthesised status and a result string
  23346. }
  23347. # implements client side of the ClamAV clamd protocol
  23348. #
  23349. sub clamav_clamd_internal {
  23350. my($query,
  23351. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
  23352. my($query_template, $socket_specs, $product_id) = !$args ? () : @$args;
  23353. # short timeout for connect
  23354. prolong_timer('clamav_connect', undef, undef, 10);
  23355. my($remaining_time, $deadline) = get_deadline('clamav_internal');
  23356. my $clamav_handle =
  23357. Amavis::IO::RW->new($socket_specs, Eol => "\000", Timeout => 10);
  23358. defined $clamav_handle or die "Can't connect to a clamd daemon";
  23359. # set a normal timeout
  23360. prolong_timer('clamav_scan');
  23361. $clamav_handle->timeout(max(2, $deadline - Time::HiRes::time));
  23362. $clamav_handle->print("zIDSESSION\0")
  23363. or die "Error writing 'zIDSESSION' to a clamd socket: $!";
  23364. my(%requests, %requests_filename, %requests_timestamp, $end_sent);
  23365. my($req_id, $requests_pending) = (0,0);
  23366. my $requests_remaining = !ref $query ? 1 : scalar @$query;
  23367. my $keep_one_success; my $output = '';
  23368. while ($requests_remaining > 0 || $requests_pending > 0) {
  23369. my $throttling = $requests_pending >= 8;
  23370. if ($throttling) {
  23371. $clamav_handle->flush or die "Error flushing socket: $!";
  23372. do_log(5,'clamav: throttling: pending %d, remaining %d',
  23373. $requests_pending, $requests_remaining);
  23374. } elsif ($requests_remaining > 0 && !$throttling) {
  23375. my $fname = !ref $query ? $query : $query->[$req_id];
  23376. $req_id++; $requests_remaining--;
  23377. $requests{$req_id} = 'INITIATING';
  23378. $requests_filename{$req_id} = $fname;
  23379. ll(5) && do_log(5,'clamav: sending contents of %s', $fname);
  23380. $clamav_handle->print("zINSTREAM\0")
  23381. or die "Error writing 'zINSTREAM' to a clamd socket: $!";
  23382. $requests{$req_id} = 'OPEN';
  23383. my $fh = IO::File->new;
  23384. $fh->open($fname,'<') or die "Can't open file $fname: $!";
  23385. binmode($fh,':bytes') or die "Can't cancel :utf8 mode: $!";
  23386. my($nbytes,$buff); $buff = pack('N',0);
  23387. while (($nbytes=$fh->read($buff, 32768-4, 4)) > 0) {
  23388. substr($buff,0,4) = pack('N',$nbytes); # 32 bits len -> 4 bytes
  23389. $clamav_handle->print($buff)
  23390. or die "Error writing $nbytes bytes to a clamd socket: $!";
  23391. $requests{$req_id} = 'SENDING';
  23392. }
  23393. my $eod = pack('N',0); # length zero indicates end of data
  23394. if ($requests_remaining <= 0) { $eod .= "zEND\0"; $end_sent = 1 }
  23395. $clamav_handle->print($eod)
  23396. or die "Error writing end-of-data to a clamd socket: $!";
  23397. # $clamav_handle->flush or die "Error flushing socket: $!";
  23398. $requests_timestamp{$req_id} = Time::HiRes::time;
  23399. $requests{$req_id} = 'SENT'; $requests_pending++;
  23400. $fh->close or die "Error closing file $fname: $!";
  23401. # do_log(5,'clamav: finished sending %s', $fname);
  23402. }
  23403. my $ln;
  23404. while ($requests_pending > 0 &&
  23405. ( !$requests_remaining || $throttling ||
  23406. $clamav_handle->response_line_available ) &&
  23407. defined($ln = $clamav_handle->get_response_line)) {
  23408. my $rx_time = Time::HiRes::time;
  23409. # do_log(5,'clamav: got response %s', $ln);
  23410. local($1,$2);
  23411. if ($ln !~ /^(\d+):\s*(.*?)\000\z/s) {
  23412. do_log(-1,'clamav: unparseable response %s', $ln);
  23413. next;
  23414. }
  23415. my($id,$resp) = ($1,$2);
  23416. if (!defined $requests{$id}) {
  23417. do_log(-1,'clamav: bogus id %s in response ignored: %s', $id, $ln);
  23418. } elsif ($requests{$id} eq 'DONE') {
  23419. do_log(-1,'clamav: duplicate result for id %s: %s', $id, $ln);
  23420. } else {
  23421. ll(5) && do_log(5,'clamav: request id %s on %s took %.1f ms',
  23422. $id, $requests_filename{$id},
  23423. 1000 * ($rx_time - $requests_timestamp{$id}));
  23424. if ($requests{$id} ne 'SENT') {
  23425. do_log(2,'clamav: result based on partial data, state %s: %s',
  23426. $requests{$id}, $ln);
  23427. }
  23428. $ln =~ s/\000\z/\n/s;
  23429. $ln =~ s/^\Q$id\E:\s*stream:\s*/$requests_filename{$id}: /s;
  23430. if ($resp =~ /\bOK\z/) { # clean
  23431. $keep_one_success = $ln if !defined $keep_one_success;
  23432. } else {
  23433. $output .= $ln if length($output) < 10000; # sanity limit
  23434. }
  23435. $requests{$id} = 'DONE';
  23436. $requests_pending-- if $requests_pending > 0;
  23437. delete $requests_filename{$id};
  23438. delete $requests_timestamp{$id};
  23439. if ($resp =~ /\bFOUND\z/ &&
  23440. $requests_remaining > 0 && c('first_infected_stops_scan')) {
  23441. do_log(2,'clamav: first infected stops scan');
  23442. $requests_remaining = 0;
  23443. }
  23444. }
  23445. }
  23446. }
  23447. $output = $keep_one_success if $output eq '' && defined $keep_one_success;
  23448. do_log(5,'clamav: result: %s', $output);
  23449. if (!$end_sent) {
  23450. $clamav_handle->print("zEND\0")
  23451. or die "Error writing 'zEND' to a clamd socket: $!";
  23452. }
  23453. $clamav_handle->close or do_log(-1, "clamav - error closing session: $!");
  23454. (0,$output); # return synthesised status and a result string
  23455. }
  23456. sub av_smtp_client($$$$) {
  23457. my($msginfo,$av_name,$av_test_method,$av_test_recip) = @_;
  23458. $av_test_recip = 'dummy@localhost' if !defined $av_test_recip;
  23459. my $test_msg = Amavis::In::Message->new;
  23460. $test_msg->rx_time($msginfo->rx_time); # copy the reception time
  23461. $test_msg->log_id($msginfo->log_id); # use the same log_id
  23462. $test_msg->partition_tag($msginfo->partition_tag); # same partition_tag
  23463. $test_msg->conn_obj($msginfo->conn_obj);
  23464. $test_msg->mail_id($msginfo->mail_id); # use the same mail_id
  23465. $test_msg->body_type($msginfo->body_type); # use the same BODY= type
  23466. $test_msg->header_8bit($msginfo->header_8bit);
  23467. $test_msg->body_8bit($msginfo->body_8bit);
  23468. $test_msg->body_digest($msginfo->body_digest); # copy original digest
  23469. $test_msg->dsn_ret($msginfo->dsn_ret);
  23470. $test_msg->dsn_envid($msginfo->dsn_envid);
  23471. $test_msg->sender($msginfo->sender); # original sender
  23472. $test_msg->sender_smtp($msginfo->sender_smtp);
  23473. $test_msg->auth_submitter($msginfo->sender_smtp);
  23474. $test_msg->auth_user(c('amavis_auth_user'));
  23475. $test_msg->auth_pass(c('amavis_auth_pass'));
  23476. $test_msg->recips([$av_test_recip]); # made-up recipient
  23477. $_->delivery_method($av_test_method) for @{$test_msg->per_recip_data};
  23478. $test_msg->originating(0); # disables DKIM signing
  23479. $test_msg->mail_text($msginfo->mail_text); # the original mail contents
  23480. $test_msg->mail_text_str($msginfo->mail_text_str);
  23481. $test_msg->body_start_pos($msginfo->body_start_pos);
  23482. $test_msg->skip_bytes($msginfo->skip_bytes);
  23483. # NOTE: $initial_submission argument is typically treated as a boolean
  23484. # but here a value of 2 is supplied to allow a forwarding method to
  23485. # distinguish it from ordinary submissions
  23486. mail_dispatch($test_msg, 'AV', 0);
  23487. my($smtp_resp, $exit_code, $dsn_needed) =
  23488. one_response_for_all($test_msg, 0); # check status
  23489. do_log(2, "av_smtp_client %s: %s, %s", $av_name,$av_test_method,$smtp_resp);
  23490. (0, $smtp_resp);
  23491. }
  23492. # same args and returns as run_av() below,
  23493. # but prepended by a $query, which is a string to be sent to the daemon.
  23494. # Handles UNIX, INET and INET6 domain sockets.
  23495. # More than one socket may be specified for redundancy, they will be tried
  23496. # one after the other until one succeeds.
  23497. #
  23498. sub ask_daemon_internal {
  23499. my($query, # expanded query template, often a command and a file or dir name
  23500. $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
  23501. $sts_clean,$sts_infected,$how_to_get_names, # regexps
  23502. ) = @_;
  23503. my($query_template_orig,$socket_specs) = @$args;
  23504. my $output = '';
  23505. $socket_specs = [ $socket_specs ] if !ref($socket_specs);
  23506. my($remaining_time, $deadline) =
  23507. get_deadline('ask_daemon_internal_connect_pre');
  23508. my $max_retries = 2 * @$socket_specs; my $retries = 0;
  23509. # Sophie, Trophie and fpscand can accept multiple requests per session
  23510. # and return a single line response each time
  23511. my $multisession = $av_name =~ /\b(Sophie|Trophie|fpscand)\b/i ? 1 : 0;
  23512. for (;;) { # gracefully handle cases when av process times out or restarts
  23513. # short timeout for connect and sending a request
  23514. prolong_timer('ask_daemon_internal_connect', undef, undef, 10);
  23515. @$socket_specs or die "panic, no sockets specified!?"; # sanity
  23516. # try the first one in the current list
  23517. my $socketname = $socket_specs->[0];
  23518. my $sock = $st_sock{$socketname};
  23519. my $eval_stat;
  23520. eval {
  23521. if (!$st_socket_created{$socketname}) {
  23522. ll(3) && do_log(3, "%s: Connecting to socket %s %s%s",
  23523. $av_name, $daemon_chroot_dir, $socketname,
  23524. !$retries ? '' : ", retry #$retries" );
  23525. $sock = Amavis::IO::RW->new($socketname, Timeout => 10);
  23526. $st_sock{$socketname} = $sock;
  23527. defined $sock or die "Can't connect to socket $socketname\n";
  23528. $st_socket_created{$socketname} = 1;
  23529. }
  23530. $query = join(' ',@$query) if ref $query;
  23531. ll(3) && do_log(3,"%s: Sending %s to socket %s",
  23532. $av_name, $query, $socketname);
  23533. $sock->print($query) or die "Error writing to socket $socketname\n";
  23534. $sock->flush or die "Error flushing socket $socketname\n";
  23535. # normal timeout for reading a response
  23536. prolong_timer('ask_daemon_internal_scan');
  23537. $sock->timeout(max(2, $deadline - Time::HiRes::time));
  23538. if ($multisession) {
  23539. # depends on TCP segment boundaries, unreliable
  23540. my $nread = $sock->read($output,16384);
  23541. defined($nread) or die "Error reading from $socketname: $!\n";
  23542. # and keep the socket open
  23543. } else { # single request/response per connection
  23544. my $buff = '';
  23545. for (;;) {
  23546. my $nread = $sock->read($buff,16384);
  23547. if (!defined($nread)) {
  23548. die "Error reading from $socketname: $!\n";
  23549. } elsif ($nread < 1) {
  23550. last; # sysread returns 0 at eof
  23551. } else { # successful read
  23552. $output .= $buff if length($output) < 100000; # sanity
  23553. }
  23554. }
  23555. $sock->close or die "Error closing socket $socketname\n";
  23556. $st_sock{$socketname} = $sock = undef;
  23557. $st_socket_created{$socketname} = 0;
  23558. }
  23559. $output ne '' or die "Empty result from $socketname\n";
  23560. 1;
  23561. } or do {
  23562. $eval_stat = $@ ne '' ? $@ : "errno=$!";
  23563. };
  23564. prolong_timer('ask_daemon_internal');
  23565. last if !defined $eval_stat; # mission accomplished
  23566. # error handling (the most interesting error codes are EPIPE and ENOTCONN)
  23567. chomp $eval_stat; my $err = "$!"; my $errn = 0+$!;
  23568. if (Time::HiRes::time >= $deadline) {
  23569. die "ask_daemon_internal: Exceeded allowed time";
  23570. }
  23571. ++$retries <= $max_retries
  23572. or die "Too many retries to talk to $socketname ($eval_stat)";
  23573. if ($retries <= 1 && $errn == EPIPE) { # common, don't cause concern
  23574. do_log(2,"%s broken pipe (don't worry), retrying (%d)",
  23575. $av_name,$retries);
  23576. } else {
  23577. do_log( ($retries > 1 ? -1 : 1),
  23578. "%s: %s, retrying (%d)", $av_name,$eval_stat,$retries);
  23579. if ($retries % @$socket_specs == 0) { # every time the list is exhausted
  23580. my $dly = min(20, 1 + 5 * ($retries/@$socket_specs - 1));
  23581. do_log(3,"%s: sleeping for %s s", $av_name,$dly);
  23582. sleep($dly); # slow down a possible runaway
  23583. }
  23584. }
  23585. if ($st_socket_created{$socketname}) {
  23586. # prepare for a retry, implicit close through DESTROY ignoring status
  23587. $st_sock{$socketname} = $sock = undef;
  23588. $st_socket_created{$socketname} = 0;
  23589. }
  23590. # leave good socket as the first entry in the list
  23591. # so that it will be tried first when needed again
  23592. if (@$socket_specs > 1) {
  23593. push(@$socket_specs, shift @$socket_specs); # circular shift left
  23594. }
  23595. }
  23596. (0,$output); # return synthesised status and a result string
  23597. }
  23598. # subroutine is available for calling from @av_scanners list entries;
  23599. # it has the same args and returns as run_av() below.
  23600. # Based on an implied protocol, or on an explicitly specified protocol name
  23601. # in the second element of array @$args, it determines a subroutine needed
  23602. # to implement the required protocol (defaulting to &ask_daemon_internal)
  23603. # and replaces $command in the argument list by this subroutine reference,
  23604. # then calls run_av with adjusted arguments. So, its main purpose is to map
  23605. # a protocol name (a string) into an internal code reference.
  23606. #
  23607. sub ask_daemon {
  23608. my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
  23609. $sts_clean,$sts_infected,$how_to_get_names) = @_;
  23610. my($av_method,$av_protocol); local($1);
  23611. # determine a protocol name from the second element of array @$args
  23612. $av_method = $args->[1] if $args && @$args >= 2;
  23613. $av_method = $av_method->[0] if ref $av_method;
  23614. $av_protocol = lc($1) if defined $av_method &&
  23615. $av_method =~ /^([a-z][a-z0-9.+-]*):/si;
  23616. my $code; my $run_spawned = 0;
  23617. if (!defined $av_protocol) {
  23618. # for compatibility with old style socket specification with
  23619. # no protocol (scheme) field, equivalent to a former call to ask_av()
  23620. # Sophie, Trophie, ClamAV-clamd, OpenAntiVirus, AVG,
  23621. # F-Prot fpscand, F-Prot f-protd, DrWebD, avast, ESET NOD32SS
  23622. $code = \&ask_daemon_internal;
  23623. } elsif ($av_protocol =~ /^(simple|sophie|trophie)\z/) {
  23624. # same as default, but with an explicit protocol prefix
  23625. $code = \&ask_daemon_internal;
  23626. } elsif ($av_protocol eq 'sssp') { # Sophos SSSP
  23627. $code = \&sophos_sssp_internal;
  23628. } elsif ($av_protocol eq 'savapi') { # Avira SAVAPI3
  23629. $code = \&avira_savapi_internal;
  23630. } elsif ($av_protocol eq 'clamd') { # ClamAV clamd protocol
  23631. $code = \&clamav_clamd_internal;
  23632. } elsif ($av_protocol eq 'smtp' || $av_protocol eq 'lmtp') {
  23633. $code = sub { av_smtp_client($Amavis::MSGINFO, $av_name,
  23634. $av_method, $args->[2]) };
  23635. } elsif ($av_protocol eq 'savi-perl') { # using SAVI-Perl perl module
  23636. if (@_ < 3+6) { # supply default arguments for backwards compatibility
  23637. $args = ['*']; $sts_clean = [0]; $sts_infected = [1];
  23638. $how_to_get_names = qr/^(.*) FOUND$/m;
  23639. }
  23640. $code = \&sophos_savi_internal;
  23641. } elsif ($av_protocol eq 'clamav-perl') { # using Mail::ClamAV perl module
  23642. clamav_module_internal_pre($av_name); # must not run as a subprocess
  23643. $code = \&clamav_module_internal; $run_spawned = 1;
  23644. }
  23645. ll(5) && do_log(5, "ask_daemon: proto=%s, spawn=%s, (%s) %s",
  23646. !defined $av_protocol ? 'DFLT' : $av_protocol,
  23647. $run_spawned, $av_name, $av_method);
  23648. ref $code or die "Unsupported AV protocol name: $av_method";
  23649. $command = $code;
  23650. # reassemble arguments, after possibly being modified
  23651. my(@run_av_args) = ($bare_fnames,$names_to_parts,$tempdir,
  23652. $av_name,$command,$args, $sts_clean,$sts_infected,$how_to_get_names);
  23653. my(@results);
  23654. if (!$run_spawned) {
  23655. @results = run_av(@run_av_args); # invoke directly
  23656. } else {
  23657. my($proc_fh,$pid) = run_as_subprocess(\&ask_av, @run_av_args);
  23658. my($results_ref,$child_stat) =
  23659. collect_results_structured($proc_fh,$pid,$av_name,200*1024);
  23660. @results = @$results_ref if $results_ref;
  23661. }
  23662. @results; # ($scan_status,$output,$virusnames)
  23663. }
  23664. # for compatibility with pre-2.6.0 versions of amavisd-new and
  23665. # old @av_scanners entries; use ask_daemon and/or run_av instead
  23666. sub ask_av(@) {
  23667. my($code, @run_av_args) = @_;
  23668. $run_av_args[4] = $code; # replaces $command with a supplied $code
  23669. run_av(@run_av_args);
  23670. }
  23671. # Call a virus scanner and parse its output.
  23672. # Returns a triplet, or dies in case of failure.
  23673. # The first element of the triplet has the following semantics:
  23674. # - true if virus found,
  23675. # - 0 if no viruses found,
  23676. # - undef if it did not complete its job;
  23677. # the second element is a string, the text as provided by the virus scanner;
  23678. # the third element is ref to a list of virus names found (if any).
  23679. # (it is guaranteed the list will be nonempty if virus was found)
  23680. #
  23681. # If there is at least one glob character '*' present in a query template, the
  23682. # subroutine will traverse supplied files (@$bare_fnames) and call a supplied
  23683. # subroutine or program for each file to be scanned, summarizing the final
  23684. # av scan result. If there are no glob characters in a template, the result
  23685. # is a single call to a supplied subroutine or program, which will presumably
  23686. # traverse a directory by itself.
  23687. #
  23688. sub run_av(@) {
  23689. my($bare_fnames, # a ref to a list of filenames to scan (basenames)
  23690. $names_to_parts, # ref to a hash that maps base file names to parts object
  23691. $tempdir, # temporary directory
  23692. # n-tuple from an @av_scanners list entry starts here
  23693. $av_name, $command, $args,
  23694. $sts_clean, # a ref to a list of status values, or a regexp
  23695. $sts_infected, # a ref to a list of status values, or a regexp
  23696. $how_to_get_names, # ref to sub, or a regexp to get list of virus names
  23697. $pre_code, $post_code, # routines to be invoked before and after av
  23698. ) = @_;
  23699. my($scan_status,@virusnames,$error_str); my $output = '';
  23700. return (0,$output,\@virusnames) if !defined($bare_fnames) || !@$bare_fnames;
  23701. my($query_template, $socket_specs); my $av_protocol = '';
  23702. if (!ref $args) {
  23703. $query_template = $args;
  23704. } else {
  23705. ($query_template, $socket_specs) = @$args;
  23706. $socket_specs = $socket_specs->[0] if ref $socket_specs;
  23707. if (defined $socket_specs) {
  23708. local($1);
  23709. $av_protocol = lc($1) if $socket_specs =~ /^([a-z][a-z0-9.+-]*):/si;
  23710. }
  23711. }
  23712. my $one_at_a_time = 0;
  23713. $one_at_a_time = 1 if ref $command &&
  23714. $av_protocol !~ /^(?:sssp|savapi|clamd)\z/;
  23715. my(@query_template) = $one_at_a_time ? $query_template # treat it as one arg
  23716. : split(' ',$query_template); # shell-like
  23717. my $bare_fnames_last = $#{$bare_fnames};
  23718. do_log(5,"run_av (%s): query template(%s,%d): %s",
  23719. $av_name,$one_at_a_time,$bare_fnames_last,$query_template);
  23720. my($remaining_time, $deadline) = prolong_timer('run_av_pre');
  23721. my $cwd = "$tempdir/parts";
  23722. chdir($cwd) or die "Can't chdir to $cwd: $!";
  23723. &$pre_code(@_) if defined $pre_code;
  23724. # a '{}' will be replaced by a directory name, '{}/*' and '*' by file names
  23725. local($1);
  23726. my(@query_expanded) = map($_ eq '*' || $_ eq '{}/*' ? []
  23727. : m{^ \{ \} ( / .* )? \z}xs ? "$tempdir/parts$1"
  23728. : $_, @query_template);
  23729. my $eval_stat;
  23730. eval {
  23731. for (my $k = 0; $k <= $bare_fnames_last; ) { # traverse fnames in chunks
  23732. my(@processed_filenames);
  23733. my $arglist_size = 0; # size of a command with its arguments so far
  23734. for ($command,@query_expanded) { $arglist_size+=length($_)+1 if !ref $_ }
  23735. for (@query_expanded) { @$_ = () if ref $_ } # reset placeholder lists
  23736. while ($k <= $bare_fnames_last) { # traverse fnames individually
  23737. my $f = $bare_fnames->[$k]; my $multi = 0;
  23738. if ($one_at_a_time) { # glob templates may be substrings anywhere
  23739. local($1); @query_expanded = @query_template; # start afresh
  23740. s{ ( {} (?: / \* )? | \* ) }
  23741. { $1 eq '{}' ? "$tempdir/parts"
  23742. : $1 eq '{}/*' ? ($multi=1,"$tempdir/parts/$f")
  23743. : $1 eq '*' ? ($multi=1,$f) : $1
  23744. }gesx for @query_expanded;
  23745. } else {
  23746. # collect as many filename arguments as suitable, but at least one
  23747. my $arg_size = 0;
  23748. for (@query_template) {
  23749. if ($_ eq '{}/*') { $arg_size += length("$tempdir/parts/$f") + 1 }
  23750. elsif ($_ eq '*') { $arg_size += length($f) + 1 }
  23751. }
  23752. # do_log(5,"run_av arglist size: %d + %d", $arglist_size,$arg_size);
  23753. if (@processed_filenames && $arglist_size + $arg_size > 4000) {
  23754. # POSIX requires 4 kB as a minimum buffer size for program args
  23755. last; # enough collected for now, the rest on the next iteration
  23756. }
  23757. # exact matching on command arguments, no substring matches
  23758. for my $j (0..$#query_template) {
  23759. if (ref($query_expanded[$j])) { # placeholders collecting fnames
  23760. my $arg = $query_template[$j];
  23761. my $repl = $arg eq '{}/*' ? "$tempdir/parts/$f"
  23762. : $arg eq '*' ? $f : undef;
  23763. $multi = 1;
  23764. push(@{$query_expanded[$j]}, untaint($repl));
  23765. $arglist_size += length($repl) + 1;
  23766. }
  23767. }
  23768. }
  23769. $k = $multi ? $k+1 : $bare_fnames_last+1;
  23770. push(@processed_filenames, $multi ? $f : "$tempdir/parts");
  23771. last if $one_at_a_time;
  23772. }
  23773. # now that arguments have been expanded, invoke the scanner
  23774. my($child_stat,$t_status,$t_output);
  23775. prolong_timer('run_av_scan'); # restart timer
  23776. if (ref $command) {
  23777. my(@q) = map(ref $_ ? @$_ : $_, @query_expanded);
  23778. ll(3) && do_log(3, "run_av Using (%s): (code) %s",
  23779. $av_name, join(' ',@q));
  23780. # call subroutine directly, passing all our arguments to it
  23781. ($t_status,$t_output) = &$command(!@q ? '' : @q==1 ? $q[0] : \@q, @_);
  23782. prolong_timer('run_av_3'); # restart timer
  23783. $child_stat = 0; # no spawned process, just declare success
  23784. do_log(4,"run_av (%s) result: %s", $av_name,$t_output);
  23785. } else {
  23786. my($proc_fh,$pid); my $results_ref;
  23787. my $eval_stat2;
  23788. eval {
  23789. my(@q) = map(ref $_ ? @$_ : $_, @query_expanded);
  23790. ll(3) && do_log(3,"run_av Using (%s): %s %s",
  23791. $av_name,$command,join(' ',@q));
  23792. ($proc_fh,$pid) = run_command(undef, '&1', $command, @q);
  23793. ($results_ref,$child_stat) =
  23794. collect_results($proc_fh,$pid, $av_name,200*1024);
  23795. 1;
  23796. } or do { $eval_stat2 = $@ ne '' ? $@ : "errno=$!" };
  23797. undef $proc_fh; undef $pid;
  23798. $error_str = exit_status_str($child_stat,0);
  23799. $t_status = WEXITSTATUS($child_stat) if defined $child_stat;
  23800. prolong_timer('run_av_4'); # restart timer
  23801. if (defined $eval_stat2) {
  23802. chomp $eval_stat2; $error_str = $eval_stat2;
  23803. do_log(-1, "run_av (%s): %s", $av_name,$eval_stat2);
  23804. }
  23805. if (defined $results_ref)
  23806. { $t_output = $$results_ref; undef $results_ref }
  23807. chomp($t_output); my $t_output_trimmed = $t_output;
  23808. $t_output_trimmed =~ s/\r\n/\n/gs; local($1);
  23809. $t_output_trimmed =~ s/([ \t\n\r])[ \t\n\r]{4,}/$1.../gs;
  23810. $t_output_trimmed = "..." . substr($t_output_trimmed,-800)
  23811. if length($t_output_trimmed) > 800;
  23812. do_log(3, "run_av: %s %s, %s", $command,$error_str,$t_output_trimmed);
  23813. }
  23814. if (!defined($child_stat) || !WIFEXITED($child_stat)) {
  23815. # leave $scan_status undefined, indicating an error
  23816. # braindamaged Perl: empty string implies the last successfully
  23817. # matched regular expression; we must avoid this
  23818. } elsif (defined $sts_infected && (
  23819. ref($sts_infected) eq 'ARRAY' ? (grep($_==$t_status, @$sts_infected))
  23820. : $sts_infected eq '' ? 1 # avoid m// stupidity
  23821. : $t_output=~/$sts_infected/m)) { # is infected
  23822. # test for infected first, in case both expressions match
  23823. $scan_status = 1; # 'true' indicates virus found
  23824. my(@t_virusnames) = ref($how_to_get_names) eq 'CODE'
  23825. ? &$how_to_get_names($t_output)
  23826. : $how_to_get_names eq '' ? ()
  23827. : $t_output=~/$how_to_get_names/gm;
  23828. @t_virusnames = grep(defined $_, @t_virusnames);
  23829. push(@virusnames, @t_virusnames);
  23830. $output .= $t_output . "\n";
  23831. do_log(2,"run_av (%s): %s INFECTED: %s", $av_name,
  23832. join(' ',@processed_filenames), join(', ',@t_virusnames) );
  23833. } elsif (!defined($sts_clean)) { # clean, but inconclusive
  23834. # by convention: undef $sts_clean means result is inconclusive,
  23835. # file appears clean, but continue scanning with other av scanners,
  23836. # the current scanner does not want to vouch for it; useful for a
  23837. # scanner like jpeg checker which tests for one vulnerability only
  23838. do_log(3,"run_av (%s): CLEAN, but inconclusive", $av_name);
  23839. } elsif (ref($sts_clean) eq 'ARRAY'
  23840. ? (grep($_==$t_status, @$sts_clean))
  23841. : ""=~/x{0}/ && $t_output=~/$sts_clean/m) { # is clean
  23842. # 'false' (but defined) indicates no viruses
  23843. $scan_status = 0 if !$scan_status; # no viruses, no errors
  23844. do_log(3,"run_av (%s): CLEAN", $av_name);
  23845. } else {
  23846. # $error_str = "unexpected $error_str, output=\"$t_output_trimmed\"";
  23847. $error_str = "unexpected $error_str, output=\"$t_output\"";
  23848. do_log(-1,"run_av (%s) FAILED - %s", $av_name,$error_str);
  23849. last; # error, bail out
  23850. }
  23851. die "Exceeded allowed time\n" if time >= $deadline;
  23852. }
  23853. 1;
  23854. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  23855. &$post_code(@_) if defined $post_code;
  23856. @virusnames = ('') if $scan_status && !@virusnames; # ensure nonempty list
  23857. do_log(3,"run_av (%s) result: clean", $av_name)
  23858. if defined($scan_status) && !$scan_status;
  23859. chdir($tempdir) or die "Can't chdir to $tempdir: $!";
  23860. if (defined $eval_stat) {
  23861. prolong_timer('run_av_5'); # restart timer
  23862. die "run_av error: $eval_stat\n";
  23863. }
  23864. if (!defined($scan_status) && defined($error_str)) {
  23865. die "$command $error_str"; # die is more informative than a return value
  23866. }
  23867. ($scan_status, $output, \@virusnames);
  23868. }
  23869. # @av_scanners is a list of n-tuples, where fields semantics is:
  23870. # 1. name: an AV scanner plain name, to be used in log and reports;
  23871. # 2a. program: a scanner program name; this string will be submitted to
  23872. # subroutine find_external_programs(), which will try to find the full
  23873. # program path name during startup according to a search path in variable
  23874. # $path; if program is not found, this scanner is disabled. Besides a
  23875. # simple string (a full program path name or just the basename to be
  23876. # looked for in PATH), this may be an array ref of alternative program
  23877. # names or full paths - the first match in the list will be used;
  23878. # 2b. subroutine: alternatively, this second field may be a subroutine
  23879. # reference, and the whole n-tuple entry is passed to it as args;
  23880. # it should return a triple: ($scan_status,$output,$virusnames_ref),
  23881. # where:
  23882. # - $scan_status is: true if a virus was found, 0 if no viruses,
  23883. # undef if scanner was unable to complete its job (failed);
  23884. # - $output is an optional result string to appear in logging and macro %v;
  23885. # - $virusnames_ref is a ref to a list of detected virus names (may be
  23886. # undef or a ref to an empty list);
  23887. # 3. args: command arguments to be given to the scanner program;
  23888. # a substring {} will be replaced by the directory name to be scanned, i.e.
  23889. # "$tempdir/parts", a "*" will be replaced by base file names of parts;
  23890. # 4. clean: an array ref of av scanner exit status values, or a regexp
  23891. # (to be matched against scanner output), indicating NO VIRUSES found;
  23892. # a special case is a value undef, which does not claim file to be clean
  23893. # (i.e. it never matches, similar to []), but suppresses a failure warning;
  23894. # to be used when the result is inconclusive (useful for specialized and
  23895. # quick partial scanners such as jpeg checker);
  23896. # 5. infected: an array ref of av scanner exit status values, or a regexp
  23897. # (to be matched against scanner output), indicating VIRUSES WERE FOUND;
  23898. # a value undef may be used and it never matches (for consistency with 4.);
  23899. # Note: the virus match prevails over a 'not found' match, so it is safe
  23900. # even if the no. 4. matches for viruses too;
  23901. # 6. virus name: a regexp (to be matched against scanner output), returning
  23902. # a list of virus names found, or a sub ref, returning such a list when
  23903. # given scanner output as argument;
  23904. # 7. and 8.: (optional) subroutines to be executed before and after scanner
  23905. # (e.g. to set environment or current directory);
  23906. # see examples for these at KasperskyLab AVP and NAI uvscan.
  23907. sub virus_scan($$) {
  23908. my($msginfo,$firsttime) = @_;
  23909. my $tempdir = $msginfo->mail_tempdir;
  23910. my($scan_status,$output,@virusname,@detecting_scanners);
  23911. my $anyone_done = 0; my $anyone_tried = 0;
  23912. my($bare_fnames_ref,$names_to_parts);
  23913. my $j; my $tier = 'primary';
  23914. for my $av (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
  23915. next if !defined $av;
  23916. if ($av eq "\000") { # 'magic' separator between lists
  23917. last if $anyone_done;
  23918. do_log(-1,"WARN: all %s virus scanners failed, considering backups",
  23919. $tier);
  23920. $tier = 'secondary'; next;
  23921. }
  23922. next if !ref $av || !defined $av->[1];
  23923. if (!defined $bare_fnames_ref) { # first time: collect file names to scan
  23924. my $parts_root = $msginfo->parts_root;
  23925. ($bare_fnames_ref,$names_to_parts) =
  23926. files_to_scan("$tempdir/parts",$parts_root);
  23927. if (!@$bare_fnames_ref) {
  23928. do_log(2, "Not calling virus scanners, no files to scan in %s/parts",
  23929. $tempdir);
  23930. } else {
  23931. do_log(5, "Calling virus scanners, %d files to scan in %s/parts",
  23932. scalar(@$bare_fnames_ref), $tempdir);
  23933. }
  23934. }
  23935. my($scanner_name,$command) = @$av;
  23936. $anyone_tried = 1; my($this_status,$this_output,$this_vn);
  23937. if (!@$bare_fnames_ref) { # no files to scan?
  23938. ($this_status,$this_output,$this_vn) = (0, '', []); # declare clean
  23939. } else { # call virus scanner
  23940. do_log(5, "invoking av-scanner %s", $scanner_name);
  23941. eval {
  23942. ($this_status,$this_output,$this_vn) = ref $command eq 'CODE'
  23943. ? &$command($bare_fnames_ref,$names_to_parts,$tempdir, @$av)
  23944. : run_av($bare_fnames_ref,$names_to_parts,$tempdir, @$av);
  23945. 1;
  23946. } or do {
  23947. my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
  23948. $err = sprintf("%s av-scanner FAILED: %s", $scanner_name, $err);
  23949. do_log(-1, "%s", $err);
  23950. $this_status = undef;
  23951. };
  23952. }
  23953. $anyone_done = 1 if defined $this_status;
  23954. $j++; section_time("AV-scan-$j");
  23955. if ($this_status && $this_vn && @$this_vn) {
  23956. @$this_vn = unique_list($this_vn);
  23957. # virus is reported by this scanner; is it for real, or is it just spam?
  23958. my(@spam_hits); my $vnts = ca('virus_name_to_spam_score_maps');
  23959. @spam_hits = # map each reported virus name to spam score or to undef
  23960. map(scalar(lookup2(0,$_,$vnts)), @$this_vn) if ref $vnts;
  23961. if (@spam_hits && !grep(!defined($_), @spam_hits)) { # all defined
  23962. # AV scanner did trigger, but all provided names are actually spam!
  23963. my(%seen);
  23964. for my $r (@{$msginfo->per_recip_data}) {
  23965. my $spam_tests = $r->spam_tests;
  23966. if (defined $spam_tests) {
  23967. local($1,$2);
  23968. for (split(/,/, join(',',map($$_,@$spam_tests)))) {
  23969. $seen{$1} = $2 if /^AV\.([^=]*)=([0-9.+-]+)\z/;
  23970. }
  23971. }
  23972. }
  23973. my(@vnms,@hits);
  23974. # remove already detected virus names and duplicates from the list
  23975. for my $j (0..$#$this_vn) {
  23976. my $vname = $this_vn->[$j];
  23977. if (!exists($seen{$vname})) {
  23978. push(@vnms,$vname); push(@hits,$spam_hits[$j]);
  23979. $seen{$vname} = $spam_hits[$j]; # keep only one copy
  23980. }
  23981. }
  23982. @$this_vn = @vnms; @spam_hits = @hits;
  23983. if (!@spam_hits) {
  23984. do_log(2,"Turning AV infection into a spam report, ".
  23985. "name already accounted for");
  23986. } else {
  23987. my $spam_level = max(@spam_hits);
  23988. my $spam_tests = join(',',
  23989. map(sprintf("AV:%s=%s", $this_vn->[$_], $spam_hits[$_]),
  23990. (0..$#$this_vn) ));
  23991. for my $r (@{$msginfo->per_recip_data}) {
  23992. $r->spam_level( ($r->spam_level || 0) + $spam_level );
  23993. if (!defined($r->spam_tests)) {
  23994. $r->spam_tests([ \$spam_tests ]);
  23995. } else {
  23996. push(@{$r->spam_tests}, \$spam_tests);
  23997. }
  23998. }
  23999. my $spam_report = $spam_tests;
  24000. my $spam_summary =
  24001. sprintf("AV scanner %s reported spam (not infection):\n%s\n",
  24002. $scanner_name, join(',',@$this_vn));
  24003. do_log(2,"Turning AV infection into a spam report: score=%s, %s",
  24004. $spam_level, $spam_tests);
  24005. if (defined($msginfo->spam_report)||defined($msginfo->spam_summary)){
  24006. $spam_report = $msginfo->spam_report . ', ' . $spam_report
  24007. if $msginfo->spam_report ne '';
  24008. $spam_summary = $msginfo->spam_summary . "\n\n" . $spam_summary
  24009. if $msginfo->spam_summary ne '';
  24010. }
  24011. $msginfo->spam_report($spam_report);
  24012. $msginfo->spam_summary($spam_summary);
  24013. }
  24014. $this_status = 0; @$this_vn = (); # TURN OFF ALERT for this AV scanner!
  24015. }
  24016. }
  24017. if ($this_status) { # a virus detected by this scanner, really! (not spam)
  24018. push(@detecting_scanners, $scanner_name);
  24019. if (!@virusname) { # store results of the first scanner detecting
  24020. # @virusname = map(sprintf('[%s] %s',$scanner_name,$_), @$this_vn);
  24021. @virusname = @$this_vn;
  24022. $scan_status = $this_status; $output = $this_output;
  24023. }
  24024. last if c('first_infected_stops_scan'); # stop now if we found a virus?
  24025. } elsif (!defined($scan_status)) { # tentatively keep regardless of status
  24026. $scan_status = $this_status; $output = $this_output;
  24027. }
  24028. }
  24029. if (ll(2) && @virusname && @detecting_scanners) {
  24030. my(@ds) = @detecting_scanners; s/,/;/ for @ds; # facilitates parsing
  24031. do_log(2, "virus_scan: (%s), detected by %d scanners: %s",
  24032. join(', ',@virusname), scalar(@ds), join(', ',@ds));
  24033. }
  24034. $output =~ s{\Q$tempdir\E/parts/?}{}gs if defined $output; # hide path info
  24035. if (!$anyone_tried) { die "NO VIRUS SCANNERS AVAILABLE\n" }
  24036. elsif (!$anyone_done) { die "ALL VIRUS SCANNERS FAILED\n" }
  24037. ($scan_status, $output, \@virusname, \@detecting_scanners); # return a quad
  24038. }
  24039. # return a ref to a list of files to be scanned in a given directory
  24040. #
  24041. sub files_to_scan($$) {
  24042. my($dir,$parts_root) = @_;
  24043. my $names_to_parts = {}; # a hash that maps base file names
  24044. # to Amavis::Unpackers::Part object
  24045. # traverse decomposed parts tree breadth-first, match it to actual files
  24046. for (my $part, my(@unvisited)=($parts_root);
  24047. @unvisited and $part=shift(@unvisited);
  24048. push(@unvisited,@{$part->children}))
  24049. { $names_to_parts->{$part->base_name} = $part if $part ne $parts_root }
  24050. my $bare_fnames_ref = []; my(%bare_fnames);
  24051. # traverse parts directory and check for actual files
  24052. local(*DIR); opendir(DIR,$dir) or die "Can't open directory $dir: $!";
  24053. # modifying a directory while traversing it can cause surprises, avoid;
  24054. # avoid slurping the whole directory contents into memory
  24055. my($f, @rmfiles, @rmdirs);
  24056. while (defined($f = readdir(DIR))) {
  24057. next if $f eq '.' || $f eq '..';
  24058. my $fname = $dir . '/' . $f;
  24059. my(@stat_list) = lstat($fname); my $errn = @stat_list ? 0 : 0+$!;
  24060. next if $errn == ENOENT;
  24061. if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
  24062. add_entropy(@stat_list);
  24063. if (!-r _) { # attempting to gain read access to the file
  24064. do_log(3,"files_to_scan: attempting to gain read access to %s", $fname);
  24065. chmod(0750, untaint($fname))
  24066. or die "files_to_scan: Can't change protection on $fname: $!";
  24067. $errn = lstat($fname) ? 0 : 0+$!;
  24068. if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
  24069. if (!-r _) { die "files_to_scan: file $fname not readable" }
  24070. }
  24071. if (!-f _ || !exists $names_to_parts->{$f}) {
  24072. # not a regular file or unexpected
  24073. my $what = -l _ ? 'symlink' : -d _ ? 'directory' : -f _ ? 'file'
  24074. : 'non-regular file';
  24075. my $msg = "removing unexpected $what $fname";
  24076. $msg .= ", it has no corresponding parts object"
  24077. if !exists $names_to_parts->{$f};
  24078. do_log(-1, "WARN: files_to_scan: %s", $msg);
  24079. if (-d _) { push(@rmdirs, $f) } else { push(@rmfiles, $f) }
  24080. } elsif (-z _) {
  24081. # empty file
  24082. } else {
  24083. if ($f !~ /^[A-Za-z0-9_.-]+\z/s) {
  24084. do_log(-1,"WARN: files_to_scan: unexpected/suspicious file name: %s",
  24085. $f);
  24086. }
  24087. push(@$bare_fnames_ref,$f); $bare_fnames{$f} = 1;
  24088. }
  24089. }
  24090. closedir(DIR) or die "Error closing directory $dir: $!";
  24091. for my $f (@rmfiles) {
  24092. my $fname = $dir . '/' . untaint($f);
  24093. do_log(5,"files_to_scan: deleting file %s", $fname);
  24094. unlink($fname) or die "Can't delete $fname: $!";
  24095. }
  24096. undef @rmfiles;
  24097. for my $d (@rmdirs) {
  24098. my $dname = $dir . '/' . untaint($d);
  24099. do_log(5,"files_to_scan: deleting directory %s", $dname);
  24100. rmdir_recursively($dname);
  24101. }
  24102. undef @rmdirs;
  24103. # remove entries from %$names_to_parts that have no corresponding files
  24104. my($fname,$part);
  24105. while ( ($fname,$part) = each %$names_to_parts ) {
  24106. next if exists $bare_fnames{$fname};
  24107. if (ll(4) && $part->exists) {
  24108. my $type_short = $part->type_short;
  24109. do_log(4,"files_to_scan: info: part %s (%s) no longer present",
  24110. $fname, (!ref $type_short ? $type_short : join(', ',@$type_short)) );
  24111. }
  24112. delete $names_to_parts->{$fname}; # delete is allowed for the current elem.
  24113. }
  24114. ($bare_fnames_ref, $names_to_parts);
  24115. }
  24116. 1;
  24117. __DATA__
  24118. #
  24119. package Amavis::SpamControl;
  24120. use strict;
  24121. use re 'taint';
  24122. use warnings;
  24123. use warnings FATAL => qw(utf8 void);
  24124. no warnings 'uninitialized';
  24125. use Fcntl qw(:flock);
  24126. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  24127. BEGIN {
  24128. require Exporter;
  24129. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  24130. $VERSION = '2.316';
  24131. @ISA = qw(Exporter);
  24132. import Amavis::Conf qw(:platform c cr ca);
  24133. import Amavis::Util qw(ll do_log min max minmax untaint untaint_inplace
  24134. unique_list);
  24135. import Amavis::Lookup qw(lookup lookup2);
  24136. import Amavis::rfc2821_2822_Tools qw(make_query_keys qquote_rfc2821_local);
  24137. }
  24138. sub new {
  24139. my($class) = @_;
  24140. my $self = bless { scanners_list => [] }, $class;
  24141. for my $as (@{ca('spam_scanners')}) {
  24142. if (ref $as && defined $as->[1] && $as->[1] ne '') {
  24143. my($scanner_name,$module,@args) = @$as; my $scanner_obj;
  24144. do_log(5, "SpamControl: attempting to load scanner %s, module %s",
  24145. $scanner_name,$module);
  24146. { no strict 'subs';
  24147. $scanner_obj = $module->new($scanner_name,$module,@args);
  24148. }
  24149. if ($scanner_obj) {
  24150. push(@{$self->{scanners_list}}, [$scanner_obj, @$as]);
  24151. do_log(2, "SpamControl: scanner %s, module %s",
  24152. $scanner_name,$module);
  24153. } else {
  24154. do_log(5, "SpamControl: no scanner %s, module %s",
  24155. $scanner_name,$module);
  24156. }
  24157. }
  24158. }
  24159. $self;
  24160. }
  24161. # called at startup, before chroot and before main fork
  24162. #
  24163. sub init_pre_chroot {
  24164. my($self) = @_;
  24165. for my $as (@{$self->{scanners_list}}) {
  24166. my($scanner_obj,$scanner_name) = @$as;
  24167. if ($scanner_obj && $scanner_obj->UNIVERSAL::can('init_pre_chroot')) {
  24168. $scanner_obj->init_pre_chroot;
  24169. do_log(1, "SpamControl: init_pre_chroot on %s done", $scanner_name);
  24170. }
  24171. }
  24172. }
  24173. # called at startup, after chroot and changing UID, but before main fork
  24174. #
  24175. sub init_pre_fork {
  24176. my($self) = @_;
  24177. for my $as (@{$self->{scanners_list}}) {
  24178. my($scanner_obj,$scanner_name) = @$as;
  24179. if ($scanner_obj && $scanner_obj->UNIVERSAL::can('init_pre_fork')) {
  24180. $scanner_obj->init_pre_fork;
  24181. do_log(1, "SpamControl: init_pre_fork on %s done", $scanner_name);
  24182. }
  24183. }
  24184. }
  24185. # called during child process initialization
  24186. #
  24187. sub init_child {
  24188. my($self) = @_;
  24189. my $failure_msg;
  24190. for my $as (@{$self->{scanners_list}}) {
  24191. my($scanner_obj,$scanner_name) = @$as;
  24192. if ($scanner_obj && $scanner_obj->UNIVERSAL::can('init_child')) {
  24193. eval {
  24194. $scanner_obj->init_child;
  24195. do_log(5, "SpamControl: init_child on %s done", $scanner_name);
  24196. 1;
  24197. } or do {
  24198. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  24199. do_log(-1, "init_child on spam scanner %s failed: %s",
  24200. $scanner_name, $eval_stat);
  24201. $failure_msg = "init_child $scanner_name failed: $eval_stat"
  24202. if !defined $failure_msg;
  24203. };
  24204. }
  24205. }
  24206. if (defined $failure_msg) { die $failure_msg }
  24207. }
  24208. sub lock {
  24209. my($self,$scanner_obj,$lock_type_name) = @_;
  24210. my $lock_file = $scanner_obj->{options}->{'lock_file'};
  24211. if (defined $lock_file && $lock_file ne '') {
  24212. my $lock_type = $scanner_obj->{options}->{$lock_type_name};
  24213. $lock_type = $scanner_obj->{options}->{'lock_type'} if !defined $lock_type;
  24214. $lock_type = 'exclusive' if !defined $lock_type;
  24215. if ($lock_type ne '' && lc($lock_type) ne 'none') {
  24216. my $lock_fh = IO::File->new;
  24217. $lock_fh->open($lock_file, O_CREAT|O_RDWR, 0640)
  24218. or die "Can't open a lock file $lock_file: $!";
  24219. $scanner_obj->{lock_fh} = $lock_fh;
  24220. my $lock_type_displ;
  24221. if (defined $lock_type && lc($lock_type) eq 'shared') {
  24222. $lock_type = LOCK_SH; $lock_type_displ = 'a shared';
  24223. } else {
  24224. $lock_type = LOCK_EX; $lock_type_displ = 'an exclusive';
  24225. }
  24226. do_log(5,"acquring %s lock on %s for %s",
  24227. $lock_type_displ, $lock_file, $scanner_obj->{scanner_name});
  24228. flock($lock_fh, $lock_type)
  24229. or die "Can't acquire $lock_type_displ lock on $lock_file: $!";
  24230. }
  24231. }
  24232. }
  24233. sub unlock {
  24234. my($self,$scanner_obj) = @_;
  24235. my $lock_fh = $scanner_obj->{lock_fh};
  24236. if ($lock_fh) {
  24237. my $scanner_name = $scanner_obj->{scanner_name};
  24238. do_log(5, "releasing a lock for %s", $scanner_name);
  24239. flock($lock_fh, LOCK_UN)
  24240. or die "Can't release a lock for $scanner_name: $!";
  24241. $lock_fh->close or die "Can't close a lock file for $scanner_name: $!";
  24242. undef $scanner_obj->{lock_fh};
  24243. }
  24244. }
  24245. # actual spam checking for every message
  24246. #
  24247. sub spam_scan {
  24248. my($self,$msginfo) = @_;
  24249. my $failure_msg;
  24250. for my $as (@{$self->{scanners_list}}) {
  24251. my($scanner_obj,$scanner_name) = @$as;
  24252. next if !$scanner_obj && !$scanner_obj->UNIVERSAL::can('check');
  24253. do_log(5, "SpamControl: calling spam scanner %s", $scanner_name);
  24254. $self->lock($scanner_obj, 'classifier_lock_type');
  24255. eval {
  24256. $scanner_obj->check($msginfo); 1;
  24257. } or do {
  24258. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  24259. do_log(-1, "checking with spam scanner %s failed: %s",
  24260. $scanner_name, $eval_stat);
  24261. $failure_msg =
  24262. "$scanner_name failed: $eval_stat" if !defined $failure_msg;
  24263. };
  24264. $self->unlock($scanner_obj);
  24265. }
  24266. if (defined $failure_msg) { die $failure_msg }
  24267. 1;
  24268. }
  24269. sub auto_learn {
  24270. my($self,$msginfo) = @_;
  24271. my $failure_msg;
  24272. for my $as (@{$self->{scanners_list}}) {
  24273. my($scanner_obj,$scanner_name) = @$as;
  24274. next if !$scanner_obj || !$scanner_obj->UNIVERSAL::can('auto_learn');
  24275. # learn-on-error logic: what was the final outcome
  24276. my($min_spam_level, $max_spam_level) =
  24277. minmax(map($_->spam_level, @{$msginfo->per_recip_data}));
  24278. next if !defined $min_spam_level || !defined $max_spam_level;
  24279. # learn-on-error logic: what this scanner thinks
  24280. my $my_verdict = $msginfo->supplementary_info('VERDICT-'.$scanner_name);
  24281. $my_verdict = !defined $my_verdict ? '' : lc $my_verdict;
  24282. my $my_score = $msginfo->supplementary_info('SCORE-'.$scanner_name);
  24283. $my_score = 0 if !defined $my_score;
  24284. # learn-on-error logic: opinions differ?
  24285. my $learn_as; # leaving out a contribution by this spam scanner
  24286. if ($my_verdict ne 'ham' && $max_spam_level-$my_score < 0.5) {
  24287. $learn_as = 'ham';
  24288. } elsif ($my_verdict ne 'spam' && $min_spam_level-$my_score >= 5) {
  24289. $learn_as = 'spam';
  24290. }
  24291. next if !defined $learn_as;
  24292. ll(2) && do_log(2,
  24293. "SpamControl: scanner %s, auto-learn as %s / %.3f (was: %s / %s)",
  24294. $scanner_name, $learn_as,
  24295. $my_verdict ne 'ham' ? $max_spam_level : $min_spam_level,
  24296. $my_verdict, !$my_score ? '0' : sprintf("%.3f",$my_score));
  24297. $self->lock($scanner_obj, 'learner_lock_type');
  24298. eval {
  24299. $scanner_obj->auto_learn($msginfo,$learn_as); 1;
  24300. } or do {
  24301. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  24302. do_log(-1, "auto-learning with spam scanner %s failed: %s",
  24303. $scanner_name, $eval_stat);
  24304. $failure_msg =
  24305. "$scanner_name failed: $eval_stat" if !defined $failure_msg;
  24306. };
  24307. $self->unlock($scanner_obj);
  24308. }
  24309. if (defined $failure_msg) { die $failure_msg }
  24310. 1;
  24311. }
  24312. # called during child process shutdown
  24313. #
  24314. sub rundown_child() {
  24315. my($self) = @_;
  24316. for my $as (@{$self->{scanners_list}}) {
  24317. my($scanner_obj,$scanner_name) = @$as;
  24318. if ($scanner_obj && $scanner_obj->UNIVERSAL::can('rundown_child')) {
  24319. eval {
  24320. $scanner_obj->rundown_child;
  24321. do_log(5, "SpamControl: rundown_child on %s done", $scanner_name);
  24322. 1;
  24323. } or do {
  24324. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  24325. do_log(-1, "rundown_child on spam scanner %s failed: %s",
  24326. $scanner_name, $eval_stat);
  24327. };
  24328. }
  24329. }
  24330. }
  24331. # check envelope sender and author for white or blacklisting by each recipient;
  24332. # Saves the result in recip_blacklisted_sender and recip_whitelisted_sender
  24333. # properties of each recipient object, and updates spam score for each
  24334. # recipient according to soft-w/b-listing.
  24335. #
  24336. sub white_black_list($$$$) {
  24337. my($msginfo,$sql_wblist,$user_id_sql,$ldap_lookups) = @_;
  24338. my $fm = $msginfo->rfc2822_from;
  24339. my(@rfc2822_from) = !defined($fm) ? () : ref $fm ? @$fm : $fm;
  24340. my(@senders) = ($msginfo->sender, @rfc2822_from);
  24341. @senders = unique_list(\@senders); # remove possible duplicates
  24342. ll(4) && do_log(4,"wbl: checking sender %s",
  24343. scalar(qquote_rfc2821_local(@senders)));
  24344. my($any_w,$any_b,$all,$wr,$br);
  24345. $any_w = 0; $any_b = 0; $all = 1;
  24346. for my $r (@{$msginfo->per_recip_data}) { # for each recipient
  24347. next if $r->recip_done; # already dealt with
  24348. my($wb,$boost); my $found = 0; my $recip = $r->recip_addr;
  24349. my($user_id_ref,$mk_ref);
  24350. $user_id_ref = $r->user_id;
  24351. $user_id_ref = [] if !defined $user_id_ref;
  24352. do_log(5,"wbl: (SQL) recip <%s>, %s matches",
  24353. $recip, scalar(@$user_id_ref)) if $sql_wblist && ll(5);
  24354. for my $sender (@senders) {
  24355. for my $ind (0..$#{$user_id_ref}) { # for ALL SQL sets matching the recip
  24356. my $user_id = $user_id_ref->[$ind]; my $mkey;
  24357. ($wb,$mkey) = lookup(0,$sender,
  24358. Amavis::Lookup::SQLfield->new($sql_wblist,'wb','S',$user_id) );
  24359. do_log(4,'wbl: (SQL) recip <%s>, rid=%s, got: "%s"',
  24360. $recip,$user_id,$wb);
  24361. if (!defined($wb)) {
  24362. # NULL field or no match: remains undefined
  24363. } elsif ($wb =~ /^ *([+-]?\d+(?:\.\d*)?) *\z/) { # numeric
  24364. my $val = 0+$1; # penalty points to be added to the score
  24365. $boost += $val;
  24366. ll(2) && do_log(2,
  24367. 'wbl: (SQL) soft-%slisted (%s) sender <%s> => <%s> (rid=%s)',
  24368. ($val<0?'white':'black'), $val, $sender, $recip, $user_id);
  24369. $wb = undef; # not hard- white or blacklisting, does not exit loop
  24370. } elsif ($wb =~ /^[ \000]*\z/) { # neutral, stops the search
  24371. $found=1; $wb = 0;
  24372. do_log(5, 'wbl: (SQL) recip <%s> is neutral to sender <%s>',
  24373. $recip,$sender);
  24374. } elsif ($wb =~ /^([BbNnFf])[ ]*\z/) { # blacklisted (B,N(o), F(alse))
  24375. $found=1; $wb = -1; $any_b++; $br = $recip;
  24376. $r->recip_blacklisted_sender(1);
  24377. do_log(5, 'wbl: (SQL) recip <%s> blacklisted sender <%s>',
  24378. $recip,$sender);
  24379. } else { # whitelisted (W, Y(es), T(true), or anything else)
  24380. if ($wb =~ /^([WwYyTt])[ ]*\z/) {
  24381. do_log(5, 'wbl: (SQL) recip <%s> whitelisted sender <%s>',
  24382. $recip,$sender);
  24383. } else {
  24384. do_log(-1,'wbl: (SQL) recip <%s> whitelisted sender <%s>, '.
  24385. 'unexpected wb field value: "%s"', $recip,$sender,$wb);
  24386. }
  24387. $found=1; $wb = +1; $any_w++; $wr = $recip;
  24388. $r->recip_whitelisted_sender(1);
  24389. }
  24390. last if $found;
  24391. }
  24392. if (!$found && $ldap_lookups && c('enable_ldap')) { # LDAP queries
  24393. my $wblist;
  24394. my($keys_ref,$rhs_ref) = make_query_keys($sender,0,0);
  24395. my(@keys) = @$keys_ref;
  24396. unshift(@keys, '<>') if $sender eq ''; # a hack for a null return path
  24397. untaint_inplace($_) for @keys; # untaint keys
  24398. $_ = Net::LDAP::Util::escape_filter_value($_) for @keys;
  24399. do_log(5,'wbl: (LDAP) query keys: %s', join(', ',map("\"$_\"",@keys)));
  24400. $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new(
  24401. $ldap_lookups, 'amavisBlacklistSender', 'L-'));
  24402. for my $key (@keys) {
  24403. if (grep(lc($_) eq lc($key), @$wblist)) {
  24404. $found=1; $wb = -1; $br = $recip; $any_b++;
  24405. $r->recip_blacklisted_sender(1);
  24406. do_log(5,'wbl: (LDAP) recip <%s> blacklisted sender <%s>',
  24407. $recip,$sender);
  24408. }
  24409. }
  24410. $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new(
  24411. $ldap_lookups, 'amavisWhitelistSender', 'L-'));
  24412. for my $key (@keys) {
  24413. if (grep(lc($_) eq lc($key), @$wblist)) {
  24414. $found=1; $wb = +1; $wr = $recip; $any_w++;
  24415. $r->recip_whitelisted_sender(1);
  24416. do_log(5,'wbl: (LDAP) recip <%s> whitelisted sender <%s>',
  24417. $recip,$sender);
  24418. }
  24419. }
  24420. }
  24421. if (!$found) { # fall back to static lookups if no match
  24422. # sender can be both white- and blacklisted at the same time
  24423. my($val, $r_ref, $mk_ref, @t);
  24424. # NOTE on the specifics of $per_recip_blacklist_sender_lookup_tables :
  24425. # the $r_ref below is supposed to be a ref to a single lookup table
  24426. # for compatibility with pre-2.0 versions of amavisd-new;
  24427. # Note that this is different from @score_sender_maps, which is
  24428. # supposed to contain a ref to a _list_ of lookup tables as a result
  24429. # of the first-level lookup (on the recipient address as a key).
  24430. #
  24431. ($r_ref,$mk_ref) = lookup(0,$recip,
  24432. Amavis::Lookup::Label->new("blacklist_recip<$recip>"),
  24433. cr('per_recip_blacklist_sender_lookup_tables'));
  24434. @t = ((defined $r_ref ? $r_ref : ()), @{ca('blacklist_sender_maps')});
  24435. $val = lookup2(0,$sender,\@t,Label=>"blacklist_sender<$sender>") if @t;
  24436. if ($val) {
  24437. $found=1; $wb = -1; $br = $recip; $any_b++;
  24438. $r->recip_blacklisted_sender(1);
  24439. do_log(5,'wbl: recip <%s> blacklisted sender <%s>', $recip,$sender);
  24440. }
  24441. # similar for whitelists:
  24442. ($r_ref,$mk_ref) = lookup(0,$recip,
  24443. Amavis::Lookup::Label->new("whitelist_recip<$recip>"),
  24444. cr('per_recip_whitelist_sender_lookup_tables'));
  24445. @t = ((defined $r_ref ? $r_ref : ()), @{ca('whitelist_sender_maps')});
  24446. $val = lookup2(0,$sender,\@t,Label=>"whitelist_sender<$sender>") if @t;
  24447. if ($val) {
  24448. $found=1; $wb = +1; $wr = $recip; $any_w++;
  24449. $r->recip_whitelisted_sender(1);
  24450. do_log(5,'wbl: recip <%s> whitelisted sender <%s>', $recip,$sender);
  24451. }
  24452. }
  24453. if (!defined($boost)) { # lookup @score_sender_maps if no match with SQL
  24454. # note the first argument of lookup() is true, requesting ALL matches
  24455. my($r_ref,$mk_ref) = lookup2(1,$recip, ca('score_sender_maps'),
  24456. Label=>"score_recip<$recip>");
  24457. for my $j (0..$#{$r_ref}) { # for ALL tables matching the recipient
  24458. my($val,$key) = lookup2(0,$sender,$r_ref->[$j],
  24459. Label=>"score_sender<$sender>");
  24460. if (defined $val && $val != 0) {
  24461. $boost += $val;
  24462. ll(2) && do_log(2,'wbl: soft-%slisted (%s) sender <%s> => <%s>, '.
  24463. 'recip_key="%s"', ($val<0?'white':'black'),
  24464. $val, $sender, $recip, $mk_ref->[$j]);
  24465. }
  24466. }
  24467. }
  24468. } # endfor on @senders
  24469. if ($boost) { # defined and nonzero
  24470. $r->spam_level( ($r->spam_level || 0) + $boost);
  24471. my $spam_tests = 'AM.WBL=' . (0+sprintf("%.3f",$boost));
  24472. if (!defined($r->spam_tests)) {
  24473. $r->spam_tests([ \$spam_tests ]);
  24474. } else {
  24475. unshift(@{$r->spam_tests}, \$spam_tests);
  24476. }
  24477. }
  24478. $all = 0 if !$wb;
  24479. } # endfor on recips
  24480. if (!ll(2)) {
  24481. # don't bother preparing a log report which will not be printed
  24482. } else {
  24483. my $msg = '';
  24484. if ($all && $any_w && !$any_b) { $msg = "whitelisted" }
  24485. elsif ($all && $any_b && !$any_w) { $msg = "blacklisted" }
  24486. elsif ($all) { $msg = "black or whitelisted by all recips" }
  24487. elsif ($any_b || $any_w) {
  24488. $msg .= "whitelisted by ".($any_w>1?"$any_w recips, ":"$wr, ") if $any_w;
  24489. $msg .= "blacklisted by ".($any_b>1?"$any_b recips, ":"$br, ") if $any_b;
  24490. $msg .= "but not by all,";
  24491. }
  24492. do_log(2,"wbl: %s sender %s",
  24493. $msg, scalar(qquote_rfc2821_local(@senders))) if $msg ne '';
  24494. }
  24495. ($any_w+$any_b, $all);
  24496. }
  24497. 1;
  24498. __DATA__
  24499. #
  24500. package Amavis::SpamControl::ExtProg;
  24501. use strict;
  24502. use re 'taint';
  24503. use warnings;
  24504. use warnings FATAL => qw(utf8 void);
  24505. no warnings 'uninitialized';
  24506. BEGIN {
  24507. require Exporter;
  24508. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  24509. $VERSION = '2.316';
  24510. @ISA = qw(Exporter);
  24511. import Amavis::Conf qw(:platform :confvars :sa c cr ca);
  24512. import Amavis::Util qw(ll do_log sanitize_str min max minmax
  24513. prolong_timer get_deadline);
  24514. import Amavis::ProcControl qw(exit_status_str proc_status_ok
  24515. kill_proc run_command run_command_consumer);
  24516. import Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local);
  24517. import Amavis::Timing qw(section_time);
  24518. }
  24519. use subs @EXPORT_OK;
  24520. use Errno qw(EIO EINTR EAGAIN ECONNRESET EBADF);
  24521. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  24522. use Time::HiRes ();
  24523. sub new {
  24524. my($class, $scanner_name,$module,@args) = @_;
  24525. my($cmd,$cmdargs,%options) = @args;
  24526. return if !defined $cmd || $cmd eq '';
  24527. bless {
  24528. scanner_name => $scanner_name, command => $cmd, args => $cmdargs,
  24529. options => \%options,
  24530. }, $class;
  24531. }
  24532. sub check {
  24533. my($self,$msginfo) = @_;
  24534. $self->check_or_learn($msginfo,undef);
  24535. };
  24536. sub auto_learn {
  24537. my($self,$msginfo,$learn_as) = @_;
  24538. $self->check_or_learn($msginfo,$learn_as);
  24539. }
  24540. # pass a mail message to an external (spam checking) program,
  24541. # extract interesting header fields from the result
  24542. #
  24543. sub check_or_learn {
  24544. my($self,$msginfo,$learn_as) = @_;
  24545. my $scanner_name = $self->{scanner_name};
  24546. my $cmd = $self->{command};
  24547. my $cmdargs; my $auto_learning;
  24548. if (!defined $learn_as) {
  24549. $cmdargs = $self->{args};
  24550. } elsif ($learn_as eq 'ham') {
  24551. $cmdargs = $self->{options}->{'learn_ham'}; $auto_learning = 1;
  24552. } elsif ($learn_as eq 'spam') {
  24553. $cmdargs = $self->{options}->{'learn_spam'}; $auto_learning = 1;
  24554. }
  24555. my $size_limit;
  24556. my $mbsl = $self->{options}->{'mail_body_size_limit'};
  24557. $mbsl = c('sa_mail_body_size_limit') if !defined $mbsl;
  24558. if (defined $mbsl) {
  24559. $size_limit = min(64*1024, $msginfo->orig_header_size) + 1 +
  24560. min($mbsl, $msginfo->orig_body_size);
  24561. # don't bother if slightly oversized, it's faster without size checks
  24562. undef $size_limit if $msginfo->msg_size < $size_limit + 5*1024;
  24563. }
  24564. my $prefix = '';
  24565. # fake a local delivery agent by inserting a Return-Path
  24566. $prefix .= sprintf("Return-Path: %s\n", $msginfo->sender_smtp);
  24567. $prefix .= sprintf("X-Envelope-To: %s\n",
  24568. join(",\n ",qquote_rfc2821_local(@{$msginfo->recips})));
  24569. my $os_fp = $msginfo->client_os_fingerprint;
  24570. $prefix .= sprintf("X-Amavis-OS-Fingerprint: %s\n",
  24571. sanitize_str($os_fp)) if defined($os_fp) && $os_fp ne '';
  24572. my(@av_tests);
  24573. my $per_recip_data = $msginfo->per_recip_data;
  24574. $per_recip_data = [] if !$per_recip_data;
  24575. for my $r (@$per_recip_data) {
  24576. my $spam_tests = $r->spam_tests;
  24577. if (defined $spam_tests) {
  24578. push(@av_tests,
  24579. grep(/^AV\..+=/, split(/,/, join(',',map($$_,@$spam_tests)))));
  24580. }
  24581. }
  24582. $prefix .= sprintf("X-Amavis-AV-Status: %s\n",
  24583. sanitize_str(join(',',@av_tests))) if @av_tests;
  24584. $prefix .= sprintf("X-Amavis-PolicyBank: %s\n", c('policy_bank_path'));
  24585. $prefix .= sprintf("X-Amavis-MessageSize: %d%s\n", $msginfo->msg_size,
  24586. !defined $size_limit ? '' : ", TRUNCATED to $size_limit");
  24587. my $resp_stdout_fh = IO::File->new; # parent reading side of the pipe
  24588. my $child_stdout_fh = IO::File->new; # child stdout writing side of a pipe
  24589. my $resp_stderr_fh = IO::File->new; # parent reading side of the pipe
  24590. my $child_stderr_fh = IO::File->new; # child stderr writing side of a pipe
  24591. pipe($resp_stdout_fh,$child_stdout_fh)
  24592. or die "$scanner_name: Can't create pipe1: $!";
  24593. pipe($resp_stderr_fh,$child_stderr_fh)
  24594. or die "$scanner_name: Can't create pipe2: $!";
  24595. binmode($resp_stdout_fh) or die "Can't set pipe1 to binmode: $!";
  24596. binmode($resp_stderr_fh) or die "Can't set pipe2 to binmode: $!";
  24597. my($proc_fh,$pid) = run_command_consumer('&='.fileno($child_stdout_fh),
  24598. '&='.fileno($child_stderr_fh),
  24599. $cmd, @$cmdargs);
  24600. $child_stdout_fh->close
  24601. or die "Parent failed to close child side of the pipe1: $!";
  24602. $child_stderr_fh->close
  24603. or die "Parent failed to close child side of the pipe2: $!";
  24604. undef $child_stdout_fh; undef $child_stderr_fh;
  24605. my($remaining_time, $deadline) = get_deadline($scanner_name.'_scan', 0.9, 5);
  24606. alarm(0); # stop the timer
  24607. my $proc_fd = fileno($proc_fh);
  24608. my $resp_stdout_fd = fileno($resp_stdout_fh);
  24609. my $resp_stderr_fd = fileno($resp_stderr_fh);
  24610. my $response = ''; my $response_stderr = ''; my $response_chopped = 0;
  24611. my $child_stat; my $bytes_sent = 0; my $err_on_child = 0;
  24612. my $msg = $msginfo->mail_text;
  24613. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  24614. $msg = $msg_str_ref if ref $msg_str_ref;
  24615. eval {
  24616. if (!defined $msg) {
  24617. # empty mail
  24618. } elsif (ref $msg ne 'SCALAR' && $msg->isa('MIME::Entity')) {
  24619. # $msg->print_body($proc_fh); # flushing the pipe?
  24620. die "$scanner_name: reading from MIME::Entity is not implemented";
  24621. } else { # handles a message in-memory or on a file
  24622. my $file_position = $msginfo->skip_bytes;
  24623. if (ref $msg ne 'SCALAR') {
  24624. $msg->seek($file_position, 0) or die "Can't rewind mail file: $!";
  24625. }
  24626. my $data_source = $prefix;
  24627. my $eof_on_response = 0;
  24628. my $eof_on_msg = 0; my $force_eof_on_msg = 0;
  24629. my($rout,$wout,$eout,$rin,$win,$ein); $rin=$win=$ein='';
  24630. vec($rin,$resp_stdout_fd,1) = 1;
  24631. vec($rin,$resp_stderr_fd,1) = 1;
  24632. for (;;) {
  24633. vec($win,$proc_fd,1) = 0;
  24634. vec($win,$proc_fd,1) = 1 if defined $proc_fh &&
  24635. (!$eof_on_msg || $data_source ne '');
  24636. $ein = $rin | $win;
  24637. my $timeout = max(2, $deadline - Time::HiRes::time);
  24638. my($nfound,$timeleft) =
  24639. select($rout=$rin, $wout=$win, $eout=$ein, $timeout);
  24640. defined $nfound && $nfound >= 0
  24641. or die "$scanner_name: select failed: $!";
  24642. if (vec($rout,$resp_stderr_fd,1)) {
  24643. my $inbuf = ''; $! = 0;
  24644. my $nread = sysread($resp_stderr_fh, $inbuf, 16384);
  24645. if (!defined($nread)) {
  24646. if ($! == EAGAIN || $! == EINTR) {
  24647. Time::HiRes::sleep(0.1); # slow down, just in case
  24648. } else {
  24649. do_log(0,"%s: error reading from pipe2: %s", $scanner_name,$!);
  24650. }
  24651. } elsif ($nread < 1) { # sysread returns 0 at eof
  24652. } else { # successful read
  24653. ll(5) && do_log(5, "rx stderr: %d %s [...]",
  24654. length($inbuf), substr($inbuf,0,1000));
  24655. $response_stderr .= $inbuf if length($response_stderr) < 10000;
  24656. }
  24657. }
  24658. if (vec($rout,$resp_stdout_fd,1)) {
  24659. my $inbuf = ''; $! = 0;
  24660. my $nread = sysread($resp_stdout_fh, $inbuf, 16384);
  24661. if (!defined($nread)) {
  24662. if ($! == EAGAIN || $! == EINTR) {
  24663. Time::HiRes::sleep(0.1); # slow down, just in case
  24664. } else {
  24665. $eof_on_response = 1;
  24666. die "$scanner_name: error reading from pipe1: $!";
  24667. }
  24668. } elsif ($nread < 1) { # sysread returns 0 at eof
  24669. $eof_on_response = 1;
  24670. } else { # successful read
  24671. ll(5) && do_log(5, "rx: %d %s [...]",
  24672. length($inbuf), substr($inbuf,0,30));
  24673. my $response_l = length($response);
  24674. if ($response_chopped || $response_l >= 65536) {
  24675. # ignore the rest of input
  24676. } else {
  24677. $response .= $inbuf;
  24678. my $j = $response_l <= 1 ? 0 : $response_l - 1;
  24679. # we only need a mail header from the returned text
  24680. $response_chopped = 1 if index($response,"\n\n",$j) >= 0;
  24681. }
  24682. }
  24683. }
  24684. if (vec($wout,$proc_fd,1)) { # subprocess is ready to receive more
  24685. if ($data_source eq '' && !$eof_on_msg) { # get more data
  24686. my $nread = 0;
  24687. if ($force_eof_on_msg) {
  24688. # pretend to already be at eof
  24689. } elsif (ref $msg ne 'SCALAR') { # message is on a file
  24690. $nread = $msg->read($data_source,32768);
  24691. } elsif ($file_position < length($$msg)) { # message in memory
  24692. # do it in chunks, saves memory, cache friendly
  24693. $data_source = substr($$msg,$file_position,32768);
  24694. $nread = length($data_source);
  24695. }
  24696. if (!$nread) {
  24697. $eof_on_msg = 1;
  24698. defined $nread or die "$scanner_name: error reading message: $!";
  24699. if (defined $proc_fh) { $proc_fh->close or $err_on_child=$! };
  24700. undef $proc_fh;
  24701. do_log(5,"tx: eof");
  24702. }
  24703. $file_position += $nread;
  24704. if (defined $size_limit) {
  24705. my $remaining_room = $size_limit - $bytes_sent;
  24706. $remaining_room = 0 if $remaining_room < 0;
  24707. if ($nread > $remaining_room) {
  24708. $data_source = substr($data_source, 0, $remaining_room);
  24709. do_log(5,"tx: (size limit) %d -> %d", $nread,$remaining_room);
  24710. $force_eof_on_msg = 1;
  24711. }
  24712. }
  24713. }
  24714. if ($data_source ne '' && defined $proc_fh) {
  24715. ll(5) && do_log(5, "tx: %d %s [...]",
  24716. length($data_source), substr($data_source,0,30));
  24717. # syswrite does a write(2), no need to call $proc_fh->flush
  24718. my $nwrite = syswrite($proc_fh, $data_source);
  24719. if (!defined($nwrite)) {
  24720. if ($! == EAGAIN || $! == EINTR) {
  24721. Time::HiRes::sleep(0.1); # slow down, just in case
  24722. } else {
  24723. $data_source = ''; $eof_on_msg = 1; # simulate an eof
  24724. do_log(-1,"%s: error writing to pipe: %s", $scanner_name,$!);
  24725. $proc_fh->close or $err_on_child=$!; undef $proc_fh;
  24726. do_log(5,"tx: eof (wr err)");
  24727. }
  24728. } elsif ($nwrite > 0) { # successful write
  24729. $bytes_sent += $nwrite;
  24730. if ($nwrite < length($data_source)) {
  24731. substr($data_source,0,$nwrite) = '';
  24732. } else {
  24733. $data_source = '';
  24734. }
  24735. }
  24736. }
  24737. }
  24738. last if $eof_on_response;
  24739. if (Time::HiRes::time >= $deadline) {
  24740. die "$scanner_name: exceeded allowed time\n";
  24741. }
  24742. }
  24743. }
  24744. if (defined $proc_fh) { $proc_fh->close or $err_on_child=$! }
  24745. $child_stat = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  24746. undef $proc_fh; undef $pid;
  24747. 1;
  24748. } or do {
  24749. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  24750. do_log(-1,"%s failed: %s", $scanner_name,$eval_stat);
  24751. kill_proc($pid,$scanner_name,1,$proc_fh,$eval_stat) if defined $pid;
  24752. undef $proc_fh; undef $pid;
  24753. };
  24754. prolong_timer($scanner_name); # restart timer
  24755. if (length($response_stderr) > 2000) {
  24756. $response_stderr = substr($response_stderr,0,2000) . '[...]';
  24757. }
  24758. if (proc_status_ok($child_stat,$err_on_child)) {
  24759. do_log(2, "%s stderr: %s",
  24760. $scanner_name,$response_stderr) if $response_stderr ne '';
  24761. } else {
  24762. do_log(-1,"%s stderr: %s",
  24763. $scanner_name,$response_stderr) if $response_stderr ne '';
  24764. die "$scanner_name: error running program $cmd: " .
  24765. exit_status_str($child_stat,$err_on_child) . "\n";
  24766. }
  24767. # keep just a header section in $response
  24768. if ($response eq '') {
  24769. # empty mail
  24770. } elsif (substr($response, 0,1) eq "\n") {
  24771. $response = ''; # empty header section
  24772. } else {
  24773. my $ind = index($response,"\n\n"); # find header/body separator
  24774. $response = substr($response, 0, $ind+1) if $ind >= 0;
  24775. }
  24776. my $crm114_score;
  24777. if ($cmd =~ /\bcrm/ && $response =~ /^\s*([+-]?\d*(?:\.\d*)?)\s*$/) {
  24778. $crm114_score = $1;
  24779. $response = ''; # skip the header parsing loop below
  24780. }
  24781. my(@response_lines) = split(/^/m, $response, -1);
  24782. push(@response_lines, "\n", "\n"); # insure a trailing NL and a separator
  24783. undef $response;
  24784. my(%header_field, @header_field_name, $curr_head);
  24785. # scan mail header section retrieved from an external program on its stdout
  24786. for my $ln (@response_lines) { # guaranteed to contain header/body separator
  24787. if ($ln =~ /^[ \t]/) { # folded
  24788. $curr_head .= $ln;
  24789. } else { # a new header field, process previous if any
  24790. if (defined $curr_head) {
  24791. local($1,$2);
  24792. if ($curr_head =~ /^ ( (?: X-DSPAM | X-CRM114 | X-Bogosity) [^:]*? )
  24793. [ \t]* : [ \t]* (.*) $/xs) {
  24794. my($hn,$hb) = ($1,$2); my $hnlc = lc($hn);
  24795. push(@header_field_name, $hn) if !exists($header_field{$hnlc});
  24796. $header_field{$hnlc} = $hb; # keep last
  24797. }
  24798. }
  24799. $curr_head = $ln;
  24800. last if $ln eq "\n";
  24801. }
  24802. }
  24803. my($spam_score, $spam_tests);
  24804. my $score_factor = $self->{options}->{'score_factor'};
  24805. my $dspam_result = $header_field{lc('X-DSPAM-Result')};
  24806. if (defined $dspam_result) {
  24807. if ($dspam_result =~ /\b(signature|result|probability|confidence)=.*;/) {
  24808. # combined result, split
  24809. my(%attribute);
  24810. for my $attr (split(/;\s*/, $dspam_result)) {
  24811. local($1,$2);
  24812. my($n,$v) = ($attr =~ /^([^=]*)=(.*)\z/s) ? ($1,$2) : ('user',$attr);
  24813. $v =~ s/^"//; $v =~ s/"\z//; $attribute{$n} = $v;
  24814. }
  24815. # simulate separate header fields
  24816. @header_field_name = qw(X-DSPAM-Result X-DSPAM-Class X-DSPAM-Confidence
  24817. X-DSPAM-Probability X-DSPAM-Signature);
  24818. for my $hn (@header_field_name) {
  24819. my $hnlc = lc($hn); my $name = $hnlc; $name =~ s/^X-DSPAM-//i;
  24820. $header_field{$hnlc} = $attribute{$name};
  24821. }
  24822. }
  24823. $dspam_result = $header_field{lc('X-DSPAM-Result')};
  24824. my $dspam_signature = $header_field{lc('X-DSPAM-Signature')};
  24825. $dspam_result = '' if !defined $dspam_result;
  24826. $dspam_signature = '' if !defined $dspam_signature;
  24827. chomp($dspam_result); chomp($dspam_signature);
  24828. $dspam_signature = '' if $dspam_signature eq 'N/A';
  24829. if (!$auto_learning) {
  24830. $msginfo->supplementary_info('DSPAMRESULT', $dspam_result);
  24831. $msginfo->supplementary_info('DSPAMSIGNATURE', $dspam_signature);
  24832. $msginfo->supplementary_info('VERDICT-'.$scanner_name, $dspam_result);
  24833. $spam_score = $dspam_result eq 'Spam' ? 10 : -1; # fabricated
  24834. $score_factor = 1 if !defined $score_factor;
  24835. $spam_score *= $score_factor;
  24836. $spam_tests = sprintf("%s.%s=%.3f",
  24837. $scanner_name, $dspam_result, $spam_score);
  24838. do_log(2,"%s result: %s, score=%.3f, sig=%s",
  24839. $scanner_name, $dspam_result, $spam_score, $dspam_signature);
  24840. }
  24841. }
  24842. my $crm114_status = $header_field{lc('X-CRM114-Status')};
  24843. if (defined $crm114_score || defined $crm114_status) {
  24844. local($1,$2);
  24845. if (!defined $crm114_status) { # presumably using --stats_only
  24846. # fabricate a Status from score
  24847. $crm114_status = !defined $crm114_score ? 'unknown'
  24848. : $crm114_score <= -10 ? uc('spam')
  24849. : $crm114_score >= +10 ? 'GOOD' : 'UNSURE';
  24850. $header_field{lc('X-CRM114-Status')} =
  24851. sprintf("%s ( %s )", $crm114_status, $crm114_score);
  24852. @header_field_name = qw(X-CRM114-Status);
  24853. } elsif ($crm114_status =~ /^([A-Z]+)\s+\(\s+([-\d\.]+)\s+\)/) {
  24854. $crm114_status = $1; $crm114_score = $2;
  24855. }
  24856. my $crm114_cacheid = $header_field{lc('X-CRM114-CacheID')};
  24857. if (defined $crm114_cacheid && $crm114_cacheid =~ /^sfid-\s*$/i) {
  24858. delete $header_field{lc('X-CRM114-CacheID')}; $crm114_cacheid = undef;
  24859. }
  24860. s/[ \t\r\n]+\z// for ($crm114_status, $crm114_score, $crm114_cacheid);
  24861. $score_factor = -0.10 if !defined $score_factor;
  24862. $spam_score = $score_factor * $crm114_score;
  24863. $spam_tests = sprintf("%s.%s(%s)=%.3f",
  24864. $scanner_name, $crm114_status, $crm114_score, $spam_score);
  24865. if (!$auto_learning) {
  24866. $msginfo->supplementary_info('VERDICT-'.$scanner_name,
  24867. uc $crm114_status eq 'GOOD' ? 'Ham' : $crm114_status);
  24868. $msginfo->supplementary_info('CRM114STATUS',
  24869. sprintf("%s ( %s )", $crm114_status,$crm114_score));
  24870. $msginfo->supplementary_info('CRM114SCORE', $crm114_score);
  24871. $msginfo->supplementary_info('CRM114CACHEID', $crm114_cacheid);
  24872. do_log(2,"%s result: score=%s (%s), status=%s, cacheid=%s",
  24873. $scanner_name, $spam_score,
  24874. $crm114_score, $crm114_status, $crm114_cacheid);
  24875. }
  24876. }
  24877. my $bogo_line = $header_field{lc('X-Bogosity')};
  24878. my($bogo_status, $bogo_score, $bogo_tests);
  24879. if (defined $bogo_line) {
  24880. ($bogo_status, $bogo_tests, $bogo_score) = split(/,\s*/,$bogo_line);
  24881. local($1);
  24882. $bogo_score =~ s/^spamicity=([0-9.+-]*).*\z/$1/s;
  24883. $spam_score = $bogo_status eq 'Spam' ? 5 : $bogo_status eq 'Ham' ? -5 : 0;
  24884. $score_factor = 1 if !defined $score_factor;
  24885. $spam_score = $score_factor * $spam_score;
  24886. # trim trailing fraction zeroes
  24887. $spam_score = 0 + sprintf("%.3f",$spam_score);
  24888. $spam_tests = sprintf("%s=%s", $scanner_name, $spam_score);
  24889. # $spam_tests = sprintf("%s(%s/%s)=%s",
  24890. # $scanner_name, $bogo_status, $bogo_score, $spam_score);
  24891. if (!$auto_learning) {
  24892. $msginfo->supplementary_info('VERDICT-'.$scanner_name, $bogo_status);
  24893. $msginfo->supplementary_info('BOGOSTATUS', sprintf("%s ( %s )",
  24894. $bogo_status, $bogo_score));
  24895. $msginfo->supplementary_info('BOGOSCORE', $bogo_score);
  24896. do_log(2,"%s result: score=%s (%s), status=%s",
  24897. $scanner_name, $spam_score, $bogo_score, $bogo_status);
  24898. }
  24899. }
  24900. if (!$auto_learning) {
  24901. my $hdr_edits = $msginfo->header_edits;
  24902. my $use_our_hdrs = cr('prefer_our_added_header_fields');
  24903. my $allowed_hdrs = cr('allowed_added_header_fields');
  24904. my $all_local = !grep(!$_->recip_is_local, @$per_recip_data);
  24905. for my $hn (@header_field_name) {
  24906. my $hnlc = lc($hn); my $hb = $header_field{$hnlc};
  24907. if (defined $hb) {
  24908. $hb =~ s/[ \t\r\n]+\z//; # trim trailing whitespace and eol
  24909. do_log(5,"%s: suppl attr: %s = '%s'", $scanner_name,$hn,$hb);
  24910. $msginfo->supplementary_info($hn,$hb);
  24911. # add header fields to passed mail for all recipients
  24912. if ($all_local && $allowed_hdrs && $allowed_hdrs->{$hnlc} &&
  24913. !($use_our_hdrs && $use_our_hdrs->{$hnlc})) {
  24914. $hdr_edits->add_header($hn,$hb,2);
  24915. }
  24916. }
  24917. }
  24918. if (defined $spam_score) {
  24919. $msginfo->supplementary_info('SCORE-'.$scanner_name, $spam_score);
  24920. for my $r (@$per_recip_data) {
  24921. $r->spam_level( ($r->spam_level || 0) + $spam_score );
  24922. if (!defined($r->spam_tests)) {
  24923. $r->spam_tests([ \$spam_tests ]);
  24924. } else {
  24925. push(@{$r->spam_tests}, \$spam_tests);
  24926. }
  24927. }
  24928. }
  24929. }
  24930. section_time($scanner_name);
  24931. }
  24932. 1;
  24933. __DATA__
  24934. #
  24935. package Amavis::SpamControl::SpamdClient;
  24936. use strict;
  24937. use re 'taint';
  24938. use warnings;
  24939. use warnings FATAL => qw(utf8 void);
  24940. no warnings 'uninitialized';
  24941. BEGIN {
  24942. require Exporter;
  24943. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  24944. $VERSION = '2.316';
  24945. @ISA = qw(Exporter);
  24946. import Amavis::Conf qw(:platform :confvars :sa c cr ca);
  24947. import Amavis::Util qw(ll do_log sanitize_str min max minmax get_deadline);
  24948. import Amavis::rfc2821_2822_Tools qw(qquote_rfc2821_local);
  24949. import Amavis::Timing qw(section_time);
  24950. }
  24951. use Errno qw(ENOENT EACCES);
  24952. sub new {
  24953. my($class, $scanner_name,$module,@args) = @_;
  24954. my(%options) = @args;
  24955. bless { scanner_name => $scanner_name, options => \%options }, $class;
  24956. }
  24957. # needs spamd running, could be started like this:
  24958. # spamd -H /var/amavis/home -r /var/amavis/home/spamd.pid -s stderr \
  24959. # -u vscan -g vscan -x -P --allow-tell --min-children=2 --max-children=2
  24960. sub check {
  24961. my($self,$msginfo) = @_;
  24962. my($which_section, $spam_level, $sa_tests, $size_limit, %attr);
  24963. my $scanner_name = $self->{scanner_name};
  24964. my $mbsl = $self->{options}->{'mail_body_size_limit'};
  24965. $mbsl = c('sa_mail_body_size_limit') if !defined $mbsl;
  24966. if (defined $mbsl) {
  24967. $size_limit = min(64*1024, $msginfo->orig_header_size) + 1 +
  24968. min($mbsl, $msginfo->orig_body_size);
  24969. # don't bother if slightly oversized, it's faster without size checks
  24970. undef $size_limit if $msginfo->msg_size < $size_limit + 5*1024;
  24971. }
  24972. my $hdr_edits = $msginfo->header_edits;
  24973. # fake a local delivery agent by inserting Return-Path
  24974. $which_section = 'prepare pseudo header section';
  24975. my $hdr_prefix = '';
  24976. $hdr_prefix .= sprintf("Return-Path: %s\n", $msginfo->sender_smtp);
  24977. $hdr_prefix .= sprintf("X-Envelope-To: %s\n",
  24978. join(",\n ",qquote_rfc2821_local(@{$msginfo->recips})));
  24979. my $os_fp = $msginfo->client_os_fingerprint;
  24980. $hdr_prefix .= sprintf("X-Amavis-OS-Fingerprint: %s\n",
  24981. sanitize_str($os_fp)) if defined($os_fp) && $os_fp ne '';
  24982. my(@av_tests);
  24983. my $per_recip_data = $msginfo->per_recip_data;
  24984. $per_recip_data = [] if !$per_recip_data;
  24985. for my $r (@$per_recip_data) {
  24986. my $spam_tests = $r->spam_tests;
  24987. if (defined $spam_tests) {
  24988. push(@av_tests,
  24989. grep(/^AV\..+=/, split(/,/, join(',',map($$_,@$spam_tests)))));
  24990. }
  24991. }
  24992. $hdr_prefix .= sprintf("X-Amavis-AV-Status: %s\n",
  24993. sanitize_str(join(',',@av_tests))) if @av_tests;
  24994. $hdr_prefix .= sprintf("X-Amavis-PolicyBank: %s\n", c('policy_bank_path'));
  24995. $hdr_prefix .= sprintf("X-Amavis-MessageSize: %d%s\n", $msginfo->msg_size,
  24996. !defined $size_limit ? '' : ", TRUNCATED to $size_limit");
  24997. my($remaining_time, $deadline) = get_deadline('spamd check', 1, 5);
  24998. my $msg = $msginfo->mail_text;
  24999. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  25000. $msg = $msg_str_ref if ref $msg_str_ref;
  25001. eval {
  25002. $which_section = 'spamd_connect'; do_log(3,"connecting to spamd");
  25003. my $spamd_handle = Amavis::IO::RW->new(
  25004. [ '127.0.0.1:783', '[::1]:783' ], Eol => "\015\012", Timeout => 10);
  25005. defined $spamd_handle or die "Can't connect to spamd, $@ ($!)";
  25006. $spamd_handle->timeout(max(2, $deadline - Time::HiRes::time));
  25007. section_time($which_section);
  25008. $which_section = 'spamd_tx'; do_log(4,"sending to spamd");
  25009. $hdr_prefix =~ s{\n}{\015\012}gs;
  25010. my $file_position = $msginfo->skip_bytes;
  25011. my $msgsize = length($hdr_prefix); # prepended lines...
  25012. $msgsize += $msginfo->msg_size; # size as defined by RFC 1870
  25013. $msgsize -= $file_position; # TODO: adjust for CRLF (alright for 0)
  25014. ll(5) && do_log(5, "spamc: message size: %d + %d - %d = %s",
  25015. length($hdr_prefix), $msginfo->msg_size, $file_position,
  25016. defined $size_limit && $msgsize > $size_limit
  25017. ? "LIM:$size_limit" : $msgsize);
  25018. if (defined $size_limit && $msgsize > $size_limit) {
  25019. # consider $size_limit in the RFC 1870 sense for simplicity
  25020. $msgsize = $size_limit;
  25021. }
  25022. $spamd_handle->print("SYMBOLS SPAMC/1.3\015\012"); # HEADERS
  25023. $spamd_handle->print("Content-length: " . $msgsize . "\015\012");
  25024. $spamd_handle->print("\015\012");
  25025. $spamd_handle->print($hdr_prefix);
  25026. my $bytes_written = length($hdr_prefix);
  25027. if (!defined $msg) {
  25028. # empty mail
  25029. } elsif (ref $msg eq 'SCALAR') {
  25030. # do it in chunks, saves memory, cache friendly
  25031. my $done;
  25032. while ($file_position < length($$msg)) {
  25033. my $buff = substr($$msg,$file_position,16384);
  25034. $file_position += length($buff);
  25035. $buff =~ s{\n}{\015\012}gs;
  25036. if (defined $size_limit &&
  25037. $bytes_written + length($buff) >= $size_limit) {
  25038. substr($buff, $size_limit - $bytes_written) = ''; # truncate
  25039. # spamd reads line-by-line and hangs if not terminated by a NL
  25040. substr($buff,-1,1) = "\012";
  25041. do_log(5,"spamc: reached size limit %d bytes, ".
  25042. "%d = %d (sent) + %d (still to go)",
  25043. $size_limit, $bytes_written+length($buff),
  25044. $bytes_written, length($buff));
  25045. $done = 1;
  25046. }
  25047. $spamd_handle->print($buff);
  25048. $bytes_written += length($buff);
  25049. last if $done;
  25050. }
  25051. } elsif ($msg->isa('MIME::Entity')) { # TODO - cont. length won't match!
  25052. do_log(3,"spamc: message is MIME::Entity, size won't match");
  25053. $msg->print_body($spamd_handle);
  25054. } else {
  25055. $msg->seek($file_position,0) or die "Can't rewind mail file: $!";
  25056. my($nbytes,$buff,$done);
  25057. while (($nbytes = $msg->sysread($buff,16384)) > 0) {
  25058. $file_position += $nbytes;
  25059. $buff =~ s{\n}{\015\012}gs;
  25060. if (defined $size_limit &&
  25061. $bytes_written + length($buff) >= $size_limit) {
  25062. substr($buff, $size_limit - $bytes_written) = ''; # truncate
  25063. # spamd reads line-by-line and hangs if not terminated by a NL
  25064. substr($buff,-1,1) = "\012";
  25065. do_log(5,"spamc: reached size limit %d bytes, ".
  25066. "%d = %d (sent) + %d (still to go)",
  25067. $size_limit, $bytes_written+length($buff),
  25068. $bytes_written, length($buff));
  25069. $done = 1;
  25070. }
  25071. $spamd_handle->print($buff);
  25072. $bytes_written += length($buff);
  25073. last if $done;
  25074. }
  25075. defined $nbytes or die "Error reading: $!";
  25076. }
  25077. $spamd_handle->flush;
  25078. $hdr_prefix = undef;
  25079. section_time($which_section);
  25080. $which_section = 'spamd_rx'; do_log(4,"receiving from spamd");
  25081. my($version, $resp_code, $resp_msg);
  25082. local($1,$2,$3); my($ln,$error,$first); $first = 1;
  25083. while (defined($ln = $spamd_handle->get_response_line)) {
  25084. do_log(4,"from spamd - resp.hdr: %s", $ln);
  25085. if ($ln eq "\015\012") {
  25086. last;
  25087. } elsif ($first) {
  25088. $first = 0; $ln =~ s/\015\012\z//;
  25089. ($version,$resp_code,$resp_msg) = split(/[ \t]+/,$ln,3);
  25090. } elsif ($ln =~ /^([^:]*?)[ \t]*:[ \t]*(.*)\015\012\z/i) {
  25091. $attr{lc($1)} = $2;
  25092. } else { $error = $ln }
  25093. }
  25094. if ($first) { do_log(-1,"Empty spamd response") }
  25095. elsif (defined $error) { do_log(-1,"Error in spamd resp: %s",$error) }
  25096. elsif ($resp_code !~ /^\d+\z/ || $resp_code != 0) {
  25097. do_log(-1,"Failure reported by spamd: %s %s %s",
  25098. $version,$resp_code,$resp_msg);
  25099. } else {
  25100. my $reply_len = 0;
  25101. while (defined($ln = $spamd_handle->get_response_line)) {
  25102. do_log(5,"from spamd: %s", $ln);
  25103. $reply_len += length($ln); $ln =~ s/\015\012\z//; $sa_tests = $ln;
  25104. }
  25105. do_log(-1,"Reply from spamd size mismatch: %d %s",
  25106. $reply_len, $attr{'content-length'}
  25107. ) if $reply_len != $attr{'content-length'};
  25108. }
  25109. $spamd_handle->close; # terminate the session, ignoring status
  25110. undef $spamd_handle;
  25111. $spam_level = $2 if $attr{'spam'} =~ m{(\S+) ; (\S+) / (\S+)};
  25112. 1;
  25113. } or do {
  25114. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  25115. do_log(-1,"%s client failed: %s", $scanner_name, $eval_stat);
  25116. };
  25117. section_time($which_section);
  25118. my $score_factor = $self->{options}->{'score_factor'};
  25119. if (defined $spam_level && defined $score_factor) {
  25120. $spam_level *= $score_factor;
  25121. }
  25122. do_log(2,"%s spamd score=%s, tests=%s",
  25123. $scanner_name, $spam_level, $sa_tests);
  25124. $msginfo->supplementary_info('SCORE-'.$scanner_name, $spam_level);
  25125. $msginfo->supplementary_info('VERDICT-'.$scanner_name,
  25126. $attr{'spam'} =~ /^True/ ? 'Spam'
  25127. : $attr{'spam'} =~ /^False/ ? 'Ham' : 'Unknown');
  25128. for my $r (@$per_recip_data) {
  25129. $r->spam_level( ($r->spam_level || 0) + $spam_level );
  25130. if (!defined($r->spam_tests)) {
  25131. $r->spam_tests([ \$sa_tests ]);
  25132. } else {
  25133. push(@{$r->spam_tests}, \$sa_tests);
  25134. }
  25135. }
  25136. }
  25137. 1;
  25138. __DATA__
  25139. #
  25140. package Mail::SpamAssassin::Logger::Amavislog;
  25141. use strict;
  25142. use re 'taint';
  25143. use warnings;
  25144. use warnings FATAL => qw(utf8 void);
  25145. no warnings 'uninitialized';
  25146. BEGIN {
  25147. require Exporter;
  25148. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  25149. $VERSION = '2.316';
  25150. @ISA = qw(Exporter);
  25151. # let a 'require' understand that this module is already loaded:
  25152. $INC{'Mail/SpamAssassin/Logger/Amavislog.pm'} = 'amavisd';
  25153. import Amavis::Util qw(ll do_log);
  25154. }
  25155. sub new {
  25156. my($class,%args) = @_;
  25157. my(%llmap) = (error => -1, warn => 0, info => 1, dbg => 3);
  25158. # $args{debug} is a simple boolean, sets the log level floor to 1 when true
  25159. if ($args{debug}) { for (keys %llmap) { $llmap{$_} = 1 if $llmap{$_} > 1 } }
  25160. bless { llmap => \%llmap }, $class;
  25161. }
  25162. sub close_log { 1 }
  25163. sub log_message {
  25164. my($self, $level,$msg) = @_;
  25165. my $ll = $self->{llmap}->{$level};
  25166. $ll = 1 if !defined $ll;
  25167. ll($ll) && do_log($ll, "SA %s: %s", $level,$msg);
  25168. 1;
  25169. }
  25170. 1;
  25171. package Amavis::SpamControl::SpamAssassin;
  25172. use strict;
  25173. use re 'taint';
  25174. use warnings;
  25175. use warnings FATAL => qw(utf8 void);
  25176. no warnings 'uninitialized';
  25177. BEGIN {
  25178. require Exporter;
  25179. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  25180. $VERSION = '2.316';
  25181. @ISA = qw(Exporter);
  25182. import Amavis::Conf qw(:platform :confvars :sa $daemon_user c cr ca);
  25183. import Amavis::Util qw(ll do_log do_log_safe sanitize_str prolong_timer
  25184. add_entropy min max minmax get_deadline);
  25185. import Amavis::ProcControl qw(exit_status_str proc_status_ok
  25186. kill_proc run_command run_as_subprocess
  25187. collect_results collect_results_structured);
  25188. import Amavis::rfc2821_2822_Tools;
  25189. import Amavis::Timing qw(section_time);
  25190. import Amavis::Lookup qw(lookup lookup2);
  25191. import Amavis::IO::FileHandle;
  25192. }
  25193. use subs @EXPORT_OK;
  25194. use Errno qw(ENOENT EACCES EAGAIN EBADF);
  25195. use FileHandle;
  25196. use Mail::SpamAssassin;
  25197. sub getCommonSAModules {
  25198. my $self = shift;
  25199. my(@modules) = qw(
  25200. Mail::SpamAssassin::Locker
  25201. Mail::SpamAssassin::Locker::Flock
  25202. Mail::SpamAssassin::Locker::UnixNFSSafe
  25203. Mail::SpamAssassin::PersistentAddrList
  25204. Mail::SpamAssassin::DBBasedAddrList
  25205. Mail::SpamAssassin::AutoWhitelist
  25206. Mail::SpamAssassin::BayesStore
  25207. Mail::SpamAssassin::BayesStore::DBM
  25208. Mail::SpamAssassin::PerMsgLearner
  25209. Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX
  25210. Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR
  25211. Net::DNS::RR::CNAME Net::DNS::RR::DNAME
  25212. Net::DNS::RR::TXT Net::DNS::RR::SPF Net::DNS::RR::NAPTR
  25213. Net::DNS::RR::RP Net::DNS::RR::HINFO Net::DNS::RR::AFSDB
  25214. Net::CIDR::Lite
  25215. Sys::Hostname::Long
  25216. URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL
  25217. URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login
  25218. URI::_query URI::_segment URI::_server URI::_userpass
  25219. URI::_idna URI::_punycode URI::data URI::ftp
  25220. URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps
  25221. URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync
  25222. URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet
  25223. URI::tn3270 URI::urn URI::urn::oid
  25224. URI::file URI::file::Base URI::file::Unix URI::file::Win32
  25225. );
  25226. # DBD::mysql
  25227. # Mail::SpamAssassin::BayesStore::SQL
  25228. # Mail::SpamAssassin::SQLBasedAddrList
  25229. # ??? ArchiveIterator Reporter Getopt::Long Sys::Syslog lib
  25230. # Net::Ping
  25231. @modules;
  25232. }
  25233. sub getSA2Modules {
  25234. qw(Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM
  25235. );
  25236. # Mail::SpamAssassin::SpamCopURI
  25237. }
  25238. sub getSA31Modules {
  25239. qw( );
  25240. # Mail::SpamAssassin::BayesStore::MySQL
  25241. # Mail::SpamAssassin::BayesStore::PgSQL
  25242. }
  25243. sub getSA32Modules {
  25244. qw(Mail::SpamAssassin::Bayes Mail::SpamAssassin::Bayes::CombineChi
  25245. Mail::SpamAssassin::Locales Encode::Detect
  25246. );
  25247. # Mail::SpamAssassin::BayesStore::MySQL
  25248. # Mail::SpamAssassin::BayesStore::PgSQL
  25249. # /var/db/spamassassin/compiled/.../Mail/SpamAssassin/CompiledRegexps/body_0.pm
  25250. }
  25251. sub getSAPlugins {
  25252. my($self) = @_;
  25253. my(@modules);
  25254. my $sa_version_num = $self->{version_num};
  25255. push(@modules, qw(Hashcash RelayCountry SPF URIDNSBL)) if $sa_version_num>=3;
  25256. push(@modules, qw(DKIM)) if $sa_version_num >= 3.001002;
  25257. if ($sa_version_num >= 3.001000) {
  25258. push(@modules, qw(
  25259. AWL AccessDB AntiVirus AutoLearnThreshold DCC MIMEHeader Pyzor Razor2
  25260. ReplaceTags TextCat URIDetail WhiteListSubject));
  25261. # 'DomainKeys' plugin fell out of fashion with SA 3.2.0, don't load it
  25262. # 'SpamCop' loads Net::SMTP and Net::Cmd, not needed otherwise
  25263. }
  25264. if ($sa_version_num >= 3.002000) {
  25265. push(@modules, qw(
  25266. BodyEval DNSEval HTMLEval HeaderEval MIMEEval RelayEval URIEval WLBLEval
  25267. ASN Bayes BodyRuleBaseExtractor Check HTTPSMismatch OneLineBodyRuleType
  25268. ImageInfo Rule2XSBody Shortcircuit VBounce));
  25269. }
  25270. if ($sa_version_num >= 3.004000) {
  25271. push(@modules, qw(AskDNS));
  25272. }
  25273. $_ = 'Mail::SpamAssassin::Plugin::'.$_ for @modules;
  25274. my(%mod_names) = map(($_,1), @modules);
  25275. # add supporting modules
  25276. push(@modules, qw(Razor2::Client::Agent))
  25277. if $mod_names{'Mail::SpamAssassin::Plugin::Razor2'};
  25278. push(@modules, qw(IP::Country::Fast))
  25279. if $mod_names{'Mail::SpamAssassin::Plugin::RelayCountry'};
  25280. # push(@modules,
  25281. # qw(Mail::DomainKeys Mail::DomainKeys::Message Mail::DomainKeys::Policy))
  25282. # if $mod_names{'Mail::SpamAssassin::Plugin::DomainKeys'};
  25283. push(@modules, qw(Mail::DKIM Mail::DKIM::Verifier))
  25284. if $mod_names{'Mail::SpamAssassin::Plugin::DKIM'};
  25285. push(@modules, qw(Image::Info Image::Info::GIF Image::Info::JPEG
  25286. Image::Info::PNG Image::Info::BMP Image::Info::TIFF))
  25287. if $mod_names{'Mail::SpamAssassin::Plugin::ImageInfo'};
  25288. if ($mod_names{'Mail::SpamAssassin::Plugin::SPF'}) {
  25289. if ($sa_version_num < 3.002000) {
  25290. # only the old Mail::SPF::Query was supported
  25291. push(@modules, qw(Mail::SPF::Query));
  25292. } else {
  25293. # SA 3.2.0 supports both the newer Mail::SPF and the old Mail::SPF::Query
  25294. # but we won't be loading the Mail::SPF::Query
  25295. push(@modules, qw(
  25296. Mail::SPF Mail::SPF::Server Mail::SPF::Request
  25297. Mail::SPF::Mech Mail::SPF::Mech::A Mail::SPF::Mech::PTR
  25298. Mail::SPF::Mech::All Mail::SPF::Mech::Exists Mail::SPF::Mech::IP4
  25299. Mail::SPF::Mech::IP6 Mail::SPF::Mech::Include Mail::SPF::Mech::MX
  25300. Mail::SPF::Mod Mail::SPF::Mod::Exp Mail::SPF::Mod::Redirect
  25301. Mail::SPF::SenderIPAddrMech
  25302. Mail::SPF::v1::Record Mail::SPF::v2::Record
  25303. NetAddr::IP NetAddr::IP::Util
  25304. auto::NetAddr::IP::_compV6 auto::NetAddr::IP::short
  25305. auto::NetAddr::IP::InetBase::inet_any2n
  25306. auto::NetAddr::IP::InetBase::inet_n2ad
  25307. auto::NetAddr::IP::InetBase::inet_n2dx
  25308. auto::NetAddr::IP::InetBase::inet_ntoa
  25309. auto::NetAddr::IP::InetBase::ipv6_aton
  25310. auto::NetAddr::IP::InetBase::ipv6_ntoa
  25311. ));
  25312. }
  25313. }
  25314. if ($mod_names{'Mail::SpamAssassin::Plugin::DomainKeys'} ||
  25315. $mod_names{'Mail::SpamAssassin::Plugin::DKIM'}) {
  25316. push(@modules, qw(
  25317. Crypt::OpenSSL::RSA
  25318. auto::Crypt::OpenSSL::RSA::new_public_key
  25319. auto::Crypt::OpenSSL::RSA::new_key_from_parameters
  25320. auto::Crypt::OpenSSL::RSA::get_key_parameters
  25321. auto::Crypt::OpenSSL::RSA::import_random_seed
  25322. Digest::SHA Error));
  25323. }
  25324. # HTML/HeadParser.pm
  25325. # do_log(5, "getSAPlugins %s: %s", $sa_version_num, join(', ',@modules));
  25326. @modules;
  25327. }
  25328. # invoked by a parent process before forking and chrooting
  25329. #
  25330. sub loadSpamAssassinModules {
  25331. my $self = shift;
  25332. my $sa_version_num = $self->{version_num};
  25333. my @modules; # modules to be loaded before chroot takes place
  25334. push(@modules, $self->getCommonSAModules);
  25335. if (!defined($sa_version_num)) {
  25336. die "loadSpamAssassinModules: unknown version of Mail::SpamAssassin";
  25337. } elsif ($sa_version_num < 3) {
  25338. push(@modules, $self->getSA2Modules);
  25339. } elsif ($sa_version_num >= 3.001 && $sa_version_num < 3.002) {
  25340. push(@modules, $self->getSA31Modules);
  25341. } elsif ($sa_version_num >= 3.002) {
  25342. push(@modules, $self->getSA32Modules);
  25343. }
  25344. push(@modules, $self->getSAPlugins);
  25345. my $missing;
  25346. $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0,
  25347. @modules) if @modules;
  25348. do_log(2, 'INFO: SA version: %s, %.6f, no optional modules: %s',
  25349. $self->{version}, $sa_version_num, join(' ',@$missing))
  25350. if ref $missing && @$missing;
  25351. }
  25352. # invoked by a parent process before forking but after chrooting
  25353. #
  25354. sub initializeSpamAssassinLogger {
  25355. my $self = shift;
  25356. local($1,$2,$3,$4,$5,$6); # just in case
  25357. if (!Mail::SpamAssassin::Logger->UNIVERSAL::can('add')) {
  25358. # old SA?
  25359. } elsif (!Mail::SpamAssassin::Logger::add(method => 'Amavislog',
  25360. debug => $sa_debug )) {
  25361. do_log(-1,"Mail::SpamAssassin::Logger::add failed");
  25362. } else { # successfully rigged SpamAssassin with our logger
  25363. Mail::SpamAssassin::Logger::remove('stderr'); # remove default SA logger
  25364. unshift(@sa_debug_fac, 'info', !$sa_debug ? () : 'all');
  25365. }
  25366. }
  25367. # invoked by a parent process before forking but after chrooting
  25368. #
  25369. sub new_SpamAssassin_instance {
  25370. my($self,$running_as_parent) = @_;
  25371. # pick next available number as an instance name
  25372. my $sa_instance_name = sprintf('%s', scalar @{$self->{instances}});
  25373. do_log(1, "initializing Mail::SpamAssassin (%s)", $sa_instance_name);
  25374. my $sa_version_num = $self->{version_num};
  25375. my(@new_sa_debug_fac);
  25376. for my $fac (@sa_debug_fac) { # handle duplicates and negation: foo,nofoo,x,x
  25377. my $bfac = $fac; $bfac =~ s/^none\z/noall/i; $bfac =~ s/^no(?=.)//si;
  25378. @new_sa_debug_fac = grep(!/^(no)?\Q$bfac\E\z/si, @new_sa_debug_fac);
  25379. push(@new_sa_debug_fac, $fac);
  25380. }
  25381. do_log(2,"SpamAssassin debug facilities: %s", join(',',@sa_debug_fac));
  25382. my $sa_args = {
  25383. debug => !@sa_debug_fac ? undef : \@sa_debug_fac,
  25384. save_pattern_hits => grep(lc($_) eq 'all', @sa_debug_fac) ? 1 : 0,
  25385. dont_copy_prefs => 1,
  25386. require_rules => 1,
  25387. stop_at_threshold => 0,
  25388. need_tags => 'TIMING,LANGUAGES,RELAYCOUNTRY,ASN,ASNCIDR',
  25389. local_tests_only => $sa_local_tests_only,
  25390. home_dir_for_helpers => $helpers_home,
  25391. rules_filename => $sa_configpath,
  25392. site_rules_filename => $sa_siteconfigpath,
  25393. userprefs_filename => $sa_userprefs_file,
  25394. skip_prng_reseeding => 1, # we'll do it ourselves (SA 3.4.0)
  25395. # PREFIX => '/usr/local',
  25396. # DEF_RULES_DIR => '/usr/local/share/spamassassin',
  25397. # LOCAL_RULES_DIR => '/etc/mail/spamassassin',
  25398. # LOCAL_STATE_DIR => '/var/lib/spamassassin',
  25399. #see Mail::SpamAssassin man page for other options
  25400. };
  25401. if ($sa_version_num < 3.001005 && !defined $sa_args->{LOCAL_STATE_DIR})
  25402. { $sa_args->{LOCAL_STATE_DIR} = '/var/lib' } # don't ignore sa-update rules
  25403. local($1,$2,$3,$4,$5,$6); # avoid Perl bug, $1 gets tainted in compile_now
  25404. my $spamassassin_obj = Mail::SpamAssassin->new($sa_args);
  25405. # $Mail::SpamAssassin::DEBUG->{rbl}=-3;
  25406. # $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64;
  25407. if ($running_as_parent) {
  25408. # load SA config files and rules, try to preload most modules
  25409. $spamassassin_obj->compile_now;
  25410. }
  25411. if (ll(2) && !@{$self->{instances}}) {
  25412. # created the first/main/only SA instance
  25413. if ($spamassassin_obj->UNIVERSAL::can('get_loaded_plugins_list')) {
  25414. my(@plugins) = $spamassassin_obj->get_loaded_plugins_list;
  25415. do_log(2, "SpamAssassin loaded plugins: %s", join(', ', sort
  25416. map { my $n = ref $_; $n =~ s/^Mail::SpamAssassin::Plugin:://; $n }
  25417. @plugins));
  25418. # printf STDOUT ("%s\n", join(", ",@plugins));
  25419. # not in use: AccessDB AntiVirus TextCat; ASN BodyRuleBaseExtractor
  25420. # OneLineBodyRuleType Rule2XSBody Shortcircuit
  25421. }
  25422. }
  25423. # provide a default username
  25424. my $sa_uname = $spamassassin_obj->{username};
  25425. if (!defined $sa_uname || $sa_uname eq '')
  25426. { $spamassassin_obj->{username} = $sa_uname = $daemon_user }
  25427. $self->{default_username} = $sa_uname if !defined $self->{default_username};
  25428. my $sa_instance = {
  25429. instance_name => $sa_instance_name,
  25430. spamassassin_obj => $spamassassin_obj,
  25431. loaded_user_name => $sa_uname, loaded_user_config => '',
  25432. conf_backup => undef, conf_backup_additional => {},
  25433. };
  25434. # remember some initial settings, like %msa_backup in spamd
  25435. for (qw(username user_dir userstate_dir learn_to_journal)) {
  25436. if (exists $spamassassin_obj->{$_}) {
  25437. $sa_instance->{conf_backup_additional}{$_} = $spamassassin_obj->{$_};
  25438. }
  25439. }
  25440. push(@{$self->{instances}}, $sa_instance);
  25441. alarm(0); # seems like SA forgets to clear alarm in some cases
  25442. umask($self->{saved_umask}); # restore our umask, SA clobbered it
  25443. section_time('SA new');
  25444. $sa_instance;
  25445. }
  25446. sub new {
  25447. my($class, $scanner_name,$module,@args) = @_;
  25448. my(%options) = @args;
  25449. my $self =
  25450. bless { scanner_name => $scanner_name, options => \%options }, $class;
  25451. $self->{initialized_stage} = 1;
  25452. $self->{saved_umask} = umask;
  25453. my $sa_version = Mail::SpamAssassin->Version;
  25454. local($1,$2,$3);
  25455. my $sa_version_num; # turn '3.1.8-pre1' into 3.001008
  25456. $sa_version_num = sprintf("%d.%03d%03d", $1,$2,$3)
  25457. if $sa_version =~ /^(\d+)\.(\d+)(?:\.(\d+))/; # ignore trailing non-digits
  25458. $self->{version} = $sa_version;
  25459. $self->{version_num} = $sa_version_num;
  25460. $self->{default_username} = undef;
  25461. $self->{instances} = [];
  25462. $self;
  25463. }
  25464. sub init_pre_chroot {
  25465. my $self = shift;
  25466. $self->{initialized_stage} == 1
  25467. or die "Wrong initialization sequence: " . $self->{initialized_stage};
  25468. $self->loadSpamAssassinModules;
  25469. $self->{initialized_stage} = 2;
  25470. }
  25471. sub init_pre_fork {
  25472. my $self = shift;
  25473. $self->{initialized_stage} == 2
  25474. or die "Wrong initialization sequence: " . $self->{initialized_stage};
  25475. $self->initializeSpamAssassinLogger;
  25476. $self->new_SpamAssassin_instance(1) for (1 .. max(1,$sa_num_instances));
  25477. $self->{initialized_stage} = 3;
  25478. }
  25479. sub init_child {
  25480. my $self = shift;
  25481. $self->{initialized_stage} == 3
  25482. or die "Wrong initialization sequence: " . $self->{initialized_stage};
  25483. for my $sa_instance (@{$self->{instances}}) {
  25484. my $spamassassin_obj = $sa_instance->{spamassassin_obj};
  25485. next if !$spamassassin_obj;
  25486. $spamassassin_obj->call_plugins("spamd_child_init");
  25487. umask($self->{saved_umask}); # restore our umask, SA may have clobbered it
  25488. }
  25489. $self->{initialized_stage} = 4;
  25490. }
  25491. sub rundown_child {
  25492. my $self = shift;
  25493. for my $sa_instance (@{$self->{instances}}) {
  25494. my $spamassassin_obj = $sa_instance->{spamassassin_obj};
  25495. next if !$spamassassin_obj;
  25496. do_log(3,'SA rundown_child (%s)', $sa_instance->{instance_name});
  25497. $spamassassin_obj->call_plugins("spamd_child_post_connection_close");
  25498. umask($self->{saved_umask}); # restore our umask, SA may have clobbered it
  25499. }
  25500. $self->{initialized_stage} = 5;
  25501. }
  25502. sub call_spamassassin($$$$) {
  25503. my($self,$msginfo,$lines,$size_limit) = @_;
  25504. my(@result); my($mail_obj,$per_msg_status);
  25505. my $which_section = 'SA prepare';
  25506. my $saved_pid = $$; my $sa_version_num = $self->{version_num};
  25507. my $msg = $msginfo->mail_text; # a file handle or a string ref
  25508. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  25509. $msg = $msg_str_ref if ref $msg_str_ref;
  25510. # pass data to SpamAssassin as ARRAY or GLOB or STRING or STRING_REF
  25511. my $data_representation = ref($msg) eq 'SCALAR' ? 'STRING' : 'GLOB';
  25512. $data_representation = 'STRING_REF'
  25513. if $data_representation eq 'STRING' && $sa_version_num >= 3.004000;
  25514. my $data; # this will be passed to SpamAssassin's parser
  25515. local(*F);
  25516. if ($data_representation eq 'STRING' ||
  25517. $data_representation eq 'STRING_REF') {
  25518. $which_section = 'SA msg read';
  25519. $data = join('', @$lines); # a string to be passed to SpamAssassin
  25520. if (!defined $msg) {
  25521. # empty mail
  25522. } elsif (ref $msg eq 'SCALAR') {
  25523. $data .= $$msg;
  25524. } elsif ($msg->isa('MIME::Entity')) {
  25525. die "passing a MIME::Entity object to SpamAssassin is not implemented";
  25526. } else { # read message into memory, yuck
  25527. my $file_position = $msginfo->skip_bytes;
  25528. $msg->seek($file_position, 0) or die "Can't rewind mail file: $!";
  25529. my $nbytes;
  25530. while (($nbytes = $msg->sysread($data, 32768, length($data))) > 0) {
  25531. $file_position += $nbytes;
  25532. last if defined $size_limit && length($data) > $size_limit;
  25533. }
  25534. defined $nbytes or die "Error reading: $!";
  25535. }
  25536. if (defined $size_limit && length($data) > $size_limit) {
  25537. substr($data,$size_limit) = "[...]\n";
  25538. }
  25539. section_time($which_section);
  25540. } elsif ($data_representation eq 'ARRAY') {
  25541. # read message into memory, yuck - even worse: line-by-line
  25542. $which_section = 'SA msg read'; my $ln; my $len = 0;
  25543. if (defined $size_limit) { $len += length($_) for @$lines }
  25544. $msg->seek($msginfo->skip_bytes, 0) or die "Can't rewind mail file: $!";
  25545. for ($! = 0; defined($ln=<$msg>); $! = 0) { # header section
  25546. push(@$lines,$ln);
  25547. if (defined $size_limit)
  25548. { $len += length($ln); last if $len > $size_limit }
  25549. last if $ln eq "\n";
  25550. }
  25551. defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug
  25552. $! == EBADF ? do_log(0,"Error reading mail header section: %s", $!)
  25553. : die "Error reading mail header section: $!";
  25554. if (!defined $size_limit) {
  25555. for ($! = 0; defined($ln=<$msg>); $! = 0) { push(@$lines,$ln) } # body
  25556. } else {
  25557. for ($! = 0; defined($ln=<$msg>); $! = 0) { # body
  25558. push(@$lines,$ln);
  25559. $len += length($ln); last if $len > $size_limit;
  25560. }
  25561. }
  25562. defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug
  25563. $! == EBADF ? do_log(1,"Error reading mail body: %s", $!)
  25564. : die "Error reading mail body: $!";
  25565. $data = $lines; # array of lines to be passed to SpamAssassin
  25566. section_time($which_section);
  25567. }
  25568. my $eval_stat;
  25569. $which_section = 'SA prelim';
  25570. eval {
  25571. if ($data_representation eq 'GLOB') { # pass mail as a GLOB to SpamAssassin
  25572. ref($msg) ne 'SCALAR' # expects $msg to be a file handle
  25573. or die "panic: data_representation is GLOB, but message is in memory";
  25574. do_log(2,"truncating a message passed to SA at %d bytes, orig %d",
  25575. $size_limit, $msginfo->msg_size) if defined $size_limit;
  25576. # present a virtual file to SA, an original mail file prefixed by @$lines
  25577. tie(*F,'Amavis::IO::FileHandle');
  25578. open(F, $msg,$lines,$size_limit) or die "Can't open SA virtual file: $!";
  25579. binmode(F) or die "Can't set binmode on a SA virtual file: $!";
  25580. $data = \*F; # a GLOB to be passed to SpamAssassin
  25581. }
  25582. $which_section = 'SA userconf';
  25583. my $sa_default_username = $self->{default_username};
  25584. my $per_recip_data = $msginfo->per_recip_data;
  25585. $per_recip_data = [] if !$per_recip_data;
  25586. my $uconf_maps_ref = ca('sa_userconf_maps');
  25587. my $uname_maps_ref = ca('sa_username_maps');
  25588. $uconf_maps_ref = [] if !$uconf_maps_ref;
  25589. $uname_maps_ref = [] if !$uname_maps_ref;
  25590. my(%uconf_filename_available);
  25591. my(%sa_configs_hash); # collects distinct config names and usernames
  25592. my $uconf_unsupported = 0;
  25593. my $r_ind = 0;
  25594. for my $r (@$per_recip_data) {
  25595. my($uconf,$uname);
  25596. my $recip_addr = $r->recip_addr;
  25597. $uconf = lookup2(0, $recip_addr, $uconf_maps_ref) if @$uconf_maps_ref;
  25598. $uname = lookup2(0, $recip_addr, $uname_maps_ref) if @$uname_maps_ref;
  25599. $uconf = '' if !defined $uconf;
  25600. $uname = $sa_default_username if !defined $uname || $uname eq '';
  25601. if ($uconf =~ /^sql:/i) {
  25602. $uconf = $uname eq $sa_default_username ? '' : 'sql:'.$uname;
  25603. }
  25604. if ($sa_version_num < 3.003000 && $uconf ne '') {
  25605. $uconf = ''; $uconf_unsupported = 1;
  25606. }
  25607. if ($uconf eq '') {
  25608. # ok, no special config required, just using a default
  25609. } elsif ($uconf =~ /^sql:/i) {
  25610. # assume data is in SQL, possibly an empty set
  25611. } else {
  25612. $uconf = "$MYHOME/$uconf" if $uconf !~ m{^/};
  25613. if ($uconf_filename_available{$uconf}) {
  25614. # good, already checked and is available, keep it
  25615. } elsif (defined $uconf_filename_available{$uconf}) {
  25616. # defined but false, already checked and failed, use a default config
  25617. $uconf = '';
  25618. } else {
  25619. # check for existence of a SA user configuration/preferences file
  25620. my(@stat_list) = stat($uconf); # symlinks-friendly
  25621. my $errn = @stat_list ? 0 : 0+$!;
  25622. my $msg = $errn == ENOENT ? "does not exist"
  25623. : $errn ? "is inaccessible: $!"
  25624. : -d _ ? "is a directory"
  25625. : !-f _ ? "is not a regular file"
  25626. : !-r _ ? "is not readable" : undef;
  25627. if (defined $msg) {
  25628. do_log(1,'SA user config file "%s" %s, ignoring it', $uconf,$msg);
  25629. $uconf_filename_available{$uconf} = 0; # defined but false
  25630. $uconf = ''; # ignoring it, use a default config
  25631. } else {
  25632. $uconf_filename_available{$uconf} = 1;
  25633. }
  25634. }
  25635. }
  25636. # collect lists of recipient indices for each unique config/user pair
  25637. # the %sa_configs_hash is a two-level hash: on $uconf and $uname
  25638. my $p = $sa_configs_hash{$uconf};
  25639. if (!$p) { $sa_configs_hash{$uconf} = $p = {} }
  25640. if (!exists $p->{$uname}) { $p->{$uname} = $r_ind }
  25641. else { $p->{$uname} .= ',' . $r_ind }
  25642. $r_ind++;
  25643. }
  25644. if ($uconf_unsupported) {
  25645. do_log(5,'SA user config loading unsupported for SA older than 3.3.0');
  25646. }
  25647. # refresh $sa_instance->{loaded_user_name}, just in case
  25648. for my $sa_instance (@{$self->{instances}}) {
  25649. my $spamassassin_obj = $sa_instance->{spamassassin_obj};
  25650. next if !$spamassassin_obj;
  25651. my $sa_uname = $spamassassin_obj->{username};
  25652. $sa_instance->{loaded_user_name} = defined $sa_uname ? $sa_uname : '';
  25653. }
  25654. my $sa_instance = $self->{instances}[0];
  25655. my $curr_conf = $sa_instance->{loaded_user_config};
  25656. my $curr_user = $sa_instance->{loaded_user_name};
  25657. # switching config files is the most expensive, sort to minimize switching
  25658. my(@conf_names); # a list of config names for which SA needs to be called;
  25659. # sorted: current first, baseline second, then the rest
  25660. push(@conf_names, $curr_conf) if exists $sa_configs_hash{$curr_conf};
  25661. push(@conf_names, '') if $curr_conf ne '' && exists $sa_configs_hash{''};
  25662. push(@conf_names,
  25663. grep($_ ne '' && $_ ne $curr_conf, keys %sa_configs_hash));
  25664. # call SA checking for each distinct SA userprefs config filename and user
  25665. for my $conf_user_pair (map { my $c = $_;
  25666. map([$c,$_], keys %{$sa_configs_hash{$c}})
  25667. } @conf_names) {
  25668. my($uconf,$uname) = @$conf_user_pair;
  25669. # comma-separated list of recip indices which use this SA config
  25670. my $rind_list = $sa_configs_hash{$uconf}{$uname};
  25671. do_log(5, "SA user config: \"%s\", username: \"%s\", %s",
  25672. $uconf, $uname, $rind_list);
  25673. my $sa_instance;
  25674. if (@{$self->{instances}} <= 1) {
  25675. # pick the only choice
  25676. $sa_instance = $self->{instances}[0];
  25677. } else {
  25678. # choosing a suitably-matching SpamAssassin instance
  25679. my(@sa_instances_matching_uconf, @sa_instances_matching_both,
  25680. @sa_instances_available);
  25681. for my $sa_instance (@{$self->{instances}}) {
  25682. next if !$sa_instance->{spamassassin_obj};
  25683. push(@sa_instances_available, $sa_instance);
  25684. if ($sa_instance->{loaded_user_config} eq $uconf) {
  25685. push(@sa_instances_matching_uconf, $sa_instance);
  25686. if ($sa_instance->{loaded_user_name} eq $uname) {
  25687. push(@sa_instances_matching_both, $sa_instance);
  25688. }
  25689. }
  25690. }
  25691. my $fit_descr;
  25692. if (@sa_instances_matching_both) {
  25693. # just pick the first
  25694. $sa_instance = $sa_instances_matching_both[0];
  25695. $fit_descr = sprintf('exact fit, %d choices',
  25696. scalar @sa_instances_matching_both);
  25697. } elsif (@sa_instances_matching_uconf) {
  25698. # picking one at random
  25699. my $j = @sa_instances_matching_uconf <= 1 ? 0
  25700. : int(rand(scalar(@sa_instances_matching_uconf)));
  25701. $sa_instance = $sa_instances_available[$j];
  25702. $fit_descr = sprintf('good fit: same config, other user, %d choices',
  25703. scalar @sa_instances_matching_uconf);
  25704. } elsif ($uconf eq '') {
  25705. # the first instance is a good choice for switching to a dflt config
  25706. $sa_instance = $self->{instances}[0];
  25707. $fit_descr = 'need a default config, picking first';
  25708. } elsif (@sa_instances_available <= 1) {
  25709. $sa_instance = $sa_instances_available[0];
  25710. $fit_descr = 'different config, picking the only one available';
  25711. } elsif (@sa_instances_available == 2) {
  25712. $sa_instance = $sa_instances_available[1];
  25713. $fit_descr = 'different config, picking the second one';
  25714. } else {
  25715. # picking one at random, preferably not the first
  25716. my $j = 1+int(rand(scalar(@sa_instances_available)-1));
  25717. $sa_instance = $sa_instances_available[$j];
  25718. $fit_descr = 'different config, picking one at random';
  25719. }
  25720. do_log(2,'SA instance chosen (%s), %s',
  25721. $sa_instance->{instance_name}, $fit_descr);
  25722. }
  25723. my $curr_conf = $sa_instance->{loaded_user_config};
  25724. my $curr_user = $sa_instance->{loaded_user_name};
  25725. my $spamassassin_obj = $sa_instance->{spamassassin_obj};
  25726. if ($curr_conf ne '' && $curr_conf ne $uconf) {
  25727. # revert SA configuration to its initial state
  25728. $which_section = 'revert_config';
  25729. ref $sa_instance->{conf_backup}
  25730. or die "panic, no conf_backup available";
  25731. for (qw(username user_dir userstate_dir learn_to_journal)) {
  25732. if (exists $sa_instance->{conf_backup_additional}{$_}) {
  25733. $spamassassin_obj->{$_} =
  25734. $sa_instance->{conf_backup_additional}{$_};
  25735. } else {
  25736. delete $spamassassin_obj->{$_};
  25737. }
  25738. }
  25739. # config leaks fixed in SpamAssassin 3.3.0, SA bug 6205, 6003, 4179
  25740. $spamassassin_obj->copy_config($sa_instance->{conf_backup}, undef)
  25741. or die "copy_config: failed to restore";
  25742. $sa_instance->{loaded_user_config} = $curr_conf = '';
  25743. do_log(5,"SA user config reverted to a saved copy");
  25744. section_time($which_section);
  25745. }
  25746. if ($uconf ne '' && $uconf ne $curr_conf) {
  25747. # load SA user configuration/preferences
  25748. if (!defined $sa_instance->{conf_backup}) {
  25749. $which_section = 'save_config';
  25750. do_log(5,"saving SA user config");
  25751. $sa_instance->{conf_backup} = {};
  25752. $spamassassin_obj->copy_config(undef, $sa_instance->{conf_backup})
  25753. or die "copy_config: failed to save configuration";
  25754. section_time($which_section);
  25755. }
  25756. $which_section = 'load_config';
  25757. # User preferences include scoring options, scores, whitelists
  25758. # and blacklists, etc, but do not include rule definitions,
  25759. # privileged settings, etc. unless allow_user_rules is enabled;
  25760. # and they never include administrator settings
  25761. if ($uconf =~ /^sql:/) {
  25762. $uconf eq 'sql:'.$uname
  25763. or die "panic: loading SA config mismatch: $uname <-> $uconf";
  25764. do_log(5,"loading SA user config from SQL %s", $uname);
  25765. $spamassassin_obj->load_scoreonly_sql($uname);
  25766. } else {
  25767. do_log(5,"loading SA user config file %s", $uconf);
  25768. $spamassassin_obj->read_scoreonly_config($uconf);
  25769. }
  25770. $sa_instance->{loaded_user_config} = $curr_conf = $uconf;
  25771. section_time($which_section);
  25772. }
  25773. if ($uname ne $curr_user) {
  25774. $which_section = 'SA switch_user';
  25775. do_log(5,'switching SA (%s) username "%s" -> "%s"',
  25776. $sa_instance->{instance_name}, $curr_user, $uname);
  25777. $spamassassin_obj->signal_user_changed({ username => $uname });
  25778. $sa_instance->{loaded_user_name} = $curr_user = $uname;
  25779. section_time($which_section);
  25780. }
  25781. ll(3) && do_log(3, "calling SA parse (%s), SA vers %s, %.6f, ".
  25782. "data as %s, recips_ind [%s]%s%s",
  25783. $sa_instance->{instance_name},
  25784. $self->{version}, $sa_version_num,
  25785. $data_representation, $rind_list,
  25786. ($uconf eq '' ? '' : ", conf: \"$uconf\""),
  25787. ($uname eq '' ? '' : ", user: \"$uname\"") );
  25788. if ($data_representation eq 'GLOB') {
  25789. seek(F,0,0) or die "Can't rewind a SA virtual file: $!";
  25790. }
  25791. $spamassassin_obj->timer_reset
  25792. if $spamassassin_obj->UNIVERSAL::can('timer_reset');
  25793. $which_section = 'SA parse';
  25794. my($remaining_time, $deadline) = get_deadline('SA check', 1, 5);
  25795. my(%suppl_attrib) = (
  25796. 'skip_prng_reseed' => 1, # do not call srand(), we already did it
  25797. 'return_path' => $msginfo->sender_smtp,
  25798. 'recipients' => [ map(qquote_rfc2821_local($_->recip_addr),
  25799. @$per_recip_data[split(/,/, $rind_list)]) ],
  25800. 'originating' => $msginfo->originating ? 1 : 0,
  25801. 'message_size' => $msginfo->msg_size,
  25802. !c('enable_dkim_verification') ? ()
  25803. : ('dkim_signatures' => $msginfo->dkim_signatures_all),
  25804. !defined $deadline ? ()
  25805. : ('master_deadline' => $deadline),
  25806. 'rule_hits' => [
  25807. # known options: rule, area, score, value, ruletype, tflags, descr
  25808. # { rule=>'AM:TEST1', score=>0.11 },
  25809. # { rule=>'TESTTEST', defscore=>0.22, descr=>'my test' },
  25810. !defined $size_limit ? () :
  25811. { rule=>'__TRUNCATED', score=>-0.1, area=>'RAW: ', tflags=>'nice',
  25812. descr=>"Message size truncated to $size_limit B" },
  25813. ],
  25814. 'amavis_policy_bank_path' => c('policy_bank_path'),
  25815. );
  25816. $mail_obj = $sa_version_num < 3
  25817. ? Mail::SpamAssassin::NoMailAudit->new(data=>$data, add_From_line=>0)
  25818. : $spamassassin_obj->parse(
  25819. $data_representation eq 'STRING_REF' ? \$data : $data,
  25820. 0, \%suppl_attrib);
  25821. section_time($which_section);
  25822. $which_section = 'SA check';
  25823. if (@conf_names <= 1) {
  25824. do_log(4,"CALLING SA check (%s)", $sa_instance->{instance_name});
  25825. } else {
  25826. do_log(4,"CALLING SA check (%s) for recips: %s",
  25827. $sa_instance->{instance_name},
  25828. join(", ", @{$suppl_attrib{'recipients'}}));
  25829. }
  25830. { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.x bug, $1 gets tainted
  25831. $per_msg_status = $spamassassin_obj->check($mail_obj);
  25832. }
  25833. do_log(4,"DONE SA check (%s)", $sa_instance->{instance_name});
  25834. section_time($which_section);
  25835. $which_section = 'SA collect';
  25836. my($spam_level,$spam_report,$spam_summary,%supplementary_info);
  25837. { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.x taint bug
  25838. if ($sa_version_num < 3) {
  25839. $spam_level = $per_msg_status->get_hits;
  25840. $supplementary_info{'TESTSSCORES'} = $supplementary_info{'TESTS'} =
  25841. $per_msg_status->get_names_of_tests_hit;
  25842. } else {
  25843. $spam_level = $per_msg_status->get_score;
  25844. for my $t (qw(TESTS TESTSSCORES ADDEDHEADERHAM ADDEDHEADERSPAM
  25845. AUTOLEARN AUTOLEARNSCORE SC SCRULE SCTYPE
  25846. LANGUAGES RELAYCOUNTRY ASN ASNCIDR DCCB DCCR DCCREP
  25847. DKIMDOMAIN DKIMIDENTITY AWLSIGNERMEAN
  25848. CRM114STATUS CRM114SCORE CRM114CACHEID)) {
  25849. $supplementary_info{$t} = $per_msg_status->get_tag($t);
  25850. }
  25851. }
  25852. { # fudge
  25853. my $crm114_status = $supplementary_info{'CRM114STATUS'};
  25854. my $crm114_score = $supplementary_info{'CRM114SCORE'};
  25855. if (defined $crm114_status && defined $crm114_score) {
  25856. $supplementary_info{'CRM114STATUS'} =
  25857. sprintf("%s ( %s )", $crm114_status,$crm114_score);
  25858. }
  25859. }
  25860. $spam_summary = $per_msg_status->get_report; # taints $1 and $2 !
  25861. # $spam_summary = $per_msg_status->get_tag('SUMMARY');
  25862. $spam_report = $per_msg_status->get_tag('REPORT');
  25863. # do the fetching of a TIMING tag last:
  25864. $supplementary_info{'TIMING'} = $per_msg_status->get_tag('TIMING');
  25865. }
  25866. # section_time($which_section); # don't bother reporting separately, short
  25867. $which_section = 'SA check finish';
  25868. if (defined $per_msg_status)
  25869. { $per_msg_status->finish; undef $per_msg_status }
  25870. if (defined $mail_obj)
  25871. { $mail_obj->finish if $sa_version_num >= 3; undef $mail_obj }
  25872. # section_time($which_section); # don't bother reporting separately, short
  25873. # returning the result as a data structure instead of modifying
  25874. # the $msginfo objects directly is used to make it possible to run
  25875. # this subroutine as a subprocess; modifications to $msginfo objects
  25876. # would be lost if done in a context of a spawned process
  25877. push(@result, {
  25878. recip_ind_list => $rind_list, user_config => $uconf,
  25879. spam_level => $spam_level,
  25880. spam_report => $spam_report, spam_summary => $spam_summary,
  25881. supplementary_info => \%supplementary_info,
  25882. });
  25883. }
  25884. 1;
  25885. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  25886. $which_section = 'SA finish';
  25887. if (defined $per_msg_status) # just in case
  25888. { $per_msg_status->finish; undef $per_msg_status }
  25889. if (defined $mail_obj) # just in case
  25890. { $mail_obj->finish if $sa_version_num >= 3; undef $mail_obj }
  25891. if ($data_representation eq 'GLOB') {
  25892. close(F) or die "Can't close SA virtual file: $!";
  25893. untie(*F);
  25894. }
  25895. umask($self->{saved_umask}); # restore our umask, SA may have clobbered it
  25896. if ($$ != $saved_pid) {
  25897. do_log_safe(-2,"PANIC, SA checking produced a clone process ".
  25898. "of [%s], CLONE [%s] SELF-TERMINATING", $saved_pid,$$);
  25899. POSIX::_exit(6); # avoid END and destructor processing
  25900. }
  25901. # section_time($which_section);
  25902. if (defined $eval_stat) { chomp $eval_stat; die $eval_stat } # resignal
  25903. \@result;
  25904. }
  25905. sub check {
  25906. my($self,$msginfo) = @_;
  25907. $self->{initialized_stage} == 4
  25908. or die "Wrong initialization sequence: " . $self->{initialized_stage};
  25909. my $scanner_name = $self->{scanner_name};
  25910. my $which_section; my $prefix = '';
  25911. my($spam_level,$sa_tests,$spam_report,$spam_summary,$supplementary_info_ref);
  25912. my $hdr_edits = $msginfo->header_edits;
  25913. my $size_limit;
  25914. my $mbsl = $self->{options}->{'mail_body_size_limit'};
  25915. $mbsl = c('sa_mail_body_size_limit') if !defined $mbsl;
  25916. if (defined $mbsl) {
  25917. $size_limit = min(64*1024, $msginfo->orig_header_size) + 1 +
  25918. min($mbsl, $msginfo->orig_body_size);
  25919. # don't bother if slightly oversized, it's faster without size checks
  25920. undef $size_limit if $msginfo->msg_size < $size_limit + 5*1024;
  25921. }
  25922. # fake a local delivery agent by inserting a Return-Path
  25923. $prefix .= sprintf("Return-Path: %s\n", $msginfo->sender_smtp);
  25924. $prefix .= sprintf("X-Envelope-To: %s\n",
  25925. join(",\n ",qquote_rfc2821_local(@{$msginfo->recips})));
  25926. my $os_fp = $msginfo->client_os_fingerprint;
  25927. $prefix .= sprintf("X-Amavis-OS-Fingerprint: %s\n",
  25928. sanitize_str($os_fp)) if defined($os_fp) && $os_fp ne '';
  25929. my(@av_tests);
  25930. for my $r (@{$msginfo->per_recip_data}) {
  25931. my $spam_tests = $r->spam_tests;
  25932. if ($spam_tests) {
  25933. push(@av_tests,
  25934. grep(/^AV[.:].+=/, split(/,/, join(',',map($$_,@$spam_tests)))));
  25935. }
  25936. }
  25937. $prefix .= sprintf("X-Amavis-AV-Status: %s\n",
  25938. sanitize_str(join(',',@av_tests))) if @av_tests;
  25939. $prefix .= sprintf("X-Amavis-PolicyBank: %s\n", c('policy_bank_path'));
  25940. $prefix .= sprintf("X-Amavis-MessageSize: %d%s\n", $msginfo->msg_size,
  25941. !defined $size_limit ? '' : ", TRUNCATED to $size_limit");
  25942. for my $hf_name (qw(
  25943. X-CRM114-Status X-CRM114-CacheID X-CRM114-Notice X-CRM114-Action
  25944. X-DSPAM-Result X-DSPAM-Class X-DSPAM-Signature X-DSPAM-Processed
  25945. X-DSPAM-Confidence X-DSPAM-Probability X-DSPAM-User X-DSPAM-Factors)) {
  25946. my $suppl_attr_val = $msginfo->supplementary_info($hf_name);
  25947. if (defined $suppl_attr_val && $suppl_attr_val ne '') {
  25948. chomp $suppl_attr_val;
  25949. $prefix .= sprintf("%s: %s\n", $hf_name, sanitize_str($suppl_attr_val));
  25950. }
  25951. }
  25952. $which_section = 'SA call';
  25953. my($proc_fh,$pid); my $eval_stat; my $results_aref;
  25954. eval {
  25955. # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose,
  25956. # disabling it before returning. It seems it only uses timer when
  25957. # external tests are enabled.
  25958. local $SIG{ALRM} = sub {
  25959. my $s = Carp::longmess("SA TIMED OUT, backtrace:");
  25960. # crop at some rather arbitrary limit
  25961. if (length($s) > 900) { $s = substr($s,0,900-3) . '[...]' }
  25962. do_log(-1,"%s",$s);
  25963. };
  25964. prolong_timer('spam_scan_sa_pre', 1, 4); # restart the timer
  25965. #
  25966. # note: array @lines at this point contains only prepended synthesized
  25967. # header fields, but may be extended in sub call_spamassassin() by
  25968. # reading-in the rest of the message; this may or may not happen in
  25969. # a separate process (called through run_as_subprocess or directly);
  25970. # lines must each be terminated by a \n character, which must be the
  25971. # only \n in a line;
  25972. #
  25973. my(@lines) = split(/^/m, $prefix, -1); $prefix = undef;
  25974. if (!$sa_spawned) {
  25975. $results_aref = call_spamassassin($self,$msginfo,\@lines,$size_limit);
  25976. } else {
  25977. ($proc_fh,$pid) = run_as_subprocess(\&call_spamassassin,
  25978. $self,$msginfo,\@lines,$size_limit);
  25979. my($results,$child_stat) =
  25980. collect_results_structured($proc_fh,$pid,'spawned SA',200*1024);
  25981. $results_aref = $results->[0] if defined $results;
  25982. }
  25983. 1;
  25984. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  25985. section_time($which_section) if $sa_spawned;
  25986. $which_section = 'SA done';
  25987. prolong_timer('spam_scan_sa'); # restart the timer
  25988. if ($results_aref) {
  25989. # for each group of recipients using the same SA userconf file
  25990. for my $h (@$results_aref) {
  25991. my $rind_list = $h->{recip_ind_list};
  25992. my(@r_list) = @{$msginfo->per_recip_data}[split(/,/,$rind_list)];
  25993. my $uconf = $h->{user_config};
  25994. $spam_level = $h->{spam_level};
  25995. $spam_report = $h->{spam_report}; $spam_summary = $h->{spam_summary};
  25996. $supplementary_info_ref = $h->{supplementary_info};
  25997. $supplementary_info_ref = {} if !$supplementary_info_ref;
  25998. $sa_tests = $supplementary_info_ref->{'TESTSSCORES'};
  25999. add_entropy($spam_level,$sa_tests);
  26000. my $score_factor = $self->{options}->{'score_factor'};
  26001. if (defined $spam_level && defined $score_factor) {
  26002. $spam_level *= $score_factor;
  26003. }
  26004. do_log(3,"spam_scan: score=%s autolearn=%s tests=[%s] recips=%s",
  26005. $spam_level, $supplementary_info_ref->{'AUTOLEARN'},
  26006. $sa_tests, $rind_list);
  26007. my(%sa_tests_h);
  26008. if (defined $sa_tests && $sa_tests ne 'none') {
  26009. for my $t (split(/,[ \t]*/, $sa_tests)) {
  26010. my($test_name,$score) = split(/=/, $t, 2);
  26011. $sa_tests_h{$test_name} = $score;
  26012. }
  26013. }
  26014. my $dkim_adsp_suppress = 0;
  26015. if (exists $sa_tests_h{'DKIM_ADSP_DISCARD'}) {
  26016. # must honour ADSP 'discardable', suppress a bounce
  26017. do_log(2,"spam_scan: dsn_suppress_reason DKIM_ADSP_DISCARD");
  26018. $dkim_adsp_suppress = 1;
  26019. }
  26020. $msginfo->supplementary_info('SCORE-'.$scanner_name, $spam_level);
  26021. $msginfo->supplementary_info('VERDICT-'.$scanner_name,
  26022. $spam_level >= 5 ? 'Spam' : $spam_level < 1 ? 'Ham' : 'Unknown');
  26023. for my $r (@r_list) {
  26024. $r->spam_level( ($r->spam_level || 0) + $spam_level );
  26025. $r->spam_report($spam_report); $r->spam_summary($spam_summary);
  26026. if (!defined($r->spam_tests)) {
  26027. $r->spam_tests([ \$sa_tests ]);
  26028. } else {
  26029. # comes last: here we use push, unlike elsewhere which may do unshift
  26030. push(@{$r->spam_tests}, \$sa_tests);
  26031. }
  26032. if ($dkim_adsp_suppress) {
  26033. $r->dsn_suppress_reason('DKIM_ADSP_DISCARD' .
  26034. !defined $_ ? '' : ", $_") for $r->dsn_suppress_reason;
  26035. }
  26036. }
  26037. }
  26038. }
  26039. if (defined($msginfo->spam_report) || defined($msginfo->spam_summary)) {
  26040. $spam_report = $msginfo->spam_report . ', ' . $spam_report
  26041. if $msginfo->spam_report ne '';
  26042. $spam_summary = $msginfo->spam_summary . "\n\n" . $spam_summary
  26043. if $msginfo->spam_summary ne '';
  26044. }
  26045. $msginfo->spam_report($spam_report); $msginfo->spam_summary($spam_summary);
  26046. for (keys %$supplementary_info_ref)
  26047. { $msginfo->supplementary_info($_, $supplementary_info_ref->{$_}) }
  26048. if (defined $eval_stat) { # SA timed out?
  26049. kill_proc($pid,'a spawned SA',1,$proc_fh,$eval_stat) if defined $pid;
  26050. undef $proc_fh; undef $pid; chomp $eval_stat;
  26051. do_log(-2, "SA failed: %s", $eval_stat);
  26052. # die "$eval_stat\n" if $eval_stat !~ /timed out\b/;
  26053. }
  26054. }
  26055. 1;
  26056. __DATA__
  26057. #
  26058. package Amavis::Unpackers;
  26059. use strict;
  26060. use re 'taint';
  26061. use warnings;
  26062. use warnings FATAL => qw(utf8 void);
  26063. no warnings 'uninitialized';
  26064. BEGIN {
  26065. require Exporter;
  26066. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  26067. $VERSION = '2.316';
  26068. @ISA = qw(Exporter);
  26069. @EXPORT_OK = qw(&init &decompose_part &determine_file_types);
  26070. import Amavis::Util qw(untaint min max minmax ll do_log snmp_count
  26071. prolong_timer rmdir_recursively add_entropy);
  26072. import Amavis::ProcControl qw(exit_status_str proc_status_ok run_command
  26073. kill_proc collect_results collect_results_structured);
  26074. import Amavis::Conf qw(:platform :confvars $file c cr ca);
  26075. import Amavis::Timing qw(section_time);
  26076. import Amavis::Lookup qw(lookup lookup2);
  26077. import Amavis::Unpackers::MIME qw(mime_decode);
  26078. import Amavis::Unpackers::NewFilename qw(consumed_bytes);
  26079. }
  26080. use subs @EXPORT_OK;
  26081. use Errno qw(ENOENT EACCES EINTR EAGAIN);
  26082. use POSIX qw(SIGALRM);
  26083. use IO::File qw(O_CREAT O_EXCL O_WRONLY);
  26084. use Time::HiRes ();
  26085. use File::Basename qw(basename);
  26086. use Compress::Zlib 1.35; # avoid security vulnerability in <= 1.34
  26087. use Archive::Zip 1.14 qw(:CONSTANTS :ERROR_CODES);
  26088. # avoid an exploitable security hole in Convert::UUlib 1.04 and older!
  26089. use Convert::UUlib 1.05 qw(:constants); # 1.08 or newer is preferred!
  26090. use Convert::TNEF; #***
  26091. # recursively descend into a directory $dir containing potentially unsafe
  26092. # files with unpredictable names, soft links, etc., rename each regular
  26093. # nonempty file to a directory $outdir giving it a generated name,
  26094. # and discard all the rest, including the directory $dir.
  26095. # Return a pair: number of bytes that 'sanitized' files now occupy,
  26096. # and a number of parts-objects created.
  26097. #
  26098. sub flatten_and_tidy_dir($$$;$$); # prototype
  26099. sub flatten_and_tidy_dir($$$;$$) {
  26100. my($dir, $outdir, $parent_obj, $item_num_offset, $orig_names) = @_;
  26101. do_log(4, 'flatten_and_tidy_dir: processing directory "%s"', $dir);
  26102. my $consumed_bytes = 0;
  26103. my $item_num = 0; my $parent_placement = $parent_obj->mime_placement;
  26104. chmod(0750, $dir) or die "Can't change protection of \"$dir\": $!";
  26105. local(*DIR); opendir(DIR,$dir) or die "Can't open directory \"$dir\": $!";
  26106. # modifying a directory while traversing it can cause surprises, avoid;
  26107. # avoid slurping the whole directory contents into memory
  26108. my($f, @rmfiles, @renames, @recurse);
  26109. while (defined($f = readdir(DIR))) {
  26110. next if $f eq '.' || $f eq '..';
  26111. my $msg; my $fname = $dir . '/' . $f;
  26112. my(@stat_list) = lstat($fname); my $errn = @stat_list ? 0 : 0+$!;
  26113. if ($errn == ENOENT) { $msg = "does not exist" }
  26114. elsif ($errn) { $msg = "inaccessible: $!" }
  26115. if (defined $msg) { die "flatten_and_tidy_dir: \"$fname\" $msg," }
  26116. add_entropy(@stat_list);
  26117. my $newpart_obj = Amavis::Unpackers::Part->new($outdir,$parent_obj);
  26118. $item_num++;
  26119. $newpart_obj->mime_placement(sprintf("%s/%d", $parent_placement,
  26120. $item_num+$item_num_offset) );
  26121. # save tainted original member name if available, or a tainted file name
  26122. my $original_name = !ref($orig_names) ? undef : $orig_names->{$f};
  26123. $newpart_obj->name_declared(defined $original_name ? $original_name : $f);
  26124. # untaint, but if $dir happens to still be tainted, we want to know and die
  26125. $fname = $dir . '/' . untaint($f);
  26126. if (-d _) {
  26127. $newpart_obj->attributes_add('D');
  26128. push(@recurse, $fname);
  26129. } elsif (-l _) {
  26130. $newpart_obj->attributes_add('L');
  26131. push(@rmfiles, [$fname, 'soft link']);
  26132. } elsif (!-f _) {
  26133. $newpart_obj->attributes_add('S');
  26134. push(@rmfiles, [$fname, 'nonregular file']);
  26135. } elsif (-z _) {
  26136. push(@rmfiles, [$fname, 'empty file']);
  26137. } else {
  26138. chmod(0750, $fname)
  26139. or die "Can't change protection of file \"$fname\": $!";
  26140. my $size = 0 + (-s _);
  26141. $newpart_obj->size($size);
  26142. $consumed_bytes += $size;
  26143. my $newpart = $newpart_obj->full_name;
  26144. push(@renames, [$fname, $newpart, $original_name]);
  26145. }
  26146. }
  26147. closedir(DIR) or die "Error closing directory \"$dir\": $!";
  26148. my $cnt_u = scalar(@rmfiles);
  26149. for my $pair (@rmfiles) {
  26150. my($fname,$what) = @$pair;
  26151. do_log(5,'flatten_and_tidy_dir: deleting %s "%s"', $what,$fname);
  26152. unlink($fname) or die "Can't remove $what \"$fname\": $!";
  26153. }
  26154. undef @rmfiles;
  26155. my $cnt_r = scalar(@renames);
  26156. for my $tuple (@renames) {
  26157. my($fname,$newpart,$original_name) = @$tuple;
  26158. ll(5) && do_log(5,'flatten_and_tidy_dir: renaming "%s"%s to %s', $fname,
  26159. !defined $original_name ? '' : " ($original_name)", $newpart);
  26160. rename($fname,$newpart) or die "Can't rename \"$fname\" to $newpart: $!";
  26161. }
  26162. undef @renames;
  26163. for my $fname (@recurse) {
  26164. do_log(5,'flatten_and_tidy_dir: descending into subdir "%s"', $fname);
  26165. my($bytes,$cnt) = flatten_and_tidy_dir($fname, $outdir, $parent_obj,
  26166. $item_num+$item_num_offset, $orig_names);
  26167. $consumed_bytes += $bytes; $item_num += $cnt;
  26168. }
  26169. rmdir($dir) or die "Can't remove directory \"$dir\": $!";
  26170. section_time("ren$cnt_r-unl$cnt_u-files$item_num");
  26171. ($consumed_bytes, $item_num);
  26172. }
  26173. # call 'file(1)' utility for each part,
  26174. # and associate (save) full and short file content types with each part
  26175. #
  26176. sub determine_file_types($$) {
  26177. my($tempdir, $partslist_ref) = @_;
  26178. defined $file && $file ne ''
  26179. or die "Unix utility file(1) not available, but is needed";
  26180. my(@all_part_list) = grep($_->exists, @$partslist_ref);
  26181. my $initial_num_parts = scalar(@all_part_list);
  26182. my $cwd = "$tempdir/parts";
  26183. if (@all_part_list) { chdir($cwd) or die "Can't chdir to $cwd: $!" }
  26184. my($proc_fh,$pid); my $eval_stat;
  26185. eval {
  26186. while (@all_part_list) {
  26187. my(@part_list,@file_list); # collect reasonably small subset of filenames
  26188. my $arglist_size = length($file); # size of a command name itself
  26189. while (@all_part_list) { # collect as many args as safe, at least one
  26190. my $nm = $all_part_list[0]->full_name;
  26191. local($1); $nm =~ s{^\Q$cwd\E/(.*)\z}{$1}s; # remove cwd from filename
  26192. # POSIX requires 4 kB as a minimum buffer size for program arguments
  26193. last if @file_list && $arglist_size + length($nm) + 1 > 4000;
  26194. push(@part_list, shift(@all_part_list)); # swallow the next one
  26195. push(@file_list, $nm); $arglist_size += length($nm) + 1;
  26196. }
  26197. if (scalar(@file_list) < $initial_num_parts) {
  26198. do_log(2, "running file(1) on %d (out of %d) files, arglist size %d",
  26199. scalar(@file_list), $initial_num_parts, $arglist_size);
  26200. } else {
  26201. do_log(5, "running file(1) on %d files, arglist size %d",
  26202. scalar(@file_list), $arglist_size);
  26203. }
  26204. ($proc_fh,$pid) = run_command(undef, '&1', $file, @file_list);
  26205. my $index = 0; my $ln;
  26206. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  26207. do_log(5, "result line from file(1): %s", $ln);
  26208. chomp($ln); local($1,$2);
  26209. if ($index > $#file_list) {
  26210. do_log(-1,"NOTICE: Skipping unexpected output from file(1): %s",$ln);
  26211. } else {
  26212. my $part = $part_list[$index]; # walk through @part_list in sync
  26213. my $expect = $file_list[$index]; # walk through @file_list in sync
  26214. if ($ln !~ /^(\Q$expect\E):[ \t]*(.*)\z/s) {
  26215. # split file name from type
  26216. do_log(-1,"NOTICE: Skipping bad output from file(1) ".
  26217. "at [%d, %s], got: %s", $index,$expect,$ln);
  26218. } else {
  26219. my $type_short; my $actual_name = $1; my $type_long = $2;
  26220. $type_short =
  26221. lookup2(0,$type_long,\@map_full_type_to_short_type_maps);
  26222. ll(4) && do_log(4, "File-type of %s: %s%s",
  26223. $part->base_name, $type_long,
  26224. (!defined $type_short ? ''
  26225. : !ref $type_short ? "; ($type_short)"
  26226. : '; (' . join(', ',@$type_short) . ')'
  26227. ) );
  26228. $part->type_long($type_long); $part->type_short($type_short);
  26229. $part->attributes_add('C') # simpleminded
  26230. if !ref($type_short) ? $type_short eq 'pgp' # encrypted?
  26231. : grep($_ eq 'pgp', @$type_short);
  26232. $index++;
  26233. }
  26234. }
  26235. }
  26236. defined $ln || $! == 0 || $! == EAGAIN
  26237. or die "Error reading from file(1) utility: $!";
  26238. do_log(-1,"unexpected(file): %s",$!) if !defined($ln) && $! == EAGAIN;
  26239. my $err = 0; $proc_fh->close or $err = $!;
  26240. my $child_stat = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  26241. undef $proc_fh; undef $pid; my(@errmsg);
  26242. # exit status is 1 when result is 'ERROR: ...', accept it mercifully
  26243. proc_status_ok($child_stat,$err, 0,1)
  26244. or push(@errmsg, "failed, ".exit_status_str($child_stat,$err));
  26245. if ($index < @part_list) {
  26246. push(@errmsg, sprintf("parsing failure - missing last %d results",
  26247. @part_list - $index));
  26248. }
  26249. !@errmsg or die join(", ",@errmsg);
  26250. # even though exit status 1 is accepted, log a warning nevertheless
  26251. proc_status_ok($child_stat,$err)
  26252. or do_log(-1, "file utility failed: %s",
  26253. exit_status_str($child_stat,$err));
  26254. }
  26255. 1;
  26256. } or do {
  26257. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  26258. kill_proc($pid,$file,1,$proc_fh,$eval_stat) if defined $pid;
  26259. };
  26260. chdir($tempdir) or die "Can't chdir to $tempdir: $!";
  26261. section_time(sprintf('get-file-type%d', $initial_num_parts));
  26262. if (defined $eval_stat) {
  26263. do_log(-2, "file(1) utility (%s) FAILED: %s", $file,$eval_stat);
  26264. # die "file(1) utility ($file) error: $eval_stat";
  26265. }
  26266. }
  26267. sub decompose_mail($$) {
  26268. my($tempdir,$file_generator_object) = @_;
  26269. my $hold; my(@parts); my $depth = 1; my $any_undecipherable = 0;
  26270. my $which_section = "parts_decode";
  26271. # fetch all not-yet-visited part names, and start a new cycle
  26272. TIER:
  26273. while (@parts = @{$file_generator_object->parts_list}) {
  26274. if ($MAXLEVELS > 0 && $depth > $MAXLEVELS) {
  26275. $hold = "Maximum decoding depth ($MAXLEVELS) exceeded";
  26276. last;
  26277. }
  26278. $file_generator_object->parts_list_reset; # new cycle of names
  26279. # clip to avoid very long log entries
  26280. my(@chopped_parts) = @parts > 5 ? @parts[0..4] : @parts;
  26281. ll(4) && do_log(4,"decode_parts: level=%d, #parts=%d : %s",
  26282. $depth, scalar(@parts),
  26283. join(', ', (map($_->base_name, @chopped_parts)),
  26284. (@chopped_parts >= @parts ? () : "...")) );
  26285. for my $part (@parts) { # test for existence of all expected files
  26286. my $fname = $part->full_name; my $errn = 0;
  26287. if ($fname eq '') { $errn = ENOENT }
  26288. else {
  26289. my(@stat_list) = lstat($fname);
  26290. if (@stat_list) { add_entropy(@stat_list) } else { $errn = 0+$! }
  26291. }
  26292. if ($errn == ENOENT) {
  26293. $part->exists(0);
  26294. # $part->type_short('no-file') if !defined $part->type_short;
  26295. } elsif ($errn) {
  26296. die "decompose_mail: inaccessible file $fname: $!";
  26297. } elsif (!-f _) { # not a regular file
  26298. my $what = -l _ ? 'symlink' : -d _ ? 'directory' : 'non-regular file';
  26299. do_log(-1, "WARN: decompose_mail: removing unexpected %s %s",
  26300. $what,$fname);
  26301. if (-d _) { rmdir_recursively($fname) }
  26302. else { unlink($fname) or die "Can't delete $what $fname: $!" }
  26303. $part->exists(0);
  26304. $part->type_short(-l _ ? 'symlink' : -d _ ? 'dir' : 'special')
  26305. if !defined $part->type_short;
  26306. } elsif (-z _) { # empty file
  26307. unlink($fname) or die "Can't remove \"$fname\": $!";
  26308. $part->exists(0);
  26309. $part->type_short('empty') if !defined $part->type_short;
  26310. $part->type_long('empty') if !defined $part->type_long;
  26311. } else {
  26312. $part->exists(1);
  26313. }
  26314. }
  26315. if (!defined $file || $file eq '') {
  26316. do_log(5,'utility file(1) not available, skipping determine_file_types');
  26317. } else {
  26318. determine_file_types($tempdir, \@parts);
  26319. }
  26320. for my $part (@parts) {
  26321. if ($part->exists && !defined($hold))
  26322. { $hold = decompose_part($part, $tempdir) }
  26323. $any_undecipherable++ if grep($_ eq 'U', @{ $part->attributes || [] });
  26324. }
  26325. last TIER if defined $hold;
  26326. $depth++;
  26327. }
  26328. section_time($which_section); prolong_timer($which_section);
  26329. ($hold, $any_undecipherable);
  26330. }
  26331. # Decompose one part
  26332. #
  26333. sub decompose_part($$) {
  26334. my($part, $tempdir) = @_;
  26335. # possible return values from eval:
  26336. # 0 - truly atomic or unknown or archiver failure; consider atomic
  26337. # 1 - some archive, successfully unpacked, result replaces original
  26338. # 2 - probably unpacked, but keep the original (eg self-extracting archive)
  26339. my $hold; my $eval_stat; my $sts = 0; my $any_called = 0;
  26340. eval {
  26341. my $type_short = $part->type_short;
  26342. my(@ts) = !defined $type_short ? ()
  26343. : !ref $type_short ? ($type_short) : @$type_short;
  26344. if (@ts) { # when one or more short types are known
  26345. snmp_count("OpsDecType-".join('.',@ts));
  26346. for my $dec_tuple (@{ca('decoders')}) { # first matching decoder wins
  26347. next if !defined $dec_tuple;
  26348. my($short_types, $code, @args) = @$dec_tuple;
  26349. if ($code && grep(ref $short_types ? $short_types->{$_}
  26350. : $_ eq $short_types, @ts)) {
  26351. $any_called = 1; $sts = &$code($part,$tempdir,@args);
  26352. last;
  26353. }
  26354. }
  26355. }
  26356. 1;
  26357. } or do {
  26358. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  26359. my $ll = -1;
  26360. if ($eval_stat =~ /\bExceeded storage quota\b.*\bbytes by/ ||
  26361. $eval_stat =~ /\bMaximum number of files\b.*\bexceeded/) {
  26362. $hold = $eval_stat; $ll = 1;
  26363. }
  26364. do_log($ll,"Decoding of %s (%s) failed, leaving it unpacked: %s",
  26365. $part->base_name, $part->type_long, $eval_stat);
  26366. $sts = 2; # keep the original, along with possible decoded files
  26367. };
  26368. if ($any_called) {
  26369. chdir($tempdir) or die "Can't chdir to $tempdir: $!"; # just in case
  26370. }
  26371. if ($sts == 1 && lookup2(0,$part->type_long,\@keep_decoded_original_maps)) {
  26372. # don't trust this file type or unpacker,
  26373. # keep both the original and the unpacked file
  26374. ll(4) && do_log(4,"file type is %s, retain original %s",
  26375. $part->type_long, $part->base_name);
  26376. $sts = 2; # keep the original, along with possible decoded files
  26377. }
  26378. if ($sts == 1) {
  26379. ll(5) && do_log(5,"decompose_part: deleting %s", $part->full_name);
  26380. unlink($part->full_name)
  26381. or die sprintf("Can't unlink %s: %s", $part->full_name, $!);
  26382. }
  26383. ll(4) && do_log(4,"decompose_part: %s - %s", $part->base_name,
  26384. ['atomic','archive, unpacked','source retained']->[$sts]);
  26385. section_time('decompose_part') if $any_called;
  26386. die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout
  26387. $hold;
  26388. }
  26389. # a trivial wrapper around mime_decode() to adjust arguments and result
  26390. #
  26391. sub do_mime_decode($$) {
  26392. my($part, $tempdir) = @_;
  26393. mime_decode($part,$tempdir,$part);
  26394. 2; # probably unpacked, but keep the original mail
  26395. };
  26396. #
  26397. # Uncompression/unarchiving routines
  26398. # Possible return codes:
  26399. # 0 - truly atomic or unknown or archiver failure; consider atomic
  26400. # 1 - some archiver format, successfully unpacked, result replaces original
  26401. # 2 - probably unpacked, but keep the original (eg self-extracting archive)
  26402. # if ASCII text, try multiple decoding methods as provided by UUlib
  26403. # (uuencoded, xxencoded, BinHex, yEnc, Base64, Quoted-Printable)
  26404. #
  26405. sub do_ascii($$) {
  26406. my($part, $tempdir) = @_;
  26407. ll(4) && do_log(4,"do_ascii: Decoding part %s", $part->base_name);
  26408. snmp_count('OpsDecByUUlibAttempt');
  26409. # prevent uunconc.c/UUDecode() from trying to create temp file in '/'
  26410. my $old_env_tmpdir = $ENV{TMPDIR}; $ENV{TMPDIR} = "$tempdir/parts";
  26411. my $any_errors = 0; my $any_decoded = 0;
  26412. alarm(0); # stop the timer
  26413. local($SIG{ALRM}); my($sigset,$action,$oldaction);
  26414. if ($] < 5.008) { # in old Perl signals could be delivered at any time
  26415. $SIG{ALRM} = sub { die "timed out\n" };
  26416. } elsif ($] < 5.008001) { # Perl 5.8.0
  26417. # 5.8.0 does not have POSIX::SigAction::safe but uses safe signals, which
  26418. # means a runaway uulib can't be aborted; tough luck, upgrade your Perl!
  26419. $SIG{ALRM} = sub { die "timed out\n" }; # old way, but won't abort
  26420. } else { # Perl >= 5.8.0 has 'safe signals', and SigAction::safe available
  26421. # POSIX::sigaction can bypass safe Perl signals on request;
  26422. # alternatively, use Perl module Sys::SigAction
  26423. $sigset = POSIX::SigSet->new(SIGALRM); $oldaction = POSIX::SigAction->new;
  26424. $action = POSIX::SigAction->new(sub { die "timed out\n" },
  26425. $sigset, &POSIX::SA_RESETHAND);
  26426. $action->safe(1);
  26427. POSIX::sigaction(SIGALRM,$action,$oldaction)
  26428. or die "Can't set ALRM handler: $!";
  26429. do_log(4,"do_ascii: Setting sigaction handler, was %d", $oldaction->safe);
  26430. }
  26431. my $eval_stat;
  26432. eval { # must not go away without calling Convert::UUlib::CleanUp !
  26433. my($sts,$count);
  26434. prolong_timer('do_ascii_pre'); # restart timer
  26435. $sts = Convert::UUlib::Initialize();
  26436. $sts = 0 if !defined $sts; # avoid Use of uninit. value in numeric eq (==)
  26437. $sts==RET_OK or die "Convert::UUlib::Initialize failed: ".
  26438. Convert::UUlib::strerror($sts);
  26439. my $uulib_version = Convert::UUlib::GetOption(OPT_VERSION);
  26440. !Convert::UUlib::SetOption(OPT_IGNMODE,1) or die "bad uulib OPT_IGNMODE";
  26441. # !Convert::UUlib::SetOption(OPT_DESPERATE,1) or die "bad uulib OPT_DESPERATE";
  26442. if (defined $action) {
  26443. $action->safe(0); # bypass safe Perl signals
  26444. POSIX::sigaction(SIGALRM,$action) or die "Can't set ALRM handler: $!";
  26445. }
  26446. # may take looong time on malformed messages, allow it to be interrupted
  26447. ($sts, $count) = Convert::UUlib::LoadFile($part->full_name);
  26448. if (defined $action) {
  26449. $action->safe(1); # re-establish safe signal handling
  26450. POSIX::sigaction(SIGALRM,$action) or die "Can't set ALRM handler: $!";
  26451. }
  26452. if ($sts != RET_OK) {
  26453. my $errmsg = Convert::UUlib::strerror($sts) . ": $!";
  26454. $errmsg .= ", (???"
  26455. . Convert::UUlib::strerror(Convert::UUlib::GetOption(OPT_ERRNO))."???)"
  26456. if $sts == RET_IOERR;
  26457. die "Convert::UUlib::LoadFile (uulib V$uulib_version) failed: $errmsg";
  26458. }
  26459. ll(4) && do_log(4,"do_ascii: Decoding part %s (%d items), uulib V%s",
  26460. $part->base_name, $count, $uulib_version);
  26461. my $uu;
  26462. my $item_num = 0; my $parent_placement = $part->mime_placement;
  26463. for (my $j = 0; $uu = Convert::UUlib::GetFileListItem($j); $j++) {
  26464. $item_num++;
  26465. ll(4) && do_log(4,
  26466. "do_ascii(%d): state=0x%02x, enc=%s%s, est.size=%s, name=%s",
  26467. $j, $uu->state, Convert::UUlib::strencoding($uu->uudet),
  26468. ($uu->mimetype ne '' ? ", mimetype=" . $uu->mimetype : ''),
  26469. $uu->size, $uu->filename);
  26470. if (!($uu->state & FILE_OK)) {
  26471. $any_errors = 1;
  26472. do_log(1,"do_ascii: Convert::UUlib info: %s not decodable, %s",
  26473. $j,$uu->state);
  26474. } else {
  26475. my $newpart_obj = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  26476. $newpart_obj->mime_placement("$parent_placement/$item_num");
  26477. $newpart_obj->name_declared($uu->filename);
  26478. my $newpart = $newpart_obj->full_name;
  26479. if (defined $action) {
  26480. $action->safe(0); # bypass safe Perl signals
  26481. POSIX::sigaction(SIGALRM,$action) or die "Can't set ALRM handlr: $!";
  26482. }
  26483. $! = 0;
  26484. $sts = $uu->decode($newpart); # decode to file $newpart
  26485. my $err_decode = "$!";
  26486. if (defined $action) {
  26487. $action->safe(1); # re-establish safe signal handling
  26488. POSIX::sigaction(SIGALRM,$action) or die "Can't set ALRM handlr: $!";
  26489. }
  26490. chmod(0750, $newpart) or $! == ENOENT # chmod, don't panic if no file
  26491. or die "Can't change protection of \"$newpart\": $!";
  26492. my $statmsg;
  26493. my $errn = lstat($newpart) ? 0 : 0+$!;
  26494. if ($errn == ENOENT) { $statmsg = "does not exist" }
  26495. elsif ($errn) { $statmsg = "inaccessible: $!" }
  26496. elsif ( -l _) { $statmsg = "is a symlink" }
  26497. elsif ( -d _) { $statmsg = "is a directory" }
  26498. elsif (!-f _) { $statmsg = "not a regular file" }
  26499. if (defined $statmsg) { $statmsg = "; file status: $newpart $statmsg" }
  26500. my $size = 0 + (-s _);
  26501. $newpart_obj->size($size);
  26502. consumed_bytes($size, 'do_ascii');
  26503. if ($sts == RET_OK && $errn==0) {
  26504. $any_decoded = 1;
  26505. do_log(4,"do_ascii: RET_OK%s", $statmsg) if defined $statmsg;
  26506. } elsif ($sts == RET_NODATA || $sts == RET_NOEND) {
  26507. $any_errors = 1;
  26508. do_log(-1,"do_ascii: Convert::UUlib error: %s%s",
  26509. Convert::UUlib::strerror($sts), $statmsg);
  26510. } else {
  26511. $any_errors = 1;
  26512. my $errmsg = Convert::UUlib::strerror($sts) . ":: $err_decode";
  26513. $errmsg .= ", " . Convert::UUlib::strerror(
  26514. Convert::UUlib::GetOption(OPT_ERRNO) ) if $sts == RET_IOERR;
  26515. die("Convert::UUlib failed: " . $errmsg . $statmsg);
  26516. }
  26517. }
  26518. }
  26519. 1;
  26520. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  26521. prolong_timer('do_ascii'); # restart timer
  26522. if (defined $oldaction) {
  26523. POSIX::sigaction(SIGALRM,$oldaction)
  26524. or die "Can't restore ALRM handler: $!";
  26525. }
  26526. Convert::UUlib::CleanUp();
  26527. snmp_count('OpsDecByUUlib') if $any_decoded;
  26528. if (defined $old_env_tmpdir) { $ENV{TMPDIR} = $old_env_tmpdir }
  26529. else { delete $ENV{TMPDIR} }
  26530. if (defined $eval_stat) { chomp $eval_stat; die "do_ascii: $eval_stat\n" }
  26531. $any_errors ? 2 : $any_decoded ? 1 : 0;
  26532. }
  26533. # use Archive-Zip
  26534. #
  26535. sub do_unzip($$;$$) {
  26536. my($part, $tempdir, $archiver_dummy, $testing_for_sfx) = @_;
  26537. ll(4) && do_log(4, "Unzipping %s", $part->base_name);
  26538. # avoid DoS vulnerability in < 2.017, CVE-2009-1391
  26539. # Compress::Raw::Zlib->VERSION(2.017); # module not loaded
  26540. snmp_count('OpsDecByArZipAttempt');
  26541. my $zip = Archive::Zip->new;
  26542. my(@err_nm) = qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR);
  26543. my $retval = 1;
  26544. # need to set up a temporary minimal error handler
  26545. Archive::Zip::setErrorHandler(sub { return 5 });
  26546. my $sts = $zip->read($part->full_name);
  26547. Archive::Zip::setErrorHandler(sub { die @_ });
  26548. my($any_unsupp_compmeth,$any_zero_length);
  26549. my($encryptedcount,$extractedcount) = (0,0);
  26550. if ($sts != AZ_OK) { # not a zip? corrupted zip file? other errors?
  26551. if ($testing_for_sfx && $sts == AZ_FORMAT_ERROR) {
  26552. # a normal status for executable that is not a self extracting archive
  26553. do_log(4, "do_unzip: ok, exe is not a zip sfx: %s (%s)",
  26554. $err_nm[$sts], $sts);
  26555. } else {
  26556. do_log(-1, "do_unzip: not a zip: %s (%s)", $err_nm[$sts], $sts);
  26557. # $part->attributes_add('U'); # perhaps not, it flags as **UNCHECKED** too
  26558. # # many bounces containing chopped-off zip
  26559. }
  26560. $retval = 0;
  26561. } else {
  26562. my $item_num = 0; my $parent_placement = $part->mime_placement;
  26563. for my $mem ($zip->members) {
  26564. my $newpart_obj = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  26565. $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
  26566. $newpart_obj->name_declared($mem->fileName);
  26567. my $compmeth = $mem->compressionMethod;
  26568. if ($compmeth!=COMPRESSION_DEFLATED && $compmeth!=COMPRESSION_STORED) {
  26569. $any_unsupp_compmeth = $compmeth;
  26570. $newpart_obj->attributes_add('U');
  26571. } elsif ($mem->isEncrypted) {
  26572. $encryptedcount++;
  26573. $newpart_obj->attributes_add('U','C');
  26574. } elsif ($mem->isDirectory) {
  26575. $newpart_obj->attributes_add('D');
  26576. } else {
  26577. # want to read uncompressed - set to COMPRESSION_STORED
  26578. my $oldc = $mem->desiredCompressionMethod(COMPRESSION_STORED);
  26579. $sts = $mem->rewindData;
  26580. $sts == AZ_OK or die sprintf("%s: error rew. member data: %s (%s)",
  26581. $part->base_name, $err_nm[$sts], $sts);
  26582. my $newpart = $newpart_obj->full_name;
  26583. my $outpart = IO::File->new;
  26584. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  26585. $outpart->open($newpart, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640)
  26586. or die "Can't create file $newpart: $!";
  26587. binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  26588. my $size = 0;
  26589. while ($sts == AZ_OK) {
  26590. my $buf_ref;
  26591. ($buf_ref, $sts) = $mem->readChunk;
  26592. $sts == AZ_OK || $sts == AZ_STREAM_END
  26593. or die sprintf("%s: error reading member: %s (%s)",
  26594. $part->base_name, $err_nm[$sts], $sts);
  26595. my $buf_len = length($$buf_ref);
  26596. if ($buf_len > 0) {
  26597. $size += $buf_len;
  26598. $outpart->print($$buf_ref) or die "Can't write to $newpart: $!";
  26599. consumed_bytes($buf_len, 'do_unzip');
  26600. }
  26601. }
  26602. $any_zero_length = 1 if $size == 0;
  26603. $newpart_obj->size($size);
  26604. $outpart->close or die "Error closing $newpart: $!";
  26605. $mem->desiredCompressionMethod($oldc);
  26606. $mem->endRead;
  26607. $extractedcount++;
  26608. }
  26609. }
  26610. snmp_count('OpsDecByArZip');
  26611. }
  26612. if ($any_unsupp_compmeth) {
  26613. $retval = 2;
  26614. do_log(-1, "do_unzip: %s, unsupported compression method: %s",
  26615. $part->base_name, $any_unsupp_compmeth);
  26616. } elsif ($any_zero_length) { # possible zip vulnerability exploit
  26617. $retval = 2;
  26618. do_log(1, "do_unzip: %s, members of zero length, archive retained",
  26619. $part->base_name);
  26620. } elsif ($encryptedcount) {
  26621. $retval = 2;
  26622. do_log(1,
  26623. "do_unzip: %s, %d members are encrypted, %s extracted, archive retained",
  26624. $part->base_name, $encryptedcount,
  26625. !$extractedcount ? 'none' : $extractedcount);
  26626. }
  26627. $retval;
  26628. }
  26629. # use external decompressor program from the compress/gzip/bzip2/xz family
  26630. #
  26631. sub do_uncompress($$$) {
  26632. my($part, $tempdir, $decompressor) = @_;
  26633. ll(4) && do_log(4,"do_uncompress %s by %s", $part->base_name,$decompressor);
  26634. my $decompressor_name = basename((split(' ',$decompressor))[0]);
  26635. snmp_count("OpsDecBy\u${decompressor_name}");
  26636. my $newpart_obj = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  26637. $newpart_obj->mime_placement($part->mime_placement."/1");
  26638. my $newpart = $newpart_obj->full_name;
  26639. my($type_short, $name_declared) = ($part->type_short, $part->name_declared);
  26640. local($1); my(@rn); # collect recommended file names
  26641. push(@rn,$1)
  26642. if $part->type_long =~ /^\S+\s+compressed data, was "(.+)"(\z|, from\b)/;
  26643. for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
  26644. next if $name_d eq '';
  26645. my $name = $name_d;
  26646. for (!ref $type_short ? ($type_short) : @$type_short) {
  26647. $_ eq 'F' and $name=~s/\.F\z//;
  26648. $_ eq 'Z' and $name=~s/\.Z\z// || $name=~s/\.tg?z\z/.tar/;
  26649. $_ eq 'gz' and $name=~s/\.gz\z// || $name=~s/\.tgz\z/.tar/;
  26650. $_ eq 'bz' and $name=~s/\.bz\z// || $name=~s/\.tbz\z/.tar/;
  26651. $_ eq 'bz2' and $name=~s/\.bz2?\z// || $name=~s/\.tbz2?\z/.tar/;
  26652. $_ eq 'xz' and $name=~s/\.xz\z// || $name=~s/\.txz\z/.tar/;
  26653. $_ eq 'lzma' and $name=~s/\.lzma\z// || $name=~s/\.tlz\z/.tar/;
  26654. $_ eq 'lrz' and $name=~s/\.lrz\z//;
  26655. $_ eq 'lzo' and $name=~s/\.lzo\z//;
  26656. $_ eq 'rpm' and $name=~s/\.rpm\z/.cpio/;
  26657. }
  26658. push(@rn,$name) if !grep($_ eq $name, @rn);
  26659. }
  26660. $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
  26661. my($proc_fh,$pid); my $retval = 1;
  26662. prolong_timer('do_uncompress_pre'); # restart timer
  26663. my $eval_stat;
  26664. eval {
  26665. ($proc_fh,$pid) =
  26666. run_command($part->full_name, '/dev/null', split(' ',$decompressor));
  26667. my($rv,$err) = run_command_copy($newpart,$proc_fh,$pid); # may die
  26668. undef $proc_fh; undef $pid;
  26669. if (!proc_status_ok($rv,$err)) {
  26670. # unlink($newpart) or die "Can't unlink $newpart: $!";
  26671. my $msg = sprintf('Error running decompressor %s on %s, %s',
  26672. $decompressor, $part->base_name, exit_status_str($rv,$err));
  26673. # bzip2 and gzip use status 2 as a warning about corrupted file
  26674. if (proc_status_ok($rv,$err, 2)) {do_log(0,"%s",$msg)} else {die $msg}
  26675. }
  26676. 1;
  26677. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  26678. prolong_timer('do_uncompress'); # restart timer
  26679. if (defined $eval_stat) {
  26680. $retval = 0; chomp $eval_stat;
  26681. kill_proc($pid,$decompressor,1,$proc_fh,$eval_stat) if defined $pid;
  26682. undef $proc_fh; undef $pid;
  26683. die "do_uncompress: $eval_stat\n"; # propagate failure
  26684. }
  26685. $retval;
  26686. }
  26687. # use Compress::Zlib to inflate
  26688. #
  26689. sub do_gunzip($$) {
  26690. my($part, $tempdir) = @_; my $retval = 0;
  26691. do_log(4, "Inflating gzip archive %s", $part->base_name);
  26692. snmp_count('OpsDecByZlib');
  26693. my $gz = Amavis::IO::Zlib->new;
  26694. $gz->open($part->full_name,'rb')
  26695. or die("do_gunzip: Can't open gzip file ".$part->full_name.": $!");
  26696. my $newpart_obj = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  26697. $newpart_obj->mime_placement($part->mime_placement."/1");
  26698. my $newpart = $newpart_obj->full_name;
  26699. my $outpart = IO::File->new;
  26700. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  26701. $outpart->open($newpart, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640)
  26702. or die "Can't create file $newpart: $!";
  26703. binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  26704. my($nbytes,$buff); my $size = 0;
  26705. while (($nbytes=$gz->read($buff,16384)) > 0) {
  26706. $outpart->print($buff) or die "Can't write to $newpart: $!";
  26707. $size += $nbytes; consumed_bytes($nbytes, 'do_gunzip');
  26708. }
  26709. my $err = defined $nbytes ? 0 : $!;
  26710. $newpart_obj->size($size);
  26711. $outpart->close or die "Error closing $newpart: $!";
  26712. undef $buff; # release storage
  26713. my(@rn); # collect recommended file name
  26714. my $name_declared = $part->name_declared;
  26715. for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
  26716. next if $name_d eq '';
  26717. my $name = $name_d;
  26718. $name=~s/\.(gz|Z)\z// || $name=~s/\.tgz\z/.tar/;
  26719. push(@rn,$name) if !grep($_ eq $name, @rn);
  26720. }
  26721. $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
  26722. if (defined $nbytes && $nbytes==0) { $retval = 1 } # success
  26723. else {
  26724. do_log(-1, "do_gunzip: Error reading file %s: %s", $part->full_name,$err);
  26725. unlink($newpart) or die "Can't unlink $newpart: $!";
  26726. $newpart_obj->size(undef); $retval = 0;
  26727. }
  26728. $gz->close or die "Error closing gzipped file: $!";
  26729. $retval;
  26730. }
  26731. # DROPED SUPPORT for Archive::Tar; main drawback of this module is: it either
  26732. # loads an entire tar into memory (horrors!), or when using extract_archive()
  26733. # it does not relativize absolute paths (which makes it possible to store
  26734. # members in any directory writable by uid), and does not provide a way to
  26735. # capture contents of members with the same name. Use pax program instead!
  26736. #
  26737. #use Archive::Tar;
  26738. #sub do_tar($$) {
  26739. # my($part, $tempdir) = @_;
  26740. # snmp_count('OpsDecByArTar');
  26741. # # Work around bug in Archive-Tar
  26742. # my $tar = eval { Archive::Tar->new($part->full_name) };
  26743. # if (!defined($tar)) {
  26744. # chomp $@;
  26745. # do_log(4, "Faulty archive %s: %s", $part->full_name, $@);
  26746. # die $@ if $@ =~ /^timed out\b/; # resignal timeout
  26747. # return 0;
  26748. # }
  26749. # do_log(4,"Untarring %s", $part->base_name);
  26750. # my $item_num = 0; my $parent_placement = $part->mime_placement;
  26751. # my(@list) = $tar->list_files;
  26752. # for (@list) {
  26753. # next if m{/\z}; # ignore directories
  26754. # # this is bad (reads whole file into scalar)
  26755. # # need some error handling, too
  26756. # my $data = $tar->get_content($_);
  26757. # my $newpart_obj = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  26758. # $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
  26759. # my $newpart = $newpart_obj->full_name;
  26760. # my $outpart = IO::File->new;
  26761. # # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  26762. # $outpart->open($newpart, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640)
  26763. # or die "Can't create file $newpart: $!";
  26764. # binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  26765. # $outpart->print($data) or die "Can't write to $newpart: $!";
  26766. # $newpart_obj->size(length($data));
  26767. # consumed_bytes(length($data), 'do_tar');
  26768. # $outpart->close or die "Error closing $newpart: $!";
  26769. # }
  26770. # 1;
  26771. #}
  26772. # use external program to expand 7-Zip archives
  26773. #
  26774. sub do_7zip($$$;$) {
  26775. my($part, $tempdir, $archiver, $testing_for_sfx) = @_;
  26776. ll(4) && do_log(4, "Expanding 7-Zip archive %s", $part->base_name);
  26777. my $decompressor_name = basename((split(' ',$archiver))[0]);
  26778. snmp_count("OpsDecBy\u${decompressor_name}Attempt");
  26779. my $last_line; my $bytes = 0; my $mem_cnt = 0;
  26780. my $retval = 1; my($proc_fh,$pid); my $fn = $part->full_name;
  26781. prolong_timer('do_7zip_pre'); # restart timer
  26782. my $eval_stat;
  26783. eval {
  26784. ($proc_fh,$pid) = run_command(undef, '&1', $archiver,
  26785. 'l', '-slt', "-w$tempdir/parts", '--', $fn);
  26786. my $ln; my($name,$size,$attr); my $entries_cnt = 0;
  26787. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  26788. $last_line = $ln if $ln !~ /^\s*$/; # keep last nonempty line
  26789. chomp($ln); local($1);
  26790. if ($ln =~ /^\s*\z/) {
  26791. if (defined $name || defined $size) {
  26792. do_log(5,'do_7zip: member: %s "%s", %s bytes', $attr,$name,$size);
  26793. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  26794. { die "Maximum number of files ($MAXFILES) exceeded" }
  26795. if (defined $size && $size > 0) { $bytes += $size; $mem_cnt++ }
  26796. }
  26797. undef $name; undef $size; undef $attr;
  26798. } elsif ($ln =~ /^Path = (.*)\z/s) { $name = $1 }
  26799. elsif ($ln =~ /^Size = ([0-9]+)\z/s) { $size = $1 }
  26800. elsif ($ln =~ /^Attributes = (.*)\z/s) { $attr = $1 }
  26801. }
  26802. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading (1): $!";
  26803. do_log(-1,"unexpected(do_7zip_1): %s",$!) if !defined($ln) && $! == EAGAIN;
  26804. if (defined $name || defined $size) {
  26805. do_log(5,'do_7zip: member: %s "%s", %s bytes', $attr,$name,$size);
  26806. if (defined $size && $size > 0) { $bytes += $size; $mem_cnt++ }
  26807. }
  26808. # consume all remaining output to avoid broken pipe
  26809. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0)
  26810. { $last_line = $ln if $ln !~ /^\s*$/ }
  26811. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading (2): $!";
  26812. do_log(-1,"unexpected(do_7zip_2): %s",$!) if !defined($ln) && $! == EAGAIN;
  26813. my $err = 0; $proc_fh->close or $err = $!;
  26814. my $rv = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  26815. undef $proc_fh; undef $pid; local($1,$2);
  26816. if (proc_status_ok($rv,$err,1) && $mem_cnt > 0 && $bytes > 0) { # just warn
  26817. do_log(4,"do_7zip: warning, %s", exit_status_str($rv,$err));
  26818. } elsif (!proc_status_ok($rv,$err)) {
  26819. die sprintf("can't get a list of archive members: %s; %s",
  26820. exit_status_str($rv,$err), $last_line);
  26821. }
  26822. if ($mem_cnt > 0 || $bytes > 0) {
  26823. consumed_bytes($bytes, 'do_7zip-pre', 1); # pre-check on estimated size
  26824. snmp_count("OpsDecBy\u${decompressor_name}");
  26825. ($proc_fh,$pid) = run_command(undef, '&1', $archiver, 'x', '-bd', '-y',
  26826. "-w$tempdir/parts", "-o$tempdir/parts/7zip", '--', $fn);
  26827. collect_results($proc_fh,$pid,$archiver,16384,[0,1]);
  26828. undef $proc_fh; undef $pid;
  26829. my $errn = lstat("$tempdir/parts/7zip") ? 0 : 0+$!;
  26830. if ($errn != ENOENT) {
  26831. my $b = flatten_and_tidy_dir("$tempdir/parts/7zip",
  26832. "$tempdir/parts", $part);
  26833. consumed_bytes($b, 'do_7zip');
  26834. }
  26835. }
  26836. 1;
  26837. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  26838. prolong_timer('do_7zip'); # restart timer
  26839. if (defined $eval_stat) {
  26840. $retval = 0; chomp $eval_stat;
  26841. kill_proc($pid,$archiver,1,$proc_fh,$eval_stat) if defined $pid;
  26842. undef $proc_fh; undef $pid;
  26843. # if ($testing_for_sfx) { die "do_7zip: $eval_stat" }
  26844. # else { do_log(-1, "do_7zip: %s", $eval_stat) };
  26845. die "do_7zip: $eval_stat\n" # propagate failure
  26846. }
  26847. $retval;
  26848. }
  26849. # use external program to expand RAR archives
  26850. #
  26851. sub do_unrar($$$;$) {
  26852. my($part, $tempdir, $archiver, $testing_for_sfx) = @_;
  26853. ll(4) && do_log(4, "Expanding RAR archive %s", $part->base_name);
  26854. my $decompressor_name = basename((split(' ',$archiver))[0]);
  26855. snmp_count("OpsDecBy\u${decompressor_name}Attempt");
  26856. # unrar exit codes: SUCCESS=0, WARNING=1, FATAL_ERROR=2, CRC_ERROR=3,
  26857. # LOCK_ERROR=4, WRITE_ERROR=5, OPEN_ERROR=6, USER_ERROR=7, MEMORY_ERROR=8,
  26858. # CREATE_ERROR=9, USER_BREAK=255
  26859. my(@list); my $hypcount = 0; my $encryptedcount = 0;
  26860. my $lcnt = 0; my $member_name; my $bytes = 0; my $last_line;
  26861. my $item_num = 0; my $parent_placement = $part->mime_placement;
  26862. my $retval = 1; my $fn = $part->full_name; my($proc_fh,$pid);
  26863. my(@common_rar_switches) = qw(-c- -p- -idcdp); # -av-
  26864. prolong_timer('do_unrar_pre'); # restart timer
  26865. my $eval_stat;
  26866. eval {
  26867. ($proc_fh,$pid) =
  26868. run_command(undef, '&1', $archiver, 'v',@common_rar_switches,'--',$fn);
  26869. # jump hoops because there is no simple way to just list all the files
  26870. my $ln; my $entries_cnt = 0;
  26871. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  26872. $last_line = $ln if $ln !~ /^\s*$/; # keep last nonempty line
  26873. chomp;
  26874. if ($ln =~ /^unexpected end of archive/) {
  26875. last;
  26876. } elsif ($ln =~ /^------/) {
  26877. $hypcount++;
  26878. last if $hypcount >= 2;
  26879. } elsif ($hypcount < 1 && $ln =~ /^Encrypted file:/) {
  26880. do_log(4,"do_unrar: %s", $ln);
  26881. $part->attributes_add('U','C');
  26882. } elsif ($hypcount == 1) {
  26883. $lcnt++; local($1,$2,$3);
  26884. if ($lcnt % 2 == 0) { # information line (every other line)
  26885. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  26886. { die "Maximum number of files ($MAXFILES) exceeded" }
  26887. if ($ln !~ /^\s+(\d+)\s+(\d+)\s+(\d+%|-->|<--|<->)/) {
  26888. do_log($testing_for_sfx ? 4 : -1,
  26889. "do_unrar: can't parse info line for \"%s\" %s",
  26890. $member_name,$ln);
  26891. } elsif (defined $member_name) {
  26892. do_log(5,'do_unrar: member: "%s", size: %s', $member_name,$1);
  26893. if ($1 > 0) { $bytes += $1; push(@list, $member_name) }
  26894. }
  26895. undef $member_name;
  26896. } elsif ($ln =~ /^(.)(.*)\z/s) {
  26897. $member_name = $2; # all but the first character (space or '*')
  26898. if ($1 eq '*') { # member is encrypted
  26899. $encryptedcount++; $item_num++;
  26900. # make a phantom entry - carrying only name and attributes
  26901. my $newpart_obj =
  26902. Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  26903. $newpart_obj->mime_placement("$parent_placement/$item_num");
  26904. $newpart_obj->name_declared($member_name);
  26905. $newpart_obj->attributes_add('U','C');
  26906. undef $member_name; # makes no sense extracting encrypted files
  26907. }
  26908. }
  26909. }
  26910. }
  26911. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading (1): $!";
  26912. do_log(-1,"unexpected(unrar_1): %s",$!) if !defined($ln) && $! == EAGAIN;
  26913. $ln = undef; # consume all remaining output to avoid broken pipe
  26914. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0)
  26915. { $last_line = $ln if $ln !~ /^\s*$/ }
  26916. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading (2): $!";
  26917. do_log(-1,"unexpected(unrar_2): %s",$!) if !defined($ln) && $! == EAGAIN;
  26918. my $err = 0; $proc_fh->close or $err = $!;
  26919. my $rv = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  26920. undef $proc_fh; undef $pid; local($1,$2);
  26921. if (proc_status_ok($rv,$err, 7)) { # USER_ERROR
  26922. die printf("perhaps this %s does not recognize switches ".
  26923. "-av- and -idcdp, it is probably too old. Upgrade: %s",
  26924. $archiver, 'http://www.rarlab.com/');
  26925. } elsif (proc_status_ok($rv,$err, 3)) { # CRC_ERROR
  26926. # NOTE: password protected files in the archive cause CRC_ERROR
  26927. do_log(4,"do_unrar: CRC_ERROR - undecipherable, %s",
  26928. exit_status_str($rv,$err));
  26929. $part->attributes_add('U');
  26930. } elsif (proc_status_ok($rv,$err, 1) && @list && $bytes > 0) {
  26931. # WARNING, probably still ok
  26932. do_log(4,"do_unrar: warning, %s", exit_status_str($rv,$err));
  26933. } elsif (!proc_status_ok($rv,$err)) {
  26934. die("can't get a list of archive members: " .
  26935. exit_status_str($rv,$err) ."; ".$last_line);
  26936. } elsif (!$bytes && $last_line =~ /^\Q$fn\E is not RAR archive$/) {
  26937. chomp($last_line); die $last_line;
  26938. } elsif ($last_line !~ /^\s*(\d+)\s+(\d+)/s) {
  26939. do_log(-1,"do_unrar: unable to obtain orig total size: %s", $last_line);
  26940. } else {
  26941. do_log(4,"do_unrar: summary size: %d, sum of sizes: %d",
  26942. $2,$bytes) if abs($bytes - $2) > 100;
  26943. $bytes = $2 if $2 > $bytes;
  26944. }
  26945. consumed_bytes($bytes, 'do_unrar-pre', 1); # pre-check on estimated size
  26946. if (!@list) {
  26947. do_log(4,"do_unrar: no archive members, or not an archive at all");
  26948. if ($testing_for_sfx) { return 0 } else { $part->attributes_add('U') }
  26949. } else {
  26950. snmp_count("OpsDecBy\u${decompressor_name}");
  26951. # unrar/rar can make a dir by itself, but can't hurt (sparc64 problem?)
  26952. mkdir("$tempdir/parts/rar", 0750)
  26953. or die "Can't mkdir $tempdir/parts/rar: $!";
  26954. ($proc_fh,$pid) =
  26955. run_command(undef, '&1', $archiver, qw(x -inul -ver -o- -kb),
  26956. @common_rar_switches, '--', $fn, "$tempdir/parts/rar/");
  26957. collect_results($proc_fh,$pid,$archiver,16384,
  26958. [0,1,3] ); # one of: SUCCESS, WARNING, CRC
  26959. undef $proc_fh; undef $pid;
  26960. my $errn = lstat("$tempdir/parts/rar") ? 0 : 0+$!;
  26961. if ($errn != ENOENT) {
  26962. my $b = flatten_and_tidy_dir("$tempdir/parts/rar",
  26963. "$tempdir/parts", $part);
  26964. consumed_bytes($b, 'do_unrar');
  26965. }
  26966. }
  26967. 1;
  26968. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  26969. prolong_timer('do_unrar'); # restart timer
  26970. if ($encryptedcount) {
  26971. do_log(1,
  26972. "do_unrar: %s, %d members are encrypted, %s extracted, archive retained",
  26973. $part->base_name, $encryptedcount, !@list ? 'none' : scalar(@list) );
  26974. $retval = 2;
  26975. }
  26976. if (defined $eval_stat) {
  26977. $retval = 0; chomp $eval_stat;
  26978. kill_proc($pid,$archiver,1,$proc_fh,$eval_stat) if defined $pid;
  26979. undef $proc_fh; undef $pid;
  26980. # if ($testing_for_sfx) { die "do_unrar: $eval_stat" }
  26981. # else { do_log(-1, "do_unrar: %s", $eval_stat) };
  26982. die "do_unrar: $eval_stat\n" # propagate failure
  26983. }
  26984. $retval;
  26985. }
  26986. # use external program to expand LHA archives
  26987. #
  26988. sub do_lha($$$;$) {
  26989. my($part, $tempdir, $archiver, $testing_for_sfx) = @_;
  26990. ll(4) && do_log(4, "Expanding LHA archive %s", $part->base_name);
  26991. my $decompressor_name = basename((split(' ',$archiver))[0]);
  26992. snmp_count("OpsDecBy\u${decompressor_name}Attempt");
  26993. # lha needs extension .exe to understand SFX!
  26994. # the downside is that in this case it only sees MS files in an archive
  26995. my $fn = $part->full_name;
  26996. symlink($fn, $fn.".exe")
  26997. or die sprintf("Can't symlink %s %s.exe: %s", $fn, $fn, $!);
  26998. my(@list); my(@checkerr); my $retval = 1; my($proc_fh,$pid);
  26999. prolong_timer('do_lha_pre'); # restart timer
  27000. my $eval_stat;
  27001. eval {
  27002. # ($proc_fh,$pid) = run_command(undef, '&1', $archiver, 'lq', $fn);
  27003. ($proc_fh,$pid) = run_command(undef, '&1', $archiver, 'lq', $fn.".exe");
  27004. my $ln; my $entries_cnt = 0;
  27005. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  27006. chomp($ln); local($1);
  27007. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27008. { die "Maximum number of files ($MAXFILES) exceeded" }
  27009. if ($ln =~ m{/\z}) {
  27010. # ignore directories
  27011. } elsif ($ln =~ /^LHa: (Warning|Fatal error): /) {
  27012. push(@checkerr,$ln) if @checkerr < 3;
  27013. } elsif ($ln=~m{^(?:\S+\s+\d+/\d+|.{23})(?:\s+\S+){5}\s*(\S.*?)\s*\z}s) {
  27014. my $name = $1; $name = $1 if $name =~ m{^(.*) -> (.*)\z}s; # symlink
  27015. push(@list, $name);
  27016. } else { do_log(5,"do_lha: skip: %s", $ln) }
  27017. }
  27018. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading: $!";
  27019. do_log(-1,"unexpected(do_lha): %s",$!) if !defined($ln) && $! == EAGAIN;
  27020. my $err = 0; $proc_fh->close or $err = $!;
  27021. my $child_stat = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27022. undef $proc_fh; undef $pid;
  27023. if (!proc_status_ok($child_stat,$err) || @checkerr) {
  27024. die('(' . join(", ",@checkerr) .') ' .exit_status_str($child_stat,$err));
  27025. } elsif (!@list) {
  27026. $part->attributes_add('U') if !$testing_for_sfx;
  27027. die "no archive members, or not an archive at all";
  27028. }
  27029. 1;
  27030. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  27031. prolong_timer('do_lha'); # restart timer
  27032. if (defined $eval_stat) {
  27033. unlink($fn.".exe") or do_log(-1, "Can't unlink %s.exe: %s", $fn,$!);
  27034. $retval = 0; chomp $eval_stat;
  27035. kill_proc($pid,$archiver,1,$proc_fh,$eval_stat) if defined $pid;
  27036. undef $proc_fh; undef $pid;
  27037. # if ($testing_for_sfx) { die "do_lha: $eval_stat" }
  27038. # else { do_log(-1, "do_lha: %s", $eval_stat) };
  27039. die "do_lha: $eval_stat\n"; # propagate failure
  27040. } else { # preliminary archive traversal done, now extract files
  27041. snmp_count("OpsDecBy\u${decompressor_name}");
  27042. my $rv;
  27043. eval {
  27044. # store_mgr may die, make sure we unlink the .exe file
  27045. $rv = store_mgr($tempdir, $part, \@list, $archiver, 'pq', $fn.".exe");
  27046. 1;
  27047. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  27048. unlink($fn.".exe") or do_log(-1, "Can't unlink %s.exe: %s", $fn,$!);
  27049. if (defined $eval_stat) { die "do_lha: $eval_stat\n" } # propagate failure
  27050. $rv==0 or die exit_status_str($rv);
  27051. }
  27052. $retval;
  27053. }
  27054. # use external program to expand ARC archives;
  27055. # works with original arc, or a GPL licensed 'nomarch'
  27056. # (http://rus.members.beeb.net/nomarch.html)
  27057. #
  27058. sub do_arc($$$) {
  27059. my($part, $tempdir, $archiver) = @_;
  27060. my $decompressor_name = basename((split(' ',$archiver))[0]);
  27061. snmp_count("OpsDecBy\u${decompressor_name}");
  27062. my $is_nomarch = $archiver =~ /nomarch/i;
  27063. ll(4) && do_log(4,"Unarcing %s, using %s",
  27064. $part->base_name, ($is_nomarch ? "nomarch" : "arc") );
  27065. my $cmdargs = ($is_nomarch ? '-l -U' : 'ln') . ' ' . $part->full_name;
  27066. my($proc_fh,$pid) = run_command(undef, '/dev/null', $archiver,
  27067. split(' ',$cmdargs));
  27068. my(@list); my $ln; my $entries_cnt = 0;
  27069. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  27070. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27071. { die "Maximum number of files ($MAXFILES) exceeded" }
  27072. push(@list,$ln);
  27073. }
  27074. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading: $!";
  27075. do_log(-1,"unexpected(do_arc): %s",$!) if !defined($ln) && $! == EAGAIN;
  27076. my $err = 0; $proc_fh->close or $err = $!;
  27077. my $child_stat = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27078. undef $proc_fh; undef $pid;
  27079. proc_status_ok($child_stat,$err)
  27080. or do_log(-1, 'do_arc: %s',exit_status_str($child_stat,$err));
  27081. #*** no spaces in filenames allowed???
  27082. local($1); s/^([^ \t\r\n]*).*\z/$1/s for @list; # keep only filenames
  27083. if (@list) {
  27084. # store_mgr may die, allow failure to propagate
  27085. my $rv = store_mgr($tempdir, $part, \@list, $archiver,
  27086. ($is_nomarch ? ('-p', '-U') : 'p'), $part->full_name);
  27087. do_log(-1, 'arc %', exit_status_str($rv)) if $rv;
  27088. }
  27089. 1;
  27090. }
  27091. # use external program to expand ZOO archives
  27092. #
  27093. sub do_zoo($$$) {
  27094. my($part, $tempdir, $archiver) = @_;
  27095. my $is_unzoo = $archiver =~ m{\bunzoo[^/]*\z}i ? 1 : 0;
  27096. ll(4) && do_log(4,"Expanding ZOO archive %s, using %s",
  27097. $part->base_name, ($is_unzoo ? "unzoo" : "zoo") );
  27098. my $decompressor_name = basename((split(' ',$archiver))[0]);
  27099. snmp_count("OpsDecBy\u${decompressor_name}");
  27100. my(@list); my $separ_count = 0; my $bytes = 0; my($ln,$last_line);
  27101. my $retval = 1; my $fn = $part->full_name; my($proc_fh,$pid);
  27102. symlink($fn, "$fn.zoo") # Zoo needs extension of .zoo!
  27103. or die sprintf("Can't symlink %s %s.zoo: %s", $fn,$fn,$!);
  27104. prolong_timer('do_zoo_pre'); # restart timer
  27105. my $eval_stat; my $entries_cnt = 0;
  27106. eval {
  27107. ($proc_fh,$pid) = run_command(undef, '&1', $archiver,
  27108. $is_unzoo ? qw(-l) : qw(l), "$fn.zoo");
  27109. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  27110. $last_line = $ln if $ln !~ /^\s*$/; # keep last nonempty line
  27111. if ($ln =~ /^------/) { $separ_count++ }
  27112. elsif ($separ_count == 1) {
  27113. local($1,$2);
  27114. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27115. { die "Maximum number of files ($MAXFILES) exceeded" }
  27116. if ($ln !~ /^\s*(\d+)(?:\s+\S+){6}\s+(?:[0-7]{3,})?\s*(.*)$/) {
  27117. do_log(3,"do_zoo: can't parse line %s", $ln);
  27118. } else {
  27119. do_log(5,'do_zoo: member: "%s", size: %s', $2,$1);
  27120. if ($1 > 0) { $bytes += $1; push(@list,$2) }
  27121. }
  27122. }
  27123. }
  27124. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading: $!";
  27125. do_log(-1,"unexpected(do_zoo): %s",$!) if !defined($ln) && $! == EAGAIN;
  27126. my $err = 0; $proc_fh->close or $err = $!;
  27127. my $rv = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27128. undef $proc_fh; undef $pid; local($1);
  27129. if (!proc_status_ok($rv,$err)) {
  27130. die("can't get a list of archive members: " .
  27131. exit_status_str($rv,$err) ."; ".$last_line);
  27132. } elsif ($last_line !~ /^\s*(\d+)\s+\d+%\s+\d+/s) {
  27133. do_log(-1,"do_zoo: unable to obtain orig total size: %s", $last_line);
  27134. } else {
  27135. do_log(4,"do_zoo: summary size: %d, sum of sizes: %d",
  27136. $1,$bytes) if abs($bytes - $1) > 100;
  27137. $bytes = $1 if $1 > $bytes;
  27138. }
  27139. consumed_bytes($bytes, 'do_zoo-pre', 1); # pre-check on estimated size
  27140. $retval = 0 if @list;
  27141. if (!$is_unzoo) {
  27142. # unzoo cannot cleanly extract to stdout without prepending a clutter
  27143. # store_mgr may die
  27144. my $rv = store_mgr($tempdir,$part,\@list,$archiver,'xpqqq:',"$fn.zoo");
  27145. do_log(-1,"do_zoo (store_mgr) %s", exit_status_str($rv)) if $rv;
  27146. } else { # this code section can handle zoo and unzoo
  27147. # but zoo is unsafe in this mode (and so is unzoo, a little less so)
  27148. my $cwd = "$tempdir/parts/zoo";
  27149. mkdir($cwd, 0750) or die "Can't mkdir $cwd: $!";
  27150. chdir($cwd) or die "Can't chdir to $cwd: $!";
  27151. # don't use "-j ./" in unzoo, it does not protect from relative paths!
  27152. # "-j X" is less bad, but: "unzoo: 'X/h/user/01.lis' cannot be created"
  27153. ($proc_fh,$pid) =
  27154. run_command(undef, '&1', $archiver,
  27155. $is_unzoo ? qw(-x -j X) : qw(x),
  27156. "$fn.zoo", $is_unzoo ? '*;*' : () );
  27157. collect_results($proc_fh,$pid,$archiver,16384,[0]);
  27158. undef $proc_fh; undef $pid;
  27159. my $b = flatten_and_tidy_dir("$tempdir/parts/zoo",
  27160. "$tempdir/parts", $part);
  27161. consumed_bytes($b, 'do_zoo');
  27162. }
  27163. 1;
  27164. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  27165. prolong_timer('do_zoo'); # restart timer
  27166. if (defined $eval_stat) {
  27167. $retval = 0; chomp $eval_stat;
  27168. kill_proc($pid,$archiver,1,$proc_fh,$eval_stat) if defined $pid;
  27169. undef $proc_fh; undef $pid;
  27170. do_log(-1,"do_zoo: %s", $eval_stat);
  27171. }
  27172. chdir($tempdir) or die "Can't chdir to $tempdir: $!";
  27173. unlink("$fn.zoo") or die "Can't unlink $fn.zoo: $!";
  27174. if (defined $eval_stat) { die "do_zoo: $eval_stat\n" } # propagate failure
  27175. $retval;
  27176. }
  27177. # use external program to expand ARJ archives
  27178. #
  27179. sub do_unarj($$$;$) {
  27180. my($part, $tempdir, $archiver, $testing_for_sfx) = @_;
  27181. do_log(4, "Expanding ARJ archive %s", $part->base_name);
  27182. my $decompressor_name = basename((split(' ',$archiver))[0]);
  27183. snmp_count("OpsDecBy\u${decompressor_name}Attempt");
  27184. # options to arj, ignored by unarj
  27185. # provide some password in -g to turn fatal error into 'bad password' error
  27186. $ENV{ARJ_SW} = "-i -jo -b5 -2h -jyc -ja1 -gsecret -w$tempdir/parts";
  27187. # unarj needs extension of .arj!
  27188. my $fn = $part->full_name;
  27189. symlink($part->full_name, $fn.".arj")
  27190. or die sprintf("Can't symlink %s %s.arj: %s", $fn, $fn, $!);
  27191. my $retval = 1; my($proc_fh,$pid);
  27192. prolong_timer('do_unarj_pre'); # restart timer
  27193. my $eval_stat;
  27194. eval {
  27195. # obtain total original size of archive members from the index/listing
  27196. ($proc_fh,$pid) = run_command(undef, '&1', $archiver, 'l', $fn.".arj");
  27197. my $last_line; my $ln;
  27198. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0)
  27199. { $last_line = $ln if $ln !~ /^\s*$/ }
  27200. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading (1): $!";
  27201. do_log(-1,"unexpected(do_unarj_1): %s",$!) if !defined($ln) && $! == EAGAIN;
  27202. my $err = 0; $proc_fh->close or $err = $!;
  27203. my $rv = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27204. undef $proc_fh; undef $pid;
  27205. if (!proc_status_ok($rv,$err, 0,1,3)) { # one of: success, warn, CRC err
  27206. $part->attributes_add('U') if !$testing_for_sfx;
  27207. die "not an ARJ archive? ".exit_status_str($rv,$err);
  27208. } elsif ($last_line =~ /^\Q$fn\E.arj is not an ARJ archive$/) {
  27209. die "last line: $last_line";
  27210. } elsif ($last_line !~ /^\s*(\d+)\s*files\s*(\d+)/s) {
  27211. $part->attributes_add('U') if !$testing_for_sfx;
  27212. die "unable to obtain orig size of files: $last_line, ".
  27213. exit_status_str($rv,$err);
  27214. } else {
  27215. consumed_bytes($2, 'do_unarj-pre', 1); # pre-check on estimated size
  27216. }
  27217. # unarj has very limited extraction options, arj is much better!
  27218. mkdir("$tempdir/parts/arj",0750)
  27219. or die "Can't mkdir $tempdir/parts/arj: $!";
  27220. chdir("$tempdir/parts/arj")
  27221. or die "Can't chdir to $tempdir/parts/arj: $!";
  27222. snmp_count("OpsDecBy\u${decompressor_name}");
  27223. ($proc_fh,$pid) = run_command(undef, '&1', $archiver, 'e', $fn.".arj");
  27224. my($encryptedcount,$skippedcount,$entries_cnt) = (0,0,0);
  27225. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  27226. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27227. { die "Maximum number of files ($MAXFILES) exceeded" }
  27228. $encryptedcount++
  27229. if $ln =~ /^(Extracting.*\bBad file data or bad password|File is password encrypted, Skipped)\b/s;
  27230. $skippedcount++
  27231. if $ln =~ /(\bexists|^File is password encrypted|^Unsupported .*), Skipped\b/s;
  27232. }
  27233. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading (2): $!";
  27234. do_log(-1,"unexpected(do_unarj_2): %s",$!) if !defined($ln) && $! == EAGAIN;
  27235. $err = 0; $proc_fh->close or $err = $!;
  27236. $rv = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27237. undef $proc_fh; undef $pid;
  27238. chdir($tempdir) or die "Can't chdir to $tempdir: $!";
  27239. if (proc_status_ok($rv,$err, 0,1)) {} # success, warn
  27240. elsif (proc_status_ok($rv,$err, 3)) # CRC err
  27241. { $part->attributes_add('U') if !$testing_for_sfx }
  27242. else { do_log(0, "unarj: error extracting: %s",exit_status_str($rv,$err)) }
  27243. # add attributes to the parent object, because we didn't remember names
  27244. # of its scrambled members
  27245. $part->attributes_add('U') if $skippedcount;
  27246. $part->attributes_add('C') if $encryptedcount;
  27247. my $errn = lstat("$tempdir/parts/arj") ? 0 : 0+$!;
  27248. if ($errn != ENOENT) {
  27249. my $b = flatten_and_tidy_dir("$tempdir/parts/arj",
  27250. "$tempdir/parts",$part);
  27251. consumed_bytes($b, 'do_unarj');
  27252. snmp_count("OpsDecBy\u${decompressor_name}");
  27253. }
  27254. proc_status_ok($rv,$err, 0,1,3) # one of: success, warn, CRC err
  27255. or die "unarj: can't extract archive members: ".
  27256. exit_status_str($rv,$err);
  27257. if ($encryptedcount || $skippedcount) {
  27258. do_log(1,
  27259. "do_unarj: %s, %d members are encrypted, %d skipped, archive retained",
  27260. $part->base_name, $encryptedcount, $skippedcount);
  27261. $retval = 2;
  27262. }
  27263. 1;
  27264. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  27265. prolong_timer('do_unarj'); # restart timer
  27266. unlink($fn.".arj") or die "Can't unlink $fn.arj: $!";
  27267. if (defined $eval_stat) {
  27268. $retval = 0; chomp $eval_stat;
  27269. kill_proc($pid,$archiver,1,$proc_fh,$eval_stat) if defined $pid;
  27270. undef $proc_fh; undef $pid;
  27271. # if ($testing_for_sfx) { die "do_unarj: $eval_stat" }
  27272. # else { do_log(-1, "do_unarj: %s", $eval_stat) };
  27273. die "do_unarj: $eval_stat\n" # propagate failure
  27274. }
  27275. $retval;
  27276. }
  27277. # use external program to expand TNEF archives
  27278. #
  27279. sub do_tnef_ext($$$) {
  27280. my($part, $tempdir, $archiver) = @_;
  27281. do_log(4, "Extracting from TNEF encapsulation (ext) %s", $part->base_name);
  27282. my $archiver_name = basename((split(' ',$archiver))[0]);
  27283. snmp_count("OpsDecBy\u${archiver_name}");
  27284. mkdir("$tempdir/parts/tnef",0750)
  27285. or die "Can't mkdir $tempdir/parts/tnef: $!";
  27286. my $retval = 1; my($proc_fh,$pid);
  27287. prolong_timer('do_tnef_ext_pre'); # restart timer
  27288. my $rem_quota = max(10*1024, untaint(consumed_bytes(0,'do_tnef_ext')));
  27289. my $eval_stat;
  27290. eval {
  27291. ($proc_fh,$pid) = run_command(undef, '&1', $archiver,
  27292. '--number-backups', '-x', "$rem_quota",
  27293. '-C', "$tempdir/parts/tnef", '-f', $part->full_name);
  27294. collect_results($proc_fh,$pid,$archiver,16384,[0]);
  27295. undef $proc_fh; undef $pid; 1;
  27296. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  27297. prolong_timer('do_tnef_ext'); # restart timer
  27298. if (defined $eval_stat) {
  27299. $retval = 0; chomp $eval_stat;
  27300. do_log(-1, "tnef_ext: %s", $eval_stat);
  27301. }
  27302. my $b = flatten_and_tidy_dir("$tempdir/parts/tnef","$tempdir/parts",$part);
  27303. if ($b > 0) {
  27304. do_log(4, "tnef_ext extracted %d bytes from a tnef container", $b);
  27305. consumed_bytes($b, 'do_tnef_ext');
  27306. }
  27307. if (defined $eval_stat) { die "do_tnef_ext: $eval_stat\n" } # propagate
  27308. $retval;
  27309. }
  27310. # use Convert-TNEF
  27311. #
  27312. sub do_tnef($$) {
  27313. my($part, $tempdir) = @_;
  27314. do_log(4, "Extracting from TNEF encapsulation (int) %s", $part->base_name);
  27315. snmp_count('OpsDecByTnef');
  27316. my $tnef = Convert::TNEF->read_in($part->full_name,
  27317. {output_dir=>"$tempdir/parts", buffer_size=>16384, ignore_checksum=>1});
  27318. defined $tnef or die "Convert::TNEF failed: ".$Convert::TNEF::errstr;
  27319. my $item_num = 0; my $parent_placement = $part->mime_placement;
  27320. for my $a ($tnef->message, $tnef->attachments) {
  27321. for my $attr_name ('AttachData','Attachment') {
  27322. my $dh = $a->datahandle($attr_name);
  27323. if (defined $dh) {
  27324. my $newpart_obj = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
  27325. $item_num++;
  27326. $newpart_obj->mime_placement("$parent_placement/$item_num");
  27327. $newpart_obj->name_declared([$a->name, $a->longname]);
  27328. my $newpart = $newpart_obj->full_name;
  27329. my $outpart = IO::File->new;
  27330. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  27331. $outpart->open($newpart, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640)
  27332. or die "Can't create file $newpart: $!";
  27333. binmode($outpart) or die "Can't set file $newpart to binmode: $!";
  27334. my $filepath = $dh->path; my $size = 0;
  27335. if (defined $filepath) {
  27336. my($io,$nbytes,$buff); $dh->binmode(1);
  27337. $io = $dh->open("r") or die "Can't open MIME::Body handle: $!";
  27338. while (($nbytes=$io->read($buff,16384)) > 0) {
  27339. $outpart->print($buff) or die "Can't write to $newpart: $!";
  27340. $size += $nbytes; consumed_bytes($nbytes, 'do_tnef_1');
  27341. }
  27342. defined $nbytes or die "Error reading from MIME::Body handle: $!";
  27343. $io->close or die "Error closing MIME::Body handle: $!";
  27344. undef $buff; # release storage
  27345. } else {
  27346. my $buff = $dh->as_string; my $nbytes = length($buff);
  27347. $outpart->print($buff) or die "Can't write to $newpart: $!";
  27348. $size += $nbytes; consumed_bytes($nbytes, 'do_tnef_2');
  27349. }
  27350. $newpart_obj->size($size);
  27351. $outpart->close or die "Error closing $newpart: $!";
  27352. }
  27353. }
  27354. }
  27355. $tnef->purge if defined $tnef;
  27356. 1;
  27357. }
  27358. # The pax and cpio utilities usually support the following archive formats:
  27359. # cpio, bcpio, sv4cpio, sv4crc, tar (old tar), ustar (POSIX.2 tar).
  27360. # The utilities from http://heirloom.sourceforge.net/ support
  27361. # several other tar/cpio variants such as SCO, Sun, DEC, Cray, SGI
  27362. #
  27363. sub do_pax_cpio($$$) {
  27364. my($part, $tempdir, $archiver) = @_;
  27365. my $archiver_name = basename((split(' ',$archiver))[0]);
  27366. snmp_count("OpsDecBy\u${archiver_name}");
  27367. ll(4) && do_log(4,"Expanding archive %s, using %s",
  27368. $part->base_name,$archiver_name);
  27369. my $is_pax = $archiver_name =~ /^cpio/i ? 0 : 1;
  27370. do_log(-1,"WARN: Using %s instead of pax can be a security ".
  27371. "risk; please add: \$pax='pax'; to amavisd.conf and check that ".
  27372. "the pax(1) utility is available on the system!",
  27373. $archiver_name) if !$is_pax;
  27374. my(@cmdargs) = $is_pax ? qw(-v) : qw(-i -t -v);
  27375. my($proc_fh,$pid) = run_command($part->full_name, '/dev/null',
  27376. $archiver, @cmdargs);
  27377. my $bytes = 0; local($1,$2); local($_); my $entries_cnt = 0;
  27378. for ($! = 0; defined($_=$proc_fh->getline); $! = 0) {
  27379. chomp;
  27380. next if /^\d+ blocks\z/;
  27381. last if /^(cpio|pax): (.*bytes read|End of archive volume)/;
  27382. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27383. { die "Maximum number of files ($MAXFILES) exceeded" }
  27384. if (!/^ (?: \S+\s+ ){4} (\d+) \s+ (.+) \z/xs) {
  27385. do_log(-1,"do_pax_cpio: can't parse toc line: %s", $_);
  27386. } else {
  27387. my($size,$mem) = ($1,$2);
  27388. if ($mem =~ /^( (?: \s* \S+ ){3} (?: \s+ \d{4}, )? ) \s+ (.+)\z/xs) {
  27389. $mem = $2; # strip away time and date
  27390. } elsif ($mem =~ /^\S \s+ (.+)\z/xs) {
  27391. # -rwxr-xr-x 1 1121 users 3135 C errorReport.sh
  27392. $mem = $1; # strip away a letter in place of a date (?)
  27393. }
  27394. $mem = $1 if $is_pax && $mem =~ /^(.*) =[=>] (.*)\z/; # hard or soft link
  27395. do_log(5,'do_pax_cpio: size: %5s, member: "%s"', $size,$mem);
  27396. $bytes += $size if $size > 0;
  27397. }
  27398. }
  27399. defined $_ || $! == 0 || $! == EAGAIN or die "Error reading (1): $!";
  27400. do_log(-1,"unexpected(pax_cpio_1): %s",$!) if !defined($_) && $! == EAGAIN;
  27401. # consume remaining output to avoid broken pipe
  27402. collect_results($proc_fh,$pid,'do_pax_cpio/1',16384,[0]);
  27403. undef $proc_fh; undef $pid;
  27404. consumed_bytes($bytes, 'do_pax_cpio/pre', 1); # pre-check on estimated size
  27405. mkdir("$tempdir/parts/arch", 0750)
  27406. or die "Can't mkdir $tempdir/parts/arch: $!";
  27407. my $name_clash = 0;
  27408. my(%orig_names); # maps filenames to archive member names when possible
  27409. prolong_timer('do_pax_cpio_pre'); # restart timer
  27410. my $eval_stat;
  27411. eval {
  27412. chdir("$tempdir/parts/arch")
  27413. or die "Can't chdir to $tempdir/parts/arch: $!";
  27414. my(@cmdargs) = $is_pax ? qw(-r -k -p am -s /[^A-Za-z0-9_]/-/gp)
  27415. : qw(-i -d --no-absolute-filenames --no-preserve-owner);
  27416. ($proc_fh,$pid) = run_command($part->full_name, '&1', $archiver, @cmdargs);
  27417. my $output = ''; my $ln; my $entries_cnt = 0;
  27418. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  27419. chomp($ln);
  27420. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27421. { die "Maximum number of files ($MAXFILES) exceeded" }
  27422. if (!$is_pax || $ln !~ /^(.*) >> (\S*)\z/) { $output .= $ln."\n" }
  27423. else { # parse output from pax -s///p
  27424. my($member_name,$file_name) = ($1,$2);
  27425. if (!exists $orig_names{$file_name}) {
  27426. $orig_names{$file_name} = $member_name;
  27427. } else {
  27428. do_log(0,'do_pax_cpio: member "%s" is hidden by a '.
  27429. 'previous archive member "%s", file: %s',
  27430. $member_name, $orig_names{$file_name}, $file_name);
  27431. undef $orig_names{$file_name}; # cause it to exist but undefined
  27432. $name_clash = 1;
  27433. }
  27434. }
  27435. }
  27436. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading (2): $!";
  27437. do_log(-1,"unexpected(pax_cpio_2): %s",$!) if !defined($ln) && $! == EAGAIN;
  27438. my $err = 0; $proc_fh->close or $err = $!;
  27439. my $child_stat = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27440. undef $proc_fh; undef $pid; chomp($output);
  27441. proc_status_ok($child_stat,$err)
  27442. or die(exit_status_str($child_stat,$err).' '.$output);
  27443. 1;
  27444. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  27445. prolong_timer('do_pax_cpio'); # restart timer
  27446. chdir($tempdir) or die "Can't chdir to $tempdir: $!";
  27447. my $b = flatten_and_tidy_dir("$tempdir/parts/arch", "$tempdir/parts",
  27448. $part, 0, \%orig_names);
  27449. consumed_bytes($b, 'do_pax_cpio');
  27450. if (defined $eval_stat) {
  27451. chomp $eval_stat;
  27452. kill_proc($pid,$archiver,1,$proc_fh,$eval_stat) if defined $pid;
  27453. undef $proc_fh; undef $pid;
  27454. die "do_pax_cpio: $eval_stat\n"; # propagate failure
  27455. }
  27456. $name_clash ? 2 : 1;
  27457. }
  27458. # command line unpacker from stuffit.com for Linux
  27459. # decodes Macintosh StuffIt archives and others
  27460. # (but it appears the Linux version is buggy and a security risk, not to use!)
  27461. #
  27462. sub do_unstuff($$$) {
  27463. my($part, $tempdir, $archiver) = @_;
  27464. my $archiver_name = basename((split(' ',$archiver))[0]);
  27465. snmp_count("OpsDecBy\u${archiver_name}");
  27466. do_log(4,"Expanding archive %s, using %s", $part->base_name,$archiver_name);
  27467. mkdir("$tempdir/parts/unstuff", 0750)
  27468. or die "Can't mkdir $tempdir/parts/unstuff: $!";
  27469. my($proc_fh,$pid) = run_command(undef, '&1', $archiver, # '-q',
  27470. "-d=$tempdir/parts/unstuff", $part->full_name);
  27471. collect_results($proc_fh,$pid,$archiver,16384,[0]);
  27472. undef $proc_fh; undef $pid;
  27473. my $b = flatten_and_tidy_dir("$tempdir/parts/unstuff",
  27474. "$tempdir/parts", $part);
  27475. consumed_bytes($b, 'do_unstuff');
  27476. 1;
  27477. }
  27478. # ar is a standard Unix binary archiver, also used by Debian packages
  27479. #
  27480. sub do_ar($$$) {
  27481. my($part, $tempdir, $archiver) = @_;
  27482. ll(4) && do_log(4,"Expanding Unix ar archive %s", $part->full_name);
  27483. my $archiver_name = basename((split(' ',$archiver))[0]);
  27484. snmp_count("OpsDecBy\u${archiver_name}");
  27485. my($proc_fh,$pid) = run_command(undef, '/dev/null',
  27486. $archiver, 'tv', $part->full_name);
  27487. my $ln; my $bytes = 0; local($1,$2,$3); my $entries_cnt = 0;
  27488. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  27489. chomp($ln);
  27490. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27491. { die "Maximum number of files ($MAXFILES) exceeded" }
  27492. if ($ln !~ /^(?:\S+\s+){2}(\d+)\s+((?:\S+\s+){3}\S+)\s+(.*)\z/) {
  27493. do_log(-1,"do_ar: can't parse contents listing line: %s", $ln);
  27494. } else {
  27495. do_log(5,"do_ar: member: \"%s\", size: %s", $3,$1);
  27496. $bytes += $1 if $1 > 0;
  27497. }
  27498. }
  27499. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading: $!";
  27500. do_log(-1,"unexpected(do_ar): %s",$!) if !defined($ln) && $! == EAGAIN;
  27501. # consume remaining output to avoid broken pipe
  27502. collect_results($proc_fh,$pid,'ar-1',16384,[0]);
  27503. undef $proc_fh; undef $pid;
  27504. consumed_bytes($bytes, 'do_ar-pre', 1); # pre-check on estimated size
  27505. mkdir("$tempdir/parts/ar", 0750)
  27506. or die "Can't mkdir $tempdir/parts/ar: $!";
  27507. chdir("$tempdir/parts/ar") or die "Can't chdir to $tempdir/parts/ar: $!";
  27508. ($proc_fh,$pid) = run_command(undef, '&1', $archiver, 'x', $part->full_name);
  27509. collect_results($proc_fh,$pid,'ar-2',16384,[0]);
  27510. undef $proc_fh; undef $pid;
  27511. chdir($tempdir) or die "Can't chdir to $tempdir: $!";
  27512. my $b = flatten_and_tidy_dir("$tempdir/parts/ar","$tempdir/parts",$part);
  27513. consumed_bytes($b, 'do_ar');
  27514. 1;
  27515. }
  27516. sub do_cabextract($$$) {
  27517. my($part, $tempdir, $archiver) = @_;
  27518. do_log(4, "Expanding cab archive %s", $part->base_name);
  27519. my $archiver_name = basename((split(' ',$archiver))[0]);
  27520. snmp_count("OpsDecBy\u${archiver_name}");
  27521. my($proc_fh,$pid) =
  27522. run_command(undef, '/dev/null', $archiver, '-l', $part->full_name);
  27523. local($1,$2); my $bytes = 0; my $ln; my $entries_cnt = 0;
  27524. for ($! = 0; defined($ln=$proc_fh->getline); $! = 0) {
  27525. chomp($ln);
  27526. next if $ln =~ /^(File size|----|Viewing cabinet:|\z)/;
  27527. if ($entries_cnt++, $MAXFILES && $entries_cnt > $MAXFILES)
  27528. { die "Maximum number of files ($MAXFILES) exceeded" }
  27529. if ($ln !~ /^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) {
  27530. do_log(-1, "do_cabextract: can't parse toc line: %s", $ln);
  27531. } else {
  27532. do_log(5, 'do_cabextract: member: "%s", size: %s', $2,$1);
  27533. $bytes += $1 if $1 > 0;
  27534. }
  27535. }
  27536. defined $ln || $! == 0 || $! == EAGAIN or die "Error reading: $!";
  27537. do_log(-1,"unexpected(cabextract): %s",$!) if !defined($ln) && $! == EAGAIN;
  27538. # consume remaining output to avoid broken pipe (just in case)
  27539. collect_results($proc_fh,$pid,'cabextract-1',16384,[0]);
  27540. undef $proc_fh; undef $pid;
  27541. mkdir("$tempdir/parts/cab",0750) or die "Can't mkdir $tempdir/parts/cab: $!";
  27542. ($proc_fh,$pid) = run_command(undef, '/dev/null', $archiver, '-q', '-d',
  27543. "$tempdir/parts/cab", $part->full_name);
  27544. collect_results($proc_fh,$pid,'cabextract-2',16384,[0]);
  27545. undef $proc_fh; undef $pid;
  27546. my $b = flatten_and_tidy_dir("$tempdir/parts/cab", "$tempdir/parts", $part);
  27547. consumed_bytes($b, 'do_cabextract');
  27548. 1;
  27549. }
  27550. sub do_ole($$$) {
  27551. my($part, $tempdir, $archiver) = @_;
  27552. do_log(4,"Expanding MS OLE document %s", $part->base_name);
  27553. my $archiver_name = basename((split(' ',$archiver))[0]);
  27554. snmp_count("OpsDecBy\u${archiver_name}");
  27555. mkdir("$tempdir/parts/ole",0750) or die "Can't mkdir $tempdir/parts/ole: $!";
  27556. my($proc_fh,$pid) = run_command(undef, '&1', $archiver, '-v',
  27557. '-i', $part->full_name, '-d',"$tempdir/parts/ole");
  27558. collect_results($proc_fh,$pid,$archiver,16384,[0]);
  27559. undef $proc_fh; undef $pid;
  27560. my $b = flatten_and_tidy_dir("$tempdir/parts/ole", "$tempdir/parts", $part);
  27561. if ($b > 0) {
  27562. do_log(4, "ripOLE extracted %d bytes from an OLE document", $b);
  27563. consumed_bytes($b, 'do_ole');
  27564. }
  27565. 2; # always keep the original OLE document
  27566. }
  27567. # Check for self-extracting archives. Note that we do not depend on
  27568. # file magic here since it's not reliable. Instead we will try each
  27569. # archiver.
  27570. #
  27571. sub do_executable($$@) {
  27572. my($part, $tempdir, $unrar, $lha, $unarj) = @_;
  27573. ll(4) && do_log(4,"Check whether %s is a self-extracting archive",
  27574. $part->base_name);
  27575. # ZIP?
  27576. return 2 if eval { do_unzip($part,$tempdir,undef,1) };
  27577. chomp $@;
  27578. do_log(3, "do_executable: not a ZIP sfx, ignoring: %s", $@) if $@ ne '';
  27579. # RAR?
  27580. return 2 if defined $unrar && eval { do_unrar($part,$tempdir,$unrar,1) };
  27581. chomp $@;
  27582. do_log(3, "do_executable: not a RAR sfx, ignoring: %s", $@) if $@ ne '';
  27583. # # LHA? not safe, tends to crash
  27584. # return 2 if defined $lha && eval { do_lha($part,$tempdir,$lha,1) };
  27585. # chomp $@;
  27586. # do_log(3, "do_executable: not an LHA sfx, ignoring: %s", $@) if $@ ne '';
  27587. # ARJ?
  27588. return 2 if defined $unarj && eval { do_unarj($part,$tempdir,$unarj,1) };
  27589. chomp $@;
  27590. do_log(3, "do_executable: not an ARJ sfx, ignoring: %s", $@) if $@ ne '';
  27591. return 0;
  27592. }
  27593. # my($k,$v,$fn);
  27594. # while (($k,$v) = each(%::)) {
  27595. # local(*e)=$v; $fn=fileno(\*e);
  27596. # printf STDOUT ("%-10s %-10s %s\n",$k,$v,$fn) if defined $fn;
  27597. # }
  27598. # Given a file handle (typically opened pipe to a subprocess, as returned
  27599. # by run_command), copy from it to a specified output file in binary mode.
  27600. #
  27601. sub run_command_copy($$$) {
  27602. my($outfile, $ifh, $pid) = @_;
  27603. my $ofh = IO::File->new;
  27604. # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502]
  27605. $ofh->open($outfile, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640) # calls sysopen
  27606. or die "Can't create file $outfile: $!";
  27607. binmode($ofh) or die "Can't set file $outfile to binmode: $!";
  27608. binmode($ifh) or die "Can't set binmode on pipe: $!";
  27609. my $eval_stat; my($rv,$rerr); $rerr = 0;
  27610. eval {
  27611. my($nread,$nwrite,$tosend,$offset,$inbuf);
  27612. for (;;) {
  27613. $nread = sysread($ifh, $inbuf, 32768);
  27614. if (!defined($nread)) {
  27615. if ($! == EAGAIN || $! == EINTR) {
  27616. Time::HiRes::sleep(0.1); # just in case
  27617. } else {
  27618. die "Error reading: $!";
  27619. }
  27620. } elsif ($nread < 1) { # sysread returns 0 at eof
  27621. last;
  27622. } else {
  27623. consumed_bytes($nread, 'run_command_copy');
  27624. $tosend = $nread; $offset = 0;
  27625. while ($tosend > 0) { # handle partial writes
  27626. $nwrite = syswrite($ofh, $inbuf, $tosend, $offset);
  27627. if (!defined($nwrite)) {
  27628. if ($! == EAGAIN || $! == EINTR) {
  27629. Time::HiRes::sleep(0.1); # just in case
  27630. } else {
  27631. die "Error writing to $outfile: $!";
  27632. }
  27633. } elsif ($nwrite < 1) {
  27634. Time::HiRes::sleep(0.1); # just in case
  27635. } else {
  27636. $tosend -= $nwrite; $offset += $nwrite;
  27637. }
  27638. }
  27639. }
  27640. }
  27641. $ifh->close or $rerr = $!;
  27642. $rv = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27643. $ofh->close or die "Error closing $outfile: $!";
  27644. 1;
  27645. } or do {
  27646. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  27647. # remember error, close socket ignoring status
  27648. $rerr = $!; $ifh->close;
  27649. $rv = defined $pid && waitpid($pid,0) > 0 ? $? : undef;
  27650. do_log(-1, "run_command_copy: %s", $eval_stat);
  27651. $ofh->close or do_log(-1, "Error closing %s: %s", $outfile,$!);
  27652. };
  27653. if (defined $eval_stat) { die "run_ccpy: $eval_stat\n" } # propagate failure
  27654. ($rv,$rerr); # return subprocess termination status and reading/close errno
  27655. }
  27656. # extract listed files from archive and store each in a new file
  27657. #
  27658. sub store_mgr($$$@) {
  27659. my($tempdir, $parent_obj, $list, $archiver, @args) = @_;
  27660. my $item_num = 0; my $parent_placement = $parent_obj->mime_placement;
  27661. my $retval = 0; my($proc_fh,$pid);
  27662. prolong_timer('store_mgr_pre'); # restart timer
  27663. my $eval_stat;
  27664. eval {
  27665. for my $f (@$list) {
  27666. next if $f =~ m{/\z}; # ignore directories
  27667. my $newpart_obj =
  27668. Amavis::Unpackers::Part->new("$tempdir/parts",$parent_obj);
  27669. $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
  27670. $newpart_obj->name_declared($f); # store tainted name
  27671. my $newpart = $newpart_obj->full_name;
  27672. ll(5) && do_log(5,'store_mgr: extracting "%s" to file %s using %s',
  27673. $f, $newpart, $archiver);
  27674. if ($f =~ m{^\.?[A-Za-z0-9_][A-Za-z0-9/._=~-]*\z}) { #presumably safe arg
  27675. } else { # this is not too bad, as run_command does not use shell
  27676. do_log(1, 'store_mgr: NOTICE: suspicious file name "%s"', $f);
  27677. }
  27678. ($proc_fh,$pid) = run_command(undef, '/dev/null',
  27679. $archiver, @args, untaint($f));
  27680. my($rv,$err) = run_command_copy($newpart,$proc_fh,$pid); # may die
  27681. my $ll = proc_status_ok($rv,$err) ? 5 : 1;
  27682. ll($ll) && do_log($ll,"store_mgr: extracted by %s, %s",
  27683. $archiver, exit_status_str($rv,$err));
  27684. $retval = $rv if $retval == 0 && $rv != 0;
  27685. }
  27686. 1;
  27687. } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
  27688. prolong_timer('store_mgr'); # restart timer
  27689. if (defined $eval_stat) {
  27690. $retval = 0; chomp $eval_stat;
  27691. kill_proc($pid,$archiver,1,$proc_fh,$eval_stat) if defined $pid;
  27692. undef $proc_fh; undef $pid;
  27693. die "store_mgr: $eval_stat\n"; # propagate failure
  27694. }
  27695. $retval; # return the first nonzero status (if any), or 0
  27696. }
  27697. 1;
  27698. __DATA__
  27699. #
  27700. package Amavis::DKIM::CustomSigner;
  27701. use strict;
  27702. use re 'taint';
  27703. use warnings;
  27704. use warnings FATAL => qw(utf8 void);
  27705. no warnings 'uninitialized';
  27706. sub new {
  27707. my($class,%params) = @_;
  27708. bless { %params }, $class;
  27709. }
  27710. sub sign_digest {
  27711. my($self_key, $digest_alg_name, $digest) = @_;
  27712. my $code = $self_key->{CustomSigner};
  27713. &$code($digest_alg_name, $digest, %$self_key);
  27714. }
  27715. 1;
  27716. package Amavis::DKIM;
  27717. use strict;
  27718. use re 'taint';
  27719. use warnings;
  27720. use warnings FATAL => qw(utf8 void);
  27721. no warnings 'uninitialized';
  27722. BEGIN {
  27723. require Exporter;
  27724. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  27725. $VERSION = '2.316';
  27726. @ISA = qw(Exporter);
  27727. @EXPORT_OK = qw(&dkim_key_postprocess &generate_authentication_results
  27728. &dkim_make_signatures &adjust_score_by_signer_reputation
  27729. &collect_some_dkim_info);
  27730. import Amavis::Conf qw(:platform c cr ca $myproduct_name
  27731. %dkim_signing_keys_by_domain
  27732. @dkim_signing_keys_list @dkim_signing_keys_storage);
  27733. import Amavis::Util qw(min max minmax untaint ll do_log unique_list
  27734. get_deadline proto_encode proto_decode);
  27735. import Amavis::rfc2821_2822_Tools qw(split_address quote_rfc2821_local
  27736. qquote_rfc2821_local format_time_interval);
  27737. import Amavis::Timing qw(section_time);
  27738. import Amavis::Lookup qw(lookup lookup2);
  27739. }
  27740. use subs @EXPORT_OK;
  27741. use IO::File ();
  27742. use Crypt::OpenSSL::RSA ();
  27743. use MIME::Base64;
  27744. use Mail::DKIM::Verifier 0.31;
  27745. use Mail::DKIM::Signer 0.31;
  27746. use Mail::DKIM::TextWrap;
  27747. use Mail::DKIM::Signature;
  27748. use Mail::DKIM::DkSignature;
  27749. # Convert private keys (as strings in PEM format) into RSA objects
  27750. # and do some pre-processing on @dkim_signing_keys_list entries
  27751. # (may run unprivileged)
  27752. #
  27753. sub dkim_key_postprocess() {
  27754. # convert private keys (as strings in PEM format) into RSA objects
  27755. for my $ks (@dkim_signing_keys_storage) {
  27756. my($pkcs1,$dev,$inode,$fname) = @$ks;
  27757. if (ref($pkcs1) && UNIVERSAL::isa($pkcs1,'Crypt::OpenSSL::RSA')) {
  27758. # it is already a Crypt::OpenSSL::RSA object
  27759. } else {
  27760. # assume a string is a private key in PEM format, convert it to RSA obj
  27761. $ks->[0] = Crypt::OpenSSL::RSA->new_private_key($pkcs1);
  27762. }
  27763. }
  27764. for my $ent (@dkim_signing_keys_list) {
  27765. my $domain = $ent->{domain};
  27766. $dkim_signing_keys_by_domain{$domain} = []
  27767. if !$dkim_signing_keys_by_domain{$domain};
  27768. }
  27769. my $any_wild; my $j = 0;
  27770. for my $ent (@dkim_signing_keys_list) {
  27771. $ent->{v} = 'DKIM1' if !defined $ent->{v}; # provide a default
  27772. if (defined $ent->{n}) { # encode n as qp-section (RFC 4871, RFC 2047)
  27773. $ent->{n} =~ s{([\000-\037\177=;"])}{sprintf('=%02X',ord($1))}egs;
  27774. }
  27775. my $domain = $ent->{domain};
  27776. if (ref($domain) eq 'Regexp') {
  27777. $ent->{domain_re} = $domain;
  27778. $any_wild = sprintf("key#%d, %s", $j+1, $domain) if !defined $any_wild;
  27779. } elsif ($domain =~ /\*/) {
  27780. # wildcarded signing domain in a key declaration, evil, asks for trouble!
  27781. # support wildcards in signing domain for compatibility with dkim_milter
  27782. my $regexp = $domain;
  27783. $regexp =~ s/\*{2,}/*/gs; # collapse successive wildcards
  27784. # '*' is a wildcard, quote the rest
  27785. $regexp =~ s{ ([@#/.^$|*+?(){}\[\]\\]) }{$1 eq '*' ? '.*' : '\\'.$1}gex;
  27786. $regexp = '^' . $regexp . '\\z'; # implicit anchors
  27787. $regexp =~ s/^\^\.\*//s; # remove leading anchor if redundant
  27788. $regexp =~ s/\.\*\\z\z//s; # remove trailing anchor if redundant
  27789. $regexp = '(?:)' if $regexp eq ''; # just in case, non-empty regexp
  27790. # presence of {'domain_re'} entry lets get_dkim_key use this regexp
  27791. # instead of a direct string comparison with {'domain'}
  27792. $ent->{domain_re} = qr{$regexp}; # compiled regexp object
  27793. $any_wild = sprintf("key#%d, %s", $j+1, $domain) if !defined $any_wild;
  27794. }
  27795. # %dkim_signing_keys_by_domain entries contain lists of indices into
  27796. # the @dkim_signing_keys_list of all potentially applicable signing keys.
  27797. # This hash (keyed by domain name) avoids linear searching for signing
  27798. # keys for all fully-specified domains in @dkim_signing_keys_list.
  27799. # Wildcarded entries must still be looked up sequentially at run-time
  27800. # to preserve the declared order and the 'first match wins' paradigm.
  27801. # Such entries are only supported for compatibility with dkim_milter
  27802. # and are evil because amavisd has no quick way of verifying that DNS RR
  27803. # really exists, so signatures generated by amavisd can fail when not all
  27804. # possible DNS resource records exist for wildcarded signing domains.
  27805. #
  27806. if (!defined($ent->{domain_re})) { # no regexp, just plain match on domain
  27807. push(@{$dkim_signing_keys_by_domain{$domain}}, $j);
  27808. } else { # a wildcard in a signing domain, compatibility with dkim_milter
  27809. # wildcarded signing domain potentially matches any _by_domain entry
  27810. for my $d (keys %dkim_signing_keys_by_domain) {
  27811. push(@{$dkim_signing_keys_by_domain{$d}}, $j);
  27812. }
  27813. # the '*' entry collects only wildcarded signing keys
  27814. $dkim_signing_keys_by_domain{'*'} = []
  27815. if !$dkim_signing_keys_by_domain{'*'};
  27816. push(@{$dkim_signing_keys_by_domain{'*'}}, $j);
  27817. }
  27818. $j++;
  27819. }
  27820. do_log(0,"dkim: wildcard in signing domain (%s), may produce unverifiable ".
  27821. "signatures with no published public key, avoid!", $any_wild)
  27822. if $any_wild;
  27823. }
  27824. # Fetch a private DKIM signing key for a given signing domain, with its
  27825. # resource-record (RR) constraints compatible with proposed signature options.
  27826. # The first such key is returned as a hash; if no key is found an empty hash
  27827. # is returned. When a selector (s) is given it must match the selector of
  27828. # a key; when algorithm (a) is given, the key type and a hash algorithm must
  27829. # match the desired use too; the service type (s) must be 'email' or '*';
  27830. # when identity (i) is given it must match the granularity (g) of a key;
  27831. #
  27832. # sign.opts. key options
  27833. # ---------- -----------
  27834. # d => domain
  27835. # s => selector
  27836. # a => k, h(list)
  27837. # i => g, t=s
  27838. #
  27839. sub get_dkim_key(@) {
  27840. @_ % 2 == 0 or die "get_dkim_key: a list of pairs is expected as query opts";
  27841. my(%options) = @_; # signature options (v, a, c, d, h, i, l, q, s, t, x, z),
  27842. # of which d is required, while s, a and t are optional but taken into
  27843. # account in searching for a compatible key - the rest are ignored
  27844. my(%key_options);
  27845. my $domain = $options{d}; my $selector = $options{s};
  27846. defined $domain && $domain ne ''
  27847. or die "get_dkim_key: domain is required, but tag 'd' is missing";
  27848. $domain = lc($domain);
  27849. $selector = lc($selector) if defined $selector;
  27850. my(@indices) = $dkim_signing_keys_by_domain{$domain} ?
  27851. @{$dkim_signing_keys_by_domain{$domain}} :
  27852. $dkim_signing_keys_by_domain{'*'} ?
  27853. @{$dkim_signing_keys_by_domain{'*'}} : ();
  27854. if (@indices) {
  27855. $selector = $selector eq '' ? undef : lc($selector) if defined $selector;
  27856. local($1,$2);
  27857. my($keytype,$hashalg) =
  27858. defined $options{a} && $options{a} =~ /^([a-z0-9]+)-(.*)\z/is ? ($1,$2)
  27859. : ('rsa',undef);
  27860. my($identity_localpart,$identity_domain) =
  27861. !defined($options{i}) ? () : split_address($options{i});
  27862. $identity_localpart = '' if !defined $identity_localpart;
  27863. $identity_domain = '' if !defined $identity_domain;
  27864. # find the first key (associated with a domain) with compatible options
  27865. for my $j (@indices) {
  27866. my $ent = $dkim_signing_keys_list[$j];
  27867. next unless defined $ent->{domain_re} ? $domain =~ $ent->{domain_re}
  27868. : $domain eq $ent->{domain};
  27869. next if defined $selector && $ent->{selector} ne $selector;
  27870. next if $keytype ne (exists $ent->{k} ? $ent->{k} : 'rsa');
  27871. next if exists $ent->{s} &&
  27872. !(grep($_ eq '*' || $_ eq 'email', split(/:/, $ent->{s})) );
  27873. next if defined $hashalg && exists $ent->{'h'} &&
  27874. !(grep($_ eq $hashalg, split(/:/, $ent->{'h'})) );
  27875. if (defined($options{i})) {
  27876. if (lc($identity_domain) eq $domain) {
  27877. # ok
  27878. } elsif (exists $ent->{t} && (grep($_ eq 's', split(/:/,$ent->{t})))) {
  27879. next; # no subdomains allowed
  27880. }
  27881. if (!exists($ent->{g}) || $ent->{g} eq '*') {
  27882. # ok
  27883. } elsif ($ent->{g} =~ /^ ([^*]*) \* (.*) \z/xs) {
  27884. next if $identity_localpart !~ /^ \Q$1\E .* \Q$2\E \z/xs;
  27885. } else {
  27886. next if $identity_localpart ne $ent->{g};
  27887. }
  27888. }
  27889. %key_options = %$ent; last; # found a suitable match
  27890. }
  27891. }
  27892. if (defined $key_options{key_storage_ind}) {
  27893. # obtain actual key from @dkim_signing_keys_storage
  27894. ($key_options{key}) =
  27895. @{$dkim_signing_keys_storage[$key_options{key_storage_ind}]};
  27896. }
  27897. %key_options;
  27898. }
  27899. # send a query to a signing service, collect its response and parse it;
  27900. # the protocol is much like the AM.PDP protocol, except that attributes
  27901. # are different
  27902. #
  27903. sub query_signing_service($$) {
  27904. my($server, $query) = @_;
  27905. my($remaining_time, $deadline) = get_deadline('query_signing_service');
  27906. my $sock = Amavis::IO::RW->new($server, Eol => "\015\012", Timeout => 10);
  27907. $sock or die "Error connecting to a signing server $server: $!";
  27908. my $req_id = sprintf("%08x", rand(0x7fffffff));
  27909. my $req_id_attr = proto_encode('request_id', $req_id);
  27910. $sock->print(join('', map($_."\015\012", (@$query, $req_id_attr, ''))))
  27911. or die "Error sending a query to a signing server";
  27912. ll(5) && do_log(5, "dkim: query_signing_service, query: %s",
  27913. join('; ', @$query, $req_id_attr));
  27914. $sock->flush or die "Error flushing signing server session";
  27915. # collect a reply
  27916. $sock->timeout(max(2, $deadline - Time::HiRes::time));
  27917. my(%attr,$ln); local($1,$2);
  27918. while (defined($ln = $sock->get_response_line)) {
  27919. last if $ln eq "\015\012"; # end of a response block
  27920. if ($ln =~ /^ ([^=\000\012]*?) = ([^\012]*?) \015\012 \z/xsi) {
  27921. $attr{proto_decode($1)} = proto_decode($2);
  27922. }
  27923. }
  27924. $sock->close or die "Error closing session to a signing server $server: $!";
  27925. ll(5) && do_log(5, "dkim: query_signing_service, got: %s",
  27926. join('; ', map($_.'='.$attr{$_}, keys %attr)));
  27927. $attr{request_id} eq $req_id
  27928. or die "Answer id '$attr{request_id}' from $server ".
  27929. "does not match the query id '$req_id'";
  27930. \%attr;
  27931. }
  27932. # send candidate originator addresses and signature options to a signing
  27933. # service and let it choose a selector 's' and a domain 'd', thus uniquely
  27934. # identifying a signing key
  27935. #
  27936. sub let_signing_service_choose($$$$) {
  27937. my($server, $msginfo, $sender_search_list_ref, $sig_opt_prelim) = @_;
  27938. my(@query) = (
  27939. proto_encode('request', 'choose_key'),
  27940. proto_encode('log_id', $msginfo->log_id),
  27941. );
  27942. # provide some additional information potentially useful in decision-making
  27943. if ($sig_opt_prelim) {
  27944. for my $opt (sort keys %$sig_opt_prelim) {
  27945. push(@query, proto_encode('sig.'.$opt, $sig_opt_prelim->{$opt}));
  27946. }
  27947. }
  27948. push(@query, proto_encode('sender', $msginfo->sender_smtp));
  27949. for my $r (@{$msginfo->per_recip_data}) {
  27950. push(@query, proto_encode('recip', $r->recip_addr_smtp));
  27951. }
  27952. for my $pair (!$sender_search_list_ref ? () : @$sender_search_list_ref) {
  27953. my($addr,$addr_src) = @$pair;
  27954. push(@query, proto_encode('candidate', $addr_src,
  27955. qquote_rfc2821_local($addr)));
  27956. }
  27957. my $attr;
  27958. eval {
  27959. $attr = query_signing_service($server,\@query); 1;
  27960. } or do {
  27961. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  27962. do_log(0, "query_signing_service failed: %s", $eval_stat);
  27963. };
  27964. my(%sig_options, $chosen_addr_src, $chosen_addr);
  27965. if ($attr) {
  27966. for my $opt (keys %$attr) {
  27967. if ($opt =~ /^sig\.(.+)\z/) {
  27968. $sig_options{$1} = $attr->{$opt} if !exists($sig_options{$1});
  27969. }
  27970. }
  27971. if (defined $attr->{chosen_candidate}) {
  27972. ($chosen_addr_src, $chosen_addr) =
  27973. split(' ', $attr->{chosen_candidate}, 2);
  27974. }
  27975. }
  27976. (!$attr ? undef : \%sig_options, $chosen_addr_src, $chosen_addr);
  27977. }
  27978. # a CustomSigner callback routine passed to Mail::DKIM in place of a key;
  27979. # the routine will be called by Mail::DKIM::Algorithm::*rsa_sha* routines
  27980. # instead of calling their own Mail::DKIM::PrivateKey::sign_digest()
  27981. #
  27982. sub remote_signer {
  27983. my($digest_alg_name, $digest, %args) = @_;
  27984. # $digest: header digest (binary), ready for signing,
  27985. # e.g. $algorithm->{header_digest}->digest
  27986. my $server = $args{Server}; # our own info passed back to us
  27987. my $msginfo = $args{MsgInfo}; # our own info passed back to us
  27988. my(@query) = (
  27989. proto_encode('request', 'sign'),
  27990. proto_encode('digest_alg', $digest_alg_name),
  27991. proto_encode('digest', encode_base64($digest,'')),
  27992. proto_encode('s', $args{Selector}),
  27993. proto_encode('d', $args{Domain}),
  27994. proto_encode('log_id', $msginfo->log_id),
  27995. );
  27996. my($attr, $b, $reason);
  27997. eval {
  27998. $attr = query_signing_service($server, \@query); 1;
  27999. } or do {
  28000. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  28001. $reason = $eval_stat;
  28002. };
  28003. if ($attr) { $b = $attr->{b}; $reason = $attr->{reason} }
  28004. if (!defined($b) || $b eq '') {
  28005. $reason = 'no signature from a signing server' if !defined $reason;
  28006. # die "Can't sign, $reason, query: " . join('; ',@query) . "\n";
  28007. do_log(0, "dkim: can't sign, %s, query: %s", $reason, join('; ',@query));
  28008. return ''; # Mail::DKIM::Algorithm::rsa_sha256 doesn't like undef
  28009. } else {
  28010. return decode_base64($b); # resulting signature
  28011. }
  28012. }
  28013. # prepare requested DKIM signatures for a provided message,
  28014. # returning them as a list of Mail::DKIM::Signature objects
  28015. #
  28016. sub dkim_make_signatures($$;$) {
  28017. my($msginfo,$initial_submission,$callback) = @_;
  28018. my(@signatures); # resulting signature objects
  28019. my(%sig_options); # signature options and constraints for choosing a key
  28020. my(%key_options); # options associated with a signing key
  28021. my(@tried_domains); # used for logging a failure
  28022. my($chosen_addr,$chosen_addr_src); my $do_sign = 0;
  28023. my $fm = $msginfo->rfc2822_from; # authors
  28024. my(@rfc2822_from) = !defined($fm) ? () : ref $fm ? @$fm : $fm;
  28025. my $allowed_hdrs = cr('allowed_added_header_fields');
  28026. my $from_str = join(', ', qquote_rfc2821_local(@rfc2822_from)); # logging
  28027. if (length($from_str) > 100) { $from_str = substr($from_str,0,100).'[...]' }
  28028. if (!$allowed_hdrs || !$allowed_hdrs->{lc('DKIM-Signature')}) {
  28029. do_log(5, "dkim: inserting a DKIM-Signature header field disabled");
  28030. } elsif (!$msginfo->originating) {
  28031. do_log(5, "dkim: not signing mail which is not originating from our site");
  28032. } elsif ($msginfo->is_in_contents_category(CC_VIRUS)) {
  28033. do_log(2, "dkim: not signing infected mail (from inside), From: %s",
  28034. $from_str);
  28035. } elsif ($msginfo->is_in_contents_category(CC_SPAM)) {
  28036. # it is prudent not to sign outgoing spam, otherwise an attacker may be
  28037. # able to replay a signed message, re-sending it to other recipients
  28038. # in bulk directly from botnets
  28039. do_log(2, "dkim: not signing spam (from inside), From: %s", $from_str);
  28040. } elsif ($msginfo->is_in_contents_category(CC_SPAMMY)) {
  28041. do_log(2, "dkim: not signing suspected spam (from inside), From: %s",
  28042. $from_str);
  28043. } else {
  28044. # Choose a signing key based on the first match on the following
  28045. # addresses (in this order): 2822.From, followed by 2822.Resent-From and
  28046. # 2822.Resent-Sender address pairs traversed top-down by resent blocks,
  28047. # followed by 2822.Sender and 2821.mail_from. We choose to look up
  28048. # a From first, as it generates an author domain signature, but the
  28049. # search order on remaining entries is admittedly unusual.
  28050. # Btw, dkim-milter uses the following search order:
  28051. # Resent-Sender, Resent-From, Sender, From.
  28052. # Only a signature based on 2822.From is considered an author domain
  28053. # signature, others are just third-party signatures and have no more
  28054. # merit than any other third-party signature according to RFC 4871.
  28055. #
  28056. my $rf = $msginfo->rfc2822_resent_from;
  28057. my $rs = $msginfo->rfc2822_resent_sender;
  28058. my(@rfc2822_resent_from, @rfc2822_resent_sender);
  28059. @rfc2822_resent_from = @$rf if defined $rf;
  28060. @rfc2822_resent_sender = @$rs if defined $rs;
  28061. my(@search_list); # collects candidate addresses for choosing a signing key
  28062. # author addresses go first (typically exactly one, but possibly more)
  28063. push(@search_list, map([$_,'From'], @rfc2822_from));
  28064. # merge Resent-From and Resent-Sender addresses by resent blocks, top-down;
  28065. # a merge is simplified by the fact that there is an equal number of
  28066. # resent blocks in @rfc2822_resent_from and @rfc2822_resent_sender lists
  28067. while (@rfc2822_resent_from || @rfc2822_resent_sender) {
  28068. # for each resent block
  28069. while (@rfc2822_resent_from) {
  28070. my $addr = shift(@rfc2822_resent_from);
  28071. last if !defined $addr; # undef delimits resent blocks
  28072. push(@search_list, [$addr, 'Resent-From']);
  28073. }
  28074. while (@rfc2822_resent_sender) {
  28075. my $addr = shift(@rfc2822_resent_sender);
  28076. last if !defined $addr; # undef delimits resent blocks
  28077. push(@search_list, [$addr, 'Resent-Sender']);
  28078. }
  28079. }
  28080. push(@search_list, [$msginfo->rfc2822_sender, 'Sender']);
  28081. push(@search_list, [$msginfo->sender, 'mail_from']);
  28082. { # remove duplicates and empty addresses
  28083. my(%addr_seen);
  28084. @search_list =
  28085. grep { my($a,$src) = @$_; defined $a && $a ne '' && !$addr_seen{$a}++ }
  28086. @search_list;
  28087. }
  28088. ll(2) && do_log(2, "dkim: candidate originators: %s",
  28089. join(", ", map($_->[1].':'.qquote_rfc2821_local($_->[0]),
  28090. @search_list)));
  28091. # dkim_signwith_sd() may provide a ref to a pair [selector,domain] - if
  28092. # available (e.g. by a custom hook), it will force signing with a private
  28093. # key associated with this selector and domain, otherwise we fall back
  28094. # to consulting an external service if available, or else we use our
  28095. # built-in algorithm for choosing a selector & domain and their associated
  28096. # signing key
  28097. #
  28098. my $sd_pair = $msginfo->dkim_signwith_sd;
  28099. if (ref($sd_pair) eq 'ARRAY') {
  28100. my($s,$d) = @$sd_pair;
  28101. if (defined $s && $s ne '' && defined $d && $d ne '') {
  28102. do_log(5, "dkim: dkim_signwith_sd presets d=%s, s=%s", $d,$s);
  28103. $sig_options{s} = $s; $sig_options{d} = $d;
  28104. }
  28105. }
  28106. my $dkim_signing_service = c('dkim_signing_service');
  28107. if (defined $dkim_signing_service && $dkim_signing_service ne '') {
  28108. # try the signing service: it should provide an 's' and 'd' if it has
  28109. # a suitable signing key available, and/or may supply signing options,
  28110. # overriding the defaults set so far
  28111. my $sig_opt_ref;
  28112. ($sig_opt_ref, $chosen_addr_src, $chosen_addr) =
  28113. let_signing_service_choose($dkim_signing_service,
  28114. $msginfo, \@search_list, undef);
  28115. if ($sig_opt_ref) { # merge returned signature options with ours
  28116. while (my($k,$v) = each(%$sig_opt_ref))
  28117. { $sig_options{$k} = $v if defined $v }
  28118. }
  28119. }
  28120. my $sobm = ca('dkim_signature_options_bysender_maps');
  28121. # last resort: fall back to our local configuration settings
  28122. for my $pair (@search_list) {
  28123. my($addr,$addr_src) = @$pair;
  28124. my($addr_localpart,$addr_domain) = split_address($addr);
  28125. $addr_domain = lc($addr_domain);
  28126. # fetch a list of hashes from all entries matching the address
  28127. my($dkim_options_ref,$mk_ref);
  28128. ($dkim_options_ref,$mk_ref) = lookup2(1,$addr,$sobm) if $sobm && @$sobm;
  28129. $dkim_options_ref = [] if !defined $dkim_options_ref;
  28130. # signature options (parenthesized options are set automatically):
  28131. # (v), a, (b), (bh), c, d, (h), i, (l), q, s, (t), x, (z)
  28132. # place a catchall default at the end of the list of options;
  28133. push(@$dkim_options_ref, { c => 'relaxed/simple', a => 'rsa-sha256' });
  28134. # start each iteration with the same set of options collected so far
  28135. my(%tmp_sig_options) = %sig_options;
  28136. # traverse list of hashes from specific to general, first match wins
  28137. for my $opts_hash_ref (@$dkim_options_ref) {
  28138. next if ref $opts_hash_ref ne 'HASH'; # just in case
  28139. while (my($k,$v) = each(%$opts_hash_ref)) { # for each entry in a hash
  28140. $tmp_sig_options{$k} = $v if !exists $tmp_sig_options{$k};
  28141. }
  28142. }
  28143. # a default for a signing domain is a domain of each tried address
  28144. if (!exists($tmp_sig_options{d}))
  28145. { my $d = $addr_domain; $d =~ s/^\@//; $tmp_sig_options{d} = $d }
  28146. push(@tried_domains, $tmp_sig_options{d});
  28147. ll(5) && do_log(5, "dkim: signature options for %s(%s): %s",
  28148. $addr, $addr_src,
  28149. join('; ', map($_.'='.$tmp_sig_options{$_},
  28150. keys %tmp_sig_options)));
  28151. # find a private key associated with a signing domain and selector,
  28152. # and meeting constraints
  28153. %key_options = get_dkim_key(%tmp_sig_options)
  28154. if defined $tmp_sig_options{d} && $tmp_sig_options{d} ne '';
  28155. # my(@domain_path); # host.sub.example.com sub.example.com example.com com
  28156. # $addr_domain =~ s/^\@//; $addr_domain =~ s/\.\z//;
  28157. # if ($addr_domain !~ /\[/) { # don't split address literals
  28158. # for (my $d=$addr_domain; $d ne ''; $d =~ s/^[^.]*(?:\.|\z)//s)
  28159. # { push(@domain_path,$d) }
  28160. # }
  28161. # for my $d (@domain_path) {
  28162. # $tmp_sig_options{d} = $d;
  28163. # %key_options = get_dkim_key(%tmp_sig_options);
  28164. # last if defined $key_options{key};
  28165. # }
  28166. my $key = $key_options{key};
  28167. if (defined $key && $key ne '') { # found; copy the key and its options
  28168. $tmp_sig_options{key} = $key;
  28169. $tmp_sig_options{s} = $key_options{selector};
  28170. $chosen_addr = $addr; $chosen_addr_src = $addr_src;
  28171. # merge the just collected signature options into the final set
  28172. while (my($k,$v) = each(%tmp_sig_options))
  28173. { $sig_options{$k} = $v if defined $v }
  28174. last;
  28175. }
  28176. }
  28177. # provide defaults for 'c' and 'a' tags if missing
  28178. $sig_options{c} = 'relaxed/simple' if !exists $sig_options{c};
  28179. $sig_options{a} = 'rsa-sha256' if !exists $sig_options{a};
  28180. # prepare for a second stage of using an external signing service:
  28181. # when we do have a 's' and 'd', thus uniquely identifying a signing key,
  28182. # but do not have a key ourselves, we'll provide a callback routine
  28183. # in place of a key object so that Mail::DKIM will call it at the time
  28184. # of signing, and our routine will consult a remote signing service
  28185. #
  28186. if (!defined $sig_options{key} &&
  28187. defined $dkim_signing_service && $dkim_signing_service ne '' &&
  28188. defined $sig_options{d} && $sig_options{d} ne '' &&
  28189. defined $sig_options{s} && $sig_options{s} ne '') {
  28190. my $s = $sig_options{s}; my $d = $sig_options{d};
  28191. # let Mail::DKIM use our custom code for signing (pref. 0.38 or later)
  28192. $key_options{key} = Amavis::DKIM::CustomSigner->new(
  28193. CustomSigner => \&remote_signer, MsgInfo => $msginfo,
  28194. Selector => $s, Domain => $d, Server => $dkim_signing_service);
  28195. $key_options{selector} = $s; $key_options{domain} = $d;
  28196. $sig_options{key} = $key_options{key};
  28197. }
  28198. if (!defined $sig_options{d} || $sig_options{d} eq '') {
  28199. do_log(2, "dkim: not signing, empty signing domain, From: %s",$from_str);
  28200. } elsif (!defined $sig_options{key} || $sig_options{key} eq '') {
  28201. do_log(2, "dkim: not signing, no applicable private key for domains %s,".
  28202. " s=%s, From: %s",
  28203. join(", ",@tried_domains), $sig_options{s}, $from_str);
  28204. } else {
  28205. # copy key's options to signature options for convenience
  28206. for (keys %key_options)
  28207. { $sig_options{'KEY.'.$_} = $key_options{$_} if /^[ghknst]\z/ }
  28208. $sig_options{'KEY.key_ind'} = $key_options{key_ind};
  28209. # check matching of identity to a signing domain or provide a default;
  28210. # presence of a t=s flag in a public key RR prohibits subdomains in i
  28211. my $key_allows_subdomains =
  28212. grep($_ eq 's', split(/:/,$sig_options{'KEY.t'})) ? 0 : 1;
  28213. if (defined $sig_options{i}) { # explicitly given, possibly empty
  28214. # have mercy: provide a leading '@' if missing
  28215. $sig_options{i} = '@'.$sig_options{i} if $sig_options{i} ne '' &&
  28216. $sig_options{i} !~ /\@/;
  28217. } elsif (!$key_allows_subdomains) {
  28218. # we have no other choice but to keep it at its default @d
  28219. } else { # the public key record permits subdomains
  28220. # provide default for i in a form of a sender's domain
  28221. local($1);
  28222. if ($chosen_addr =~ /\@([^\@]*)\z/) {
  28223. my $identity_domain = lc($1);
  28224. if ($identity_domain =~ /.\.\Q$sig_options{d}\E\z/si) {
  28225. $sig_options{i} = '@'.$identity_domain;
  28226. do_log(5, "dkim: identity defaults to %s", $sig_options{i});
  28227. }
  28228. }
  28229. }
  28230. if (!defined $sig_options{i} || $sig_options{i} eq '') {
  28231. $do_sign = 1; # just sign, don't bother with i
  28232. } else { # check if the requested i is compatible with d
  28233. local($1);
  28234. my $identity_domain = $sig_options{i} =~ /\@([^\@]*)\z/ ? $1 : '';
  28235. if (!$key_allows_subdomains &&
  28236. lc($identity_domain) ne lc($sig_options{d})) {
  28237. do_log(2, "dkim: not signing, identity domain %s not the same as ".
  28238. "a signing domain %s, flags t=%s, From: %s",
  28239. $sig_options{i}, $sig_options{d}, $sig_options{'KEY.t'},
  28240. $from_str);
  28241. } elsif ($key_allows_subdomains &&
  28242. $identity_domain !~ /(?:^|\.)\Q$sig_options{d}\E\z/i) {
  28243. do_log(2, "dkim: not signing, identity %s not a subdomain of %s, ".
  28244. "From: %s", $sig_options{i}, $sig_options{d}, $from_str);
  28245. } else {
  28246. $do_sign = 1;
  28247. }
  28248. }
  28249. }
  28250. }
  28251. if ($do_sign) { # avoid adding same signature on multiple passes through MTA
  28252. my $sigs_ref = $msginfo->dkim_signatures_valid;
  28253. if ($sigs_ref) {
  28254. for my $sig (@$sigs_ref) {
  28255. if ( lc($sig_options{d}) eq lc($sig->domain) &&
  28256. (!defined $sig_options{i} || $sig_options{i} eq $sig->identity)) {
  28257. do_log(2, "dkim: not signing, already signed by domain %s, ".
  28258. "From: %s", $sig_options{d}, $from_str);
  28259. $do_sign = 0;
  28260. }
  28261. }
  28262. }
  28263. }
  28264. if ($do_sign) {
  28265. # relative expiration time
  28266. $sig_options{x} = $msginfo->rx_time + $sig_options{ttl}
  28267. if defined $sig_options{ttl} && $sig_options{ttl} > 0;
  28268. # remove redundant options with RFC 4871 -default values
  28269. for my $k (keys %sig_options) { delete $sig_options{$k} if !defined $k }
  28270. delete $sig_options{i} if lc($sig_options{i}) eq '@'.lc($sig_options{d});
  28271. delete $sig_options{c} if $sig_options{c} eq 'simple/simple' ||
  28272. $sig_options{c} eq 'simple';
  28273. delete $sig_options{q} if $sig_options{q} eq 'dns/txt';
  28274. if (ref $callback eq 'CODE') { &$callback($msginfo,\%sig_options) }
  28275. if (ll(2)) {
  28276. my $opts = join(', ', map($_ eq 'key' ? () : ($_.'=>'.$sig_options{$_}),
  28277. sort keys %sig_options));
  28278. do_log(2,"dkim: signing (%s), From: %s (%s:%s), %s",
  28279. grep(/\@\Q$sig_options{d}\E\z/si, @rfc2822_from) ? 'author'
  28280. : '3rd-party',
  28281. $from_str, $chosen_addr_src, qquote_rfc2821_local($chosen_addr),
  28282. $opts);
  28283. }
  28284. my $key = $sig_options{key};
  28285. if (UNIVERSAL::isa($key,'Crypt::OpenSSL::RSA')) {
  28286. # my $pkcs1 = $key->get_private_key_string; # most compact
  28287. # $pkcs1 =~ s/^---.*?---(?:\r?\n|\z)//gm; $pkcs1 =~ tr/\r\n//d;
  28288. # $key = Mail::DKIM::PrivateKey->load(Data => $pkcs1);
  28289. $key = Mail::DKIM::PrivateKey->load(Cork => $key); # avail since 0.31
  28290. } elsif (ref $key) {
  28291. # already a Mail::DKIM::PrivateKey or Amavis::DKIM::CustomSigner object
  28292. } else {
  28293. $key = Mail::DKIM::PrivateKey->load(File => $key); # read from a file
  28294. }
  28295. # Sendmail milter interface does not provide a just-generated Received
  28296. # header field to milters. Milters therefore need to fabricate a pseudo
  28297. # Received header field in order to provide client IP address to a filter.
  28298. # Unfortunately it is not possible to reliably fabricate a header field
  28299. # which will exactly match the later-inserted one, so we must not sign
  28300. # it to avoid a likely possibility of a signature being invalidated.
  28301. my $conn = $msginfo->conn_obj;
  28302. my $appl_proto = !$conn ? undef : $conn->appl_proto;
  28303. my $skip_topmost_received = defined($appl_proto) &&
  28304. ($appl_proto eq 'AM.PDP' || $appl_proto eq 'AM.CL');
  28305. my $policyfn = sub {
  28306. my $dkim = shift;
  28307. my $signed_header_fields_ref = cr('signed_header_fields') || {};
  28308. my $hfn = $dkim->{header_field_names};
  28309. my(@field_names_to_be_signed);
  28310. #
  28311. # when $signed_header_fields_ref->{$nm} is greater than 1 it indicates
  28312. # that one surplus occurrence of a header filed name in an 'h' tag
  28313. # should be inserted, consequently prohibiting further instances of
  28314. # such header field to be added to a message header section without
  28315. # breaking a signature; useful for example for a From and Subject
  28316. #
  28317. if ($hfn) {
  28318. my(%hfn_cnt);
  28319. $hfn_cnt{lc $_}++ for @$hfn;
  28320. for (@$hfn) {
  28321. my $nm = lc($_);
  28322. push(@field_names_to_be_signed, $nm); $hfn_cnt{$nm}--;
  28323. if (!$hfn_cnt{$nm} && $signed_header_fields_ref->{$nm} > 1) {
  28324. # causes signing one additional null occurrence of a header field
  28325. push(@field_names_to_be_signed, $nm);
  28326. }
  28327. }
  28328. }
  28329. @field_names_to_be_signed =
  28330. grep($signed_header_fields_ref->{$_}, @field_names_to_be_signed);
  28331. if ($skip_topmost_received) { # don't sign topmost Received header field
  28332. for my $j (0..$#field_names_to_be_signed) {
  28333. if (lc($field_names_to_be_signed[$j]) eq 'received')
  28334. { splice(@field_names_to_be_signed,$j,1); last }
  28335. }
  28336. }
  28337. my $expiration;
  28338. if (defined $sig_options{x}) {
  28339. $expiration = $sig_options{x};
  28340. my $j = int($expiration);
  28341. $expiration = $expiration > $j ? $j+1 : $j; # ceiling
  28342. }
  28343. $dkim->add_signature( Mail::DKIM::Signature->new(
  28344. Selector => $sig_options{s},
  28345. Domain => $sig_options{d},
  28346. Timestamp => int($msginfo->rx_time), # floor
  28347. Headers => join(':', reverse @field_names_to_be_signed),
  28348. Key => $key,
  28349. !defined $sig_options{c} ? () : (Method => $sig_options{c}),
  28350. !defined $sig_options{a} ? () : (Algorithm => $sig_options{a}),
  28351. !defined $sig_options{q} ? () : (Query => $sig_options{q}),
  28352. !defined $sig_options{i} ? () : (Identity => $sig_options{i}),
  28353. !defined $expiration ? () : (Expiration => $expiration), # ceiling
  28354. ));
  28355. undef;
  28356. }; # end sub
  28357. my $dkim_wrapper;
  28358. eval {
  28359. my $dkim_signer = Mail::DKIM::Signer->new(Policy => $policyfn);
  28360. $dkim_signer or die "Could not create a Mail::DKIM::Signer object\n";
  28361. #
  28362. # NOTE: dkim wrapper will strip bare CR before signing, which suits
  28363. # forwarding by SMTP which does the same; with other forwarding methods
  28364. # such as a pipe or milter, bare CRs in a message may break signatures
  28365. #
  28366. # feeding mail to a DKIM signer
  28367. $dkim_wrapper = Amavis::Out::SMTP->new_dkim_wrapper($dkim_signer,1);
  28368. my $msg = $msginfo->mail_text; # a file handle or a MIME::Entity object
  28369. my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy?
  28370. $msg = $msg_str_ref if ref $msg_str_ref;
  28371. my $hdr_edits = $msginfo->header_edits;
  28372. $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
  28373. my($received_cnt,$file_position) =
  28374. $hdr_edits->write_header($msginfo,$dkim_wrapper,!$initial_submission);
  28375. if (!defined $msg) {
  28376. # empty mail
  28377. } elsif (ref $msg eq 'SCALAR') {
  28378. # do it in chunks, saves memory, cache friendly
  28379. while ($file_position < length($$msg)) {
  28380. $dkim_wrapper->print(substr($$msg,$file_position,16384))
  28381. or die "Can't write to dkim signer: $!";
  28382. $file_position += 16384; # may overshoot, no problem
  28383. }
  28384. } elsif ($msg->isa('MIME::Entity')) {
  28385. $msg->print_body($dkim_wrapper);
  28386. } else {
  28387. my($nbytes,$buff);
  28388. while (($nbytes = $msg->read($buff,16384)) > 0) {
  28389. $dkim_wrapper->print($buff) or die "Can't write to dkim signer: $!";
  28390. }
  28391. defined $nbytes or die "Error reading: $!";
  28392. }
  28393. $dkim_wrapper->close or die "Can't close dkim wrapper: $!";
  28394. undef $dkim_wrapper;
  28395. $dkim_signer->CLOSE or die "Can't close dkim signer: $!";
  28396. @signatures = $dkim_signer->signatures;
  28397. undef $dkim_signer;
  28398. 1;
  28399. } or do {
  28400. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  28401. do_log(0, "dkim: signing error: %s", $eval_stat);
  28402. };
  28403. if (defined $dkim_wrapper) { $dkim_wrapper->close } # ignoring status
  28404. section_time('fwd-data-dkim');
  28405. }
  28406. # signatures must have all the required tags: d, s, b, bh; check to make sure
  28407. # if (ll(5)) { do_log(5, "dkim: %s", $_->as_string) for @signatures }
  28408. my(@sane_signatures);
  28409. for my $s (@signatures) {
  28410. my(@missing);
  28411. for my $pair ( ['d', $s->domain], ['s', $s->selector],
  28412. ['b', $s->data], ['bh', $s->body_hash] ) {
  28413. my($tag,$val) = @$pair;
  28414. push(@missing,$tag) if !defined($val) || $val eq '';
  28415. }
  28416. if (!@missing) {
  28417. push(@sane_signatures, $s);
  28418. # remember just the last one (typically the only one)
  28419. $msginfo->dkim_signwith_sd( [$s->selector, $s->domain] );
  28420. } else {
  28421. do_log(2, "dkim: signature is missing tag %s, skipping: %s",
  28422. join(',',@missing), $s->as_string);
  28423. }
  28424. }
  28425. @sane_signatures;
  28426. }
  28427. # Prepare Authentication-Results header fields according to RFC 5451
  28428. # and RFC 6008. The RFC 5617 (ADSP) added 'dkim-adsp' to the IANA
  28429. # Authentication Method Name Registry as used with Authentication-Results,
  28430. # but this is not yet implemented here.
  28431. #
  28432. sub generate_authentication_results($;$$) {
  28433. my($msginfo,$allow_none,$sigs_ref) = @_;
  28434. $sigs_ref = $msginfo->dkim_signatures_all if @_ < 3; # for all by default
  28435. my $authservid = c('myauthservid');
  28436. $authservid = c('myhostname') if !defined $authservid || $authservid eq '';
  28437. # note that RFC 5451 declares A-R header field as structured, which is why
  28438. # we are inserting a \n into top-level locations suitable for folding,
  28439. # and let sub hdr() choose suitable folding points
  28440. my(@results, %all_b, %all_b_valid, %all_b_8);
  28441. my($sig_cnt_dk, $sig_cnt_dkim, $result_str) = (0, 0, '');
  28442. for my $sig (!$sigs_ref ? () : @$sigs_ref) { # first pass
  28443. my($sig_result, $details, $str);
  28444. $sig_result = $sig->result;
  28445. if (defined $sig_result) {
  28446. $sig_result = lc $sig_result;
  28447. } else {
  28448. ($sig_result, $details) = ('pass', 'just generated, assumed good');
  28449. $sig->result($sig_result, $details);
  28450. }
  28451. my $valid = $sig_result eq 'pass';
  28452. if ($valid) {
  28453. my $expiration_time = $sig->expiration;
  28454. if (defined $expiration_time &&
  28455. $expiration_time =~ /^\d{1,10}\z/ &&
  28456. $msginfo->rx_time > $expiration_time) {
  28457. ($sig_result, $details) = ('fail', 'good, but expired');
  28458. $sig->result($sig_result, $details);
  28459. $valid = 0;
  28460. }
  28461. }
  28462. if ($sig->isa('Mail::DKIM::DkSignature')) { $sig_cnt_dk++ }
  28463. else { $sig_cnt_dkim++ };
  28464. my $b = $sig->data;
  28465. if (defined $b) {
  28466. $b =~ tr/ \t\n//d; # remove FWS, just in case
  28467. $all_b_8{substr($b,0,8)}++;
  28468. $all_b{$b}++;
  28469. $all_b_valid{$b}++ if $valid;
  28470. }
  28471. }
  28472. # RFC 5451 result: none, pass, fail, policy, neutral, temperror, permerror
  28473. # Mail::DKIM result: pass, fail, invalid, temperror, none
  28474. for my $sig (!$sigs_ref ? () : @$sigs_ref) { # second pass
  28475. my $result_val; # RFC 5451 result value
  28476. my $sig_result = lc $sig->result;
  28477. my $details = $sig->result_detail;
  28478. my $valid = $sig_result eq 'pass';
  28479. if ($valid) {
  28480. $result_val = 'pass';
  28481. } else {
  28482. # map a Mail::DKIM::Signature result into an RFC 5451 result value
  28483. $result_val = $sig_result eq 'temperror' ? 'temperror'
  28484. : $sig_result eq 'fail' ? 'fail'
  28485. : $sig_result eq 'invalid' ? 'neutral' : 'permerror';
  28486. }
  28487. my $d = $sig->domain;
  28488. $d = lc $d if defined $d;
  28489. my $str = '';
  28490. my $add_header_b; # RFC 6008, should we add a header.b for this signature?
  28491. my $key_size;
  28492. eval {
  28493. my $pk = $sig->get_public_key;
  28494. $key_size = $pk->cork->size * 8 if $pk && $pk->cork;
  28495. } or do {
  28496. undef $key_size;
  28497. do_log(5, "gen_auth_results: obtaining key size failed: %s", $@);
  28498. };
  28499. if ($sig->isa('Mail::DKIM::DkSignature')) {
  28500. $add_header_b = 1 if $sig_cnt_dk > 1;
  28501. my $rfc2822_sender = $msginfo->rfc2822_sender;
  28502. my $fm = $msginfo->rfc2822_from;
  28503. my(@rfc2822_from) = !defined($fm) ? () : ref $fm ? @$fm : $fm;
  28504. my $id = defined $d ? '@'.$d : '';
  28505. $str .= ";\n domainkeys=" . $result_val;
  28506. $str .= sprintf(' (%d-bit key)', $key_size) if defined $key_size;
  28507. if (defined $details && $details ne '' && lc $details ne lc $result_val){
  28508. local($1); # turn it into an RFC 2045 quoted-string
  28509. $details =~ s{([\000-\037\177"\\])}{\\$1}gs; # RFC 5322 qtext
  28510. $str .= "\n reason=\"$details\"";
  28511. }
  28512. if (@rfc2822_from &&
  28513. $rfc2822_from[0] =~ /(\@[^\@]*)\z/s && lc($1) eq $id) {
  28514. $str .= "\n header.from=" .
  28515. join(',', map(quote_rfc2821_local($_), @rfc2822_from));
  28516. }
  28517. if (defined($rfc2822_sender) &&
  28518. $rfc2822_sender =~ /(\@[^\@]*)\z/s && lc($1) eq $id) {
  28519. $str .= "\n header.sender=" . quote_rfc2821_local($rfc2822_sender);
  28520. }
  28521. } else { # a DKIM signature
  28522. $add_header_b = 1 if $sig_cnt_dkim > 1;
  28523. $str .= ";\n dkim=" . $result_val;
  28524. $str .= sprintf(' (%d-bit key)', $key_size) if defined $key_size;
  28525. if (defined $details && $details ne '' && lc $details ne lc $result_val){
  28526. local($1); # turn it into an RFC 2045 quoted-string
  28527. $details =~ s{([\000-\037\177"\\])}{\\$1}gs; # RFC 5322 qtext
  28528. $str .= "\n reason=\"$details\"";
  28529. }
  28530. }
  28531. $str .= "\n header.d=" . $d if defined $d;
  28532. my $b = $sig->data;
  28533. if (defined $b && $add_header_b) {
  28534. # RFC 6008: The value associated with this item in the header field
  28535. # MUST be at least the first eight characters of the digital signature
  28536. # (the "b=" tag from a DKIM-Signature) for which a result is being
  28537. # relayed, and MUST be long enough to be unique among the results
  28538. # being reported.
  28539. $b =~ tr/ \t\n//d; # remove FWS, just in case
  28540. if ($b !~ m{^ [A-Za-z0-9+/]+ =* \z}xs) { # ensure base64 syntax
  28541. do_log(2, "generate_AR: bad signature tag b=%s", $b);
  28542. } elsif ($all_b{$b} > 1 && $all_b_valid{$b} && !$valid) {
  28543. # exact duplicates: do not report invalid ones if at least one is valid
  28544. # RFC 6008 section 6.2.: a cautious implementation could discard
  28545. # the false negative in that instance.
  28546. do_log(2, "generate_AR: not reporting bad duplicates: %s", $b);
  28547. $str = ''; # ditch the report for this signature
  28548. } elsif ($all_b_8{$b} > $all_b{$b}) {
  28549. do_log(2, "generate_AR: not reporting b for collisions: %s", $b);
  28550. } else {
  28551. $str .= "\n header.b=" . substr($b,0,8);
  28552. }
  28553. }
  28554. $result_str .= $str;
  28555. }
  28556. # just provide a single A-R with all results combined
  28557. push(@results, $result_str) if $result_str ne '';
  28558. push(@results, ";\n dkim=none") if !@results && $allow_none;
  28559. $_ = sprintf("%s (%s)%s", $authservid, $myproduct_name, $_) for @results;
  28560. @results; # none, one, or more A-R header field bodies
  28561. }
  28562. # adjust spam score for each recipient so that the final spam score
  28563. # will be shifted towards a fixed score assigned to a signing domain (its
  28564. # 'reputation', as obtained through @signer_reputation_maps); the formula is:
  28565. # adjusted_spam_score = f*reputation + (1-f)*spam_score; 0 <= f <= 1
  28566. # which has the same semantics as auto_whitelist_factor in SpamAssassin AWL
  28567. #
  28568. sub adjust_score_by_signer_reputation($) {
  28569. my($msginfo) = @_;
  28570. my $reputation_factor = c('reputation_factor');
  28571. $reputation_factor = 0 if $reputation_factor < 0;
  28572. $reputation_factor = 1 if $reputation_factor > 1;
  28573. my $sigs_ref = $msginfo->dkim_signatures_valid;
  28574. if (defined $reputation_factor && $reputation_factor > 0 &&
  28575. $sigs_ref && @$sigs_ref) {
  28576. my($best_reputation_signer,$best_reputation_score);
  28577. my $srm = ca('signer_reputation_maps');
  28578. # walk through all valid signatures, find best (smallest) reputation value
  28579. for my $sig (@$sigs_ref) {
  28580. my $sdid = lc($sig->domain);
  28581. my($val,$key) = lookup2(0, '@'.$sdid, $srm);
  28582. if (defined $val &&
  28583. (!defined $best_reputation_score || $val < $best_reputation_score)) {
  28584. $best_reputation_signer = $sdid; $best_reputation_score = $val;
  28585. }
  28586. }
  28587. if (defined $best_reputation_score) {
  28588. my $ll = 2; # initial log level
  28589. for my $r (@{$msginfo->per_recip_data}) {
  28590. my $spam_level = $r->spam_level;
  28591. next if !defined $spam_level;
  28592. my $new_level = $reputation_factor * $best_reputation_score
  28593. + (1-$reputation_factor) * $spam_level;
  28594. $r->spam_level($new_level);
  28595. my $spam_tests = 'AM.DKIM_REPUT=' .
  28596. (0+sprintf("%.3f", $new_level-$spam_level));
  28597. if (!defined($r->spam_tests)) {
  28598. $r->spam_tests([ \$spam_tests ]);
  28599. } else {
  28600. unshift(@{$r->spam_tests}, \$spam_tests);
  28601. }
  28602. ll($ll) &&
  28603. do_log($ll, "dkim: score %.3f adjusted to %.3f due to reputation ".
  28604. "(%s) of a signer domain %s", $spam_level, $new_level,
  28605. $best_reputation_score, $best_reputation_signer);
  28606. $ll = 5; # reduce log clutter after the first recipient
  28607. }
  28608. }
  28609. }
  28610. }
  28611. # check if we have a valid author domain signature and do
  28612. # other DKIM pre-processing; called from collect_some_dkim()
  28613. #
  28614. sub collect_some_dkim_info($) {
  28615. my($msginfo) = @_;
  28616. my $rfc2822_sender = $msginfo->rfc2822_sender;
  28617. my(@rfc2822_from) = $msginfo->rfc2822_from;
  28618. # now that we have a parsed From, check if we have a valid
  28619. # author domain signature and do other DKIM pre-processing
  28620. my(@bank_names, %bank_names, %bn_auth_already_queried);
  28621. my $atpbm = ca('author_to_policy_bank_maps');
  28622. my(@signatures_valid);
  28623. my $sigs_ref = $msginfo->dkim_signatures_all;
  28624. my $sig_ind = 0; # index of a signature in a signature array
  28625. for my $sig (!$sigs_ref ? () : @$sigs_ref) { # for each signature
  28626. my $valid = lc($sig->result) eq 'pass';
  28627. my($timestamp_age, $creation_time, $expiration_time);
  28628. if (!$sig->isa('Mail::DKIM::DkSignature')) {
  28629. $creation_time = $sig->timestamp; # method only implemented for DKIM sig
  28630. $timestamp_age = $msginfo->rx_time - $creation_time
  28631. if defined $creation_time && $creation_time =~ /^\d{1,10}\z/;
  28632. }
  28633. $expiration_time = $sig->expiration;
  28634. my $expired =
  28635. defined $expiration_time && $expiration_time =~ /^\d{1,10}\z/ &&
  28636. ($msginfo->rx_time > $expiration_time ||
  28637. ( defined $creation_time && $creation_time =~ /^\d{1,10}\z/ &&
  28638. $creation_time > $expiration_time )
  28639. );
  28640. my $sdid = lc($sig->domain);
  28641. # See if a signature matches address in any of the sender/author fields.
  28642. # In the absence of an explicit Sender header field, the first author
  28643. # acts as the 'agent responsible for the transmission of the message'.
  28644. my(@addr_list) = ($msginfo->sender,
  28645. defined $rfc2822_sender ? $rfc2822_sender : $rfc2822_from[0],
  28646. @rfc2822_from);
  28647. for my $addr (@addr_list) {
  28648. next if !defined $addr;
  28649. local($1); my $domain;
  28650. $domain = lc($1) if $addr =~ /\@([^\@]*)\z/s;
  28651. # turn addresses in @addr_list into booleans, representing match outcome
  28652. $addr = defined($domain) && $domain eq $sdid ? 1 : 0;
  28653. }
  28654. # # Label which header fields are covered by each signature;
  28655. # # doesn't work for old DomainKeys signatures where h may be missing
  28656. # # and where recurring header fields may only be listed once.
  28657. # # NOTE: currently unused and commented out
  28658. # { my(%field_counts);
  28659. # my(@signed_header_field_names) = map(lc($_), $sig->headerlist); # 'h' tag
  28660. # $field_counts{$_}++ for @signed_header_field_names;
  28661. # for (my $j=-1; ; $j--) { # walk through header fields, bottom-up
  28662. # my($f_ind,$fld) = $msginfo->get_header_field2(undef,$j);
  28663. # last if !defined $f_ind; # reached the top
  28664. # local $1;
  28665. # my $f_name = lc($1) if $fld =~ /^([^:]*?)[ \t]*:/s;
  28666. # if ($field_counts{$f_name} > 0) { # header field is covered by this sig
  28667. # $msginfo->header_field_signed_by($f_ind,$sig_ind); # store sig index
  28668. # $field_counts{$f_name}--;
  28669. # }
  28670. # }
  28671. # }
  28672. if ($valid && !$expired) {
  28673. push(@signatures_valid, $sig);
  28674. my $sig_domain = $sig->domain;
  28675. $sig_domain = '?' if !$sig_domain; # make sure it is true as a boolean
  28676. #
  28677. # note that only the author domain signature (based on RFC 2822.From)
  28678. # is a valid concept in ADSP; we are also using the same rules to match
  28679. # against RFC 2822.Sender and envelope sender address, but results are
  28680. # only of informational/curiosity interest and deeper significance
  28681. # must not be attributed to dkim_envsender_sig and dkim_sender_sig!
  28682. #
  28683. $msginfo->dkim_envsender_sig($sig_domain) if $addr_list[0];
  28684. $msginfo->dkim_sender_sig($sig_domain) if $addr_list[1];
  28685. $msginfo->dkim_author_sig($sig_domain)
  28686. if grep($_, @addr_list[2..$#addr_list]); # SDID matches addr
  28687. $msginfo->dkim_thirdparty_sig($sig_domain) if !$msginfo->dkim_author_sig;
  28688. if (@$atpbm) { # any author to policy bank name mappings?
  28689. for my $j (0..$#rfc2822_from) { # for each author (usually only one)
  28690. my $key = $rfc2822_from[$j];
  28691. # query key: as-is author address for author domain signatures, and
  28692. # author address with '/@signer-domain' appended for 3rd party sign.
  28693. # e.g.: 'user@example.com', 'user@sub.example.com/@example.org'
  28694. for my $opt ( ($addr_list[$j+2] ? '' : ()), '/@'.lc($sig->domain) ) {
  28695. next if $bn_auth_already_queried{$key.$opt};
  28696. my($result,$matchingkey) = lookup2(0,$key,$atpbm,
  28697. Label=>'AuthToPB', $opt eq '' ? () : (AppendStr=>$opt));
  28698. $bn_auth_already_queried{$key.$opt} = 1;
  28699. if ($result) {
  28700. if ($result eq '1') {
  28701. # a handy usability trick to supply a hardwired policy bank
  28702. # name when acl-style lookup table is used, which can only
  28703. # return a boolean (undef, 0, or 1)
  28704. $result = 'AUTHOR_APPROVED';
  28705. }
  28706. # $result is a list of policy banks as a comma-separated string
  28707. my(@pbn); # collect list of newly encountered policy bank names
  28708. for (map { my $s=$_; $s =~ s/^[ \t]+//; $s =~ s/[ \t]+\z//; $s }
  28709. split(/,/, $result)) {
  28710. next if $_ eq '' || $bank_names{$_};
  28711. push(@pbn,$_); $bank_names{$_} = 1;
  28712. }
  28713. if (@pbn) {
  28714. push(@bank_names,@pbn);
  28715. ll(2) && do_log(2, "dkim: policy bank %s by %s",
  28716. join(',',@pbn), $matchingkey);
  28717. }
  28718. }
  28719. }
  28720. }
  28721. }
  28722. }
  28723. if (ll(5)) {
  28724. my($pubkey,$eval_stat);
  28725. # Mail::DKIM >=0.31 caches result; it can die with "not available"
  28726. eval {
  28727. $pubkey = $sig->get_public_key; 1;
  28728. } or do {
  28729. $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  28730. };
  28731. if (defined $eval_stat) {
  28732. do_log(5, "dkim: public key s=%s d=%s, error: %s",
  28733. $sig->selector, $sig->domain, $eval_stat);
  28734. } elsif (!$pubkey) {
  28735. do_log(5, "dkim: no public key s=%s d=%s",$sig->selector,$sig->domain);
  28736. } else {
  28737. do_log(5, "dkim: public key s=%s d=%s%s", $sig->selector, $sig->domain,
  28738. join('',map { my $v = $pubkey->get_tag($_);
  28739. defined $v ? " $_=$v" : '' } qw(v g h k t s)));
  28740. }
  28741. }
  28742. ll(2) && do_log(2, "dkim: %s%s%s %s signature by d=%s, From: %s, ".
  28743. "a=%s, c=%s, s=%s, i=%s%s%s%s",
  28744. $valid ? 'VALID' : 'FAILED', $expired ? ', EXPIRED' : '',
  28745. $timestamp_age >= -1 ? ''
  28746. : ', IN_FUTURE:('.format_time_interval(-$timestamp_age).')',
  28747. join('+', (map($_ ? 'Author' : (), @addr_list[2..$#addr_list])),
  28748. $addr_list[1] ? 'Sender' : (),
  28749. $addr_list[0] ? 'MailFrom' : (),
  28750. !grep($_, @addr_list) ? 'third-party' : ()),
  28751. $sig->domain, join(", ", qquote_rfc2821_local(@rfc2822_from)),
  28752. $sig->algorithm, scalar($sig->canonicalization),
  28753. $sig->selector, $sig->identity,
  28754. !$msginfo->originating ? ''
  28755. : ', ORIG [' . $msginfo->client_addr . ']:' . $msginfo->client_port,
  28756. !defined($msginfo->is_mlist) ? '' : ", m.list(".$msginfo->is_mlist.")",
  28757. $valid ? '' : ', '.$sig->result_detail,
  28758. );
  28759. $sig_ind++;
  28760. }
  28761. if (@bank_names) {
  28762. # ignore nonexisting bank names
  28763. @bank_names = grep(defined $Amavis::policy_bank{$_},
  28764. unique_list(\@bank_names));
  28765. if (@bank_names) {
  28766. Amavis::load_policy_bank($_,$msginfo) for @bank_names;
  28767. $msginfo->originating(c('originating')); # may have changed
  28768. }
  28769. }
  28770. $msginfo->dkim_signatures_valid(\@signatures_valid) if @signatures_valid;
  28771. # if (ll(5) && $sig_ind > 0) {
  28772. # # show which header fields are covered by which signature
  28773. # for (my $j=0; ; $j++) {
  28774. # my($f_ind,$fld) = $msginfo->get_header_field2(undef,$j);
  28775. # last if !defined $f_ind;
  28776. # my(@sig_ind) = $msginfo->header_field_signed_by($f_ind);
  28777. # do_log(5, "dkim: %-5s %s.", !@sig_ind ? '' : '['.join(',',@sig_ind).']',
  28778. # substr($fld,0,54));
  28779. # }
  28780. # }
  28781. }
  28782. 1;
  28783. __DATA__
  28784. #
  28785. package Amavis::Tools;
  28786. use strict;
  28787. use re 'taint';
  28788. use warnings;
  28789. use warnings FATAL => qw(utf8 void);
  28790. no warnings 'uninitialized';
  28791. BEGIN {
  28792. require Exporter;
  28793. use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  28794. $VERSION = '2.316';
  28795. @ISA = qw(Exporter);
  28796. @EXPORT_OK = qw(&show_or_test_dkim_public_keys &generate_dkim_private_key
  28797. &convert_dkim_keys_file);
  28798. import Amavis::Conf qw(:platform c cr ca
  28799. @dkim_signing_keys_list @dkim_signing_keys_storage);
  28800. import Amavis::Util qw(untaint ll do_log);
  28801. import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp);
  28802. }
  28803. use subs @EXPORT_OK;
  28804. use Errno qw(ENOENT EACCES);
  28805. use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
  28806. use Crypt::OpenSSL::RSA ();
  28807. # Prints DNS TXT resource records for corresponding DKIM private keys (as
  28808. # previously declared by calls to dkim_key) in a format directly suitable
  28809. # for inclusion in DNS zone files. If an argument is provided the result is
  28810. # restricted to listed domains only, otherwise RR for all domains are shown.
  28811. # Note that a domain may have more than one RR: one RR for each selector.
  28812. #
  28813. # When a search argument is provided (even if '.'), the printed list is
  28814. # sorted according to reversed domain labels (e.g. com.example.sub.host),
  28815. # entries with the same domain are kept in original order. When there are
  28816. # no search arguments, the original order is retained.
  28817. #
  28818. sub show_or_test_dkim_public_keys($$) {
  28819. my($cmd,$args) = @_;
  28820. my(@seek_domains) = @$args; # when list is empty all domains are implied
  28821. my(@sort_list) = map { my $d = lc($dkim_signing_keys_list[$_]->{domain});
  28822. my $d_re = $dkim_signing_keys_list[$_]->{domain_re};
  28823. [$_, $d, $d_re, join('.',reverse split(/\./,$d,-1))] }
  28824. 0 .. $#dkim_signing_keys_list;
  28825. if (@seek_domains) { # sort only when there are any search arguments present
  28826. @sort_list = sort {$a->[3] cmp $b->[3] || $a->[0] <=> $b->[0]} @sort_list;
  28827. }
  28828. my $any = 0;
  28829. for my $e (@sort_list) {
  28830. my($j,$domain,$domain_re) = @$e; local($1);
  28831. next if @seek_domains &&
  28832. !grep { defined $domain_re ? lc($_) =~ /$domain_re/
  28833. : /^\.(.*)\z/s ?
  28834. $domain eq lc($1) || $domain =~ /(?:\.|\z)\Q$1\E\z/si
  28835. : $domain eq lc($_) } @seek_domains;
  28836. $any++;
  28837. my $key_opts = $dkim_signing_keys_list[$j];
  28838. if ($cmd eq 'testkeys' || $cmd eq 'testkey') {
  28839. test_dkim_key(%$key_opts);
  28840. } else {
  28841. my $key_storage_ind = $key_opts->{key_storage_ind};
  28842. my($key,$dev,$inode,$fname) =
  28843. @{ $dkim_signing_keys_storage[$key_storage_ind] };
  28844. my(@pub) = split(/\r?\n/, $key->get_public_key_x509_string);
  28845. @pub = grep(!/^---.*?---\z/ && !/^[ \t]*\z/, @pub);
  28846. my(@tags) = map($_.'='.$key_opts->{$_},
  28847. grep(defined $key_opts->{$_}, qw(v g h k s t n)));
  28848. printf("; key#%d, domain %s, %s\n",
  28849. $key_opts->{key_ind} + 1, $domain, $fname) if defined $fname;
  28850. printf("; CANNOT DECLARE A WILDCARDED LABEL IN DNS, ".
  28851. "AVOID OR EDIT MANUALLY!\n") if defined $key_opts->{domain_re};
  28852. printf("%s._domainkey.%s.\t%s TXT (%s)\n\n",
  28853. $key_opts->{selector}, $domain, '3600',
  28854. join('', map("\n" . ' "' . $_ . '"',
  28855. join('; ',@tags,'p='), @pub)) );
  28856. }
  28857. }
  28858. if (!@dkim_signing_keys_list) {
  28859. printf("No DKIM private keys declared in a config file.\n");
  28860. } elsif (!$any) {
  28861. printf("No DKIM private keys match the selection list.\n");
  28862. }
  28863. }
  28864. sub test_dkim_key(@) {
  28865. my(%key_options) = @_;
  28866. my $now = Time::HiRes::time;
  28867. my $key_storage_ind = $key_options{key_storage_ind};
  28868. my($key,$dev,$inode,$fname) =
  28869. @{ $dkim_signing_keys_storage[$key_storage_ind] };
  28870. if (UNIVERSAL::isa($key,'Crypt::OpenSSL::RSA')) {
  28871. $key = Mail::DKIM::PrivateKey->load(Cork => $key); # avail since 0.31
  28872. # my $pkcs1 = $key->get_private_key_string; # most compact
  28873. # $pkcs1 =~ s/^---.*?---(?:\r?\n|\z)//gm; $pkcs1 =~ tr/\r\n//d;
  28874. # $key = Mail::DKIM::PrivateKey->load(Data => $pkcs1);
  28875. }
  28876. my $policyfn = sub {
  28877. my $dkim = shift;
  28878. $dkim->add_signature( Mail::DKIM::Signature->new(
  28879. Selector => $key_options{selector}, Domain => $key_options{domain},
  28880. Method => 'simple/simple', Algorithm => 'rsa-sha256',
  28881. Timestamp => int($now), Expiration => int($now)+24*3600, Key => $key,
  28882. )); undef;
  28883. };
  28884. my $msg = sprintf(
  28885. "From: test\@%s\nMessage-ID: <123\@%s>\nDate: %s\nSubject: test\n\ntest\n",
  28886. $key_options{domain}, $key_options{domain}, rfc2822_timestamp($now));
  28887. $msg =~ s{\n}{\015\012}gs;
  28888. my(@gen_signatures, @read_signatures);
  28889. eval {
  28890. my $dkim_signer = Mail::DKIM::Signer->new(Policy => $policyfn);
  28891. $dkim_signer or die "Could not create a Mail::DKIM::Signer object";
  28892. $dkim_signer->PRINT($msg) or die "Can't write to dkim: $!";
  28893. $dkim_signer->CLOSE or die "Can't close dkim signer: $!";
  28894. @gen_signatures = $dkim_signer->signatures;
  28895. } or do {
  28896. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  28897. print STDERR "dkim signing failed: $eval_stat\n";
  28898. };
  28899. $msg = $_->as_string . "\015\012" . $msg for @gen_signatures;
  28900. eval {
  28901. my $dkim_verifier = Mail::DKIM::Verifier->new;
  28902. $dkim_verifier or die "Could not create a Mail::DKIM::Verifier object";
  28903. $dkim_verifier->PRINT($msg) or die "Can't write to dkim: $!";
  28904. $dkim_verifier->CLOSE or die "Can't close dkim_verifier: $!";
  28905. @read_signatures = $dkim_verifier->signatures;
  28906. } or do {
  28907. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  28908. print STDERR "dkim verification failed: $eval_stat\n";
  28909. };
  28910. # printf("%s\n", $fname) if defined $fname;
  28911. printf("TESTING#%d: %-33s => %s\n", $key_options{key_ind} + 1,
  28912. $_->selector . '._domainkey.' . $_->domain,
  28913. $_->result_detail) for @read_signatures;
  28914. }
  28915. sub generate_dkim_private_key(@) {
  28916. my($fname,$nbits) = @_;
  28917. my $fh;
  28918. eval {
  28919. $nbits = 1024 if !defined($nbits) || $nbits eq '';
  28920. $nbits =~ /^\d+\z/ or die "Number of bits in a key must be numeric\n";
  28921. $nbits >= 512 or die "Number of bits too small (suggested 768..1536)\n";
  28922. $nbits <= 4096 or die "Number of bits too large (suggested 768..1536)\n";
  28923. defined $fname && $fname ne '' or die "File name for a key not provided\n";
  28924. $fh = IO::File->new;
  28925. $fh->open(untaint($fname), O_CREAT|O_EXCL|O_RDWR, 0600)
  28926. or die "Can't create file \"$fname\": $!\n";
  28927. my $rsa = Crypt::OpenSSL::RSA->generate_key($nbits);
  28928. $fh->print($rsa->get_private_key_string)
  28929. or die "Error writing key to a file \"$fname\": $!\n";
  28930. $fh->close or die "Can't close file \"$fname\": $!\n";
  28931. undef $fh;
  28932. printf STDERR ("Private RSA key successfully written to file \"%s\" ".
  28933. "(%d bits, PEM format) \n", $fname,$nbits);
  28934. 1;
  28935. } or do {
  28936. my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  28937. $fh->close if defined $fh; # ignoring status
  28938. die "genrsa: $eval_stat\n";
  28939. }
  28940. }
  28941. # Reads a dkim-filter -compatible key specifications. From the dkim-filter
  28942. # man page: The keyfile should contain a set of lines of the form
  28943. # sender-pattern:signing-domain:keypath where sender-pattern is a pattern
  28944. # to match against message senders (with a special character "*" interpreted
  28945. # as "zero or more characters"), signing-domain is the domain to announce as
  28946. # the signing domain when generating signatures (or a '*', implying author's
  28947. # domain), and keypath is a path to the PEM-formatted private key to be used
  28948. # for signing messages which match the sender-pattern. The selector used in
  28949. # the signature will be the filename portion of keypath. A line starting
  28950. # with "/" is interpreted as a root directory for keys, meaning the keypath
  28951. # values after that line in the file are taken relative to that path. If a
  28952. # file referenced by keypath cannot be opened, the filter will try again by
  28953. # appending ".pem" and then ".private". '#'-delimited comments and blank
  28954. # lines are ignored.
  28955. #
  28956. sub convert_dkim_keys_file($) {
  28957. my($keysfile) = @_;
  28958. my $inp = IO::File->new;
  28959. $inp->open($keysfile,'<')
  28960. or die "dkim_key_file: Can't open file $keysfile for reading: $!";
  28961. my($basedir,@options,@opt_re,%domain_selectors); my $rn = 0; my $ln;
  28962. for ($! = 0; defined($ln=$inp->getline); $! = 0) {
  28963. chomp($ln); $rn++; local($1); my($selector,$key_fn);
  28964. if ($ln =~ /^ \s* (?: \# | \z)/xs) {
  28965. # skip empty and all-comment lines
  28966. } elsif ($ln =~ m{^/}) {
  28967. $basedir = $ln; $basedir .= '/' if $basedir !~ m{/\z};
  28968. } else {
  28969. my($sender_pattern,$signing_domain,$keypath) =
  28970. map { my $s = $_; $s =~ s/^\s+//; $s =~ s/\s+\z//; $s }
  28971. split(/:/, $ln, 3);
  28972. defined $sender_pattern && $sender_pattern ne ''
  28973. or die "Error in $keysfile, empty sender pattern, line $rn: $ln\n";
  28974. defined $keypath && $keypath ne '' || $signing_domain eq ''
  28975. or die "Error in $keysfile, empty file name field, line $rn: $ln\n";
  28976. $keypath = $basedir . $keypath if defined $basedir && $keypath !~ m{^/};
  28977. for my $ext ('', '.pem', '.private') {
  28978. my $errn = stat($keypath.$ext) ? 0 : 0+$!;
  28979. if ($errn != ENOENT) { $key_fn = $keypath.$ext; last }
  28980. }
  28981. defined $key_fn
  28982. or die "File $keypath does not exist, $keysfile line $rn: $ln\n";
  28983. $selector = lc($1) if $keypath =~ m{ (?: ^ | / ) ( [^/]+? )
  28984. (?: \.pem | \.private )? \z }xs;
  28985. # must convert sender pattern to unquoted form to match actual addresses
  28986. my $sender_domain;
  28987. if ($sender_pattern eq '*' || $sender_pattern eq '*@*') {
  28988. $sender_pattern = $sender_domain = '*';
  28989. } else {
  28990. my $sender_localpart;
  28991. ($sender_localpart, $sender_domain) =
  28992. Amavis::rfc2821_2822_Tools::split_address(
  28993. Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($sender_pattern));
  28994. $sender_domain =~ s/^\@//; $sender_domain = lc($sender_domain);
  28995. $sender_pattern = $sender_localpart . '@' . $sender_domain;
  28996. }
  28997. if ($signing_domain eq '*') { $signing_domain = $sender_domain }
  28998. $signing_domain = lc($signing_domain);
  28999. if ($signing_domain ne '' &&
  29000. !$domain_selectors{$signing_domain}{$selector}) {
  29001. # dkim_key($signing_domain,$selector,$key_fn); # declare a signing key
  29002. printf("dkim_key(%-18s %-12s '%s');\n",
  29003. "'".$signing_domain."',", "'".$selector."',", $key_fn);
  29004. $domain_selectors{$signing_domain}{$selector} = 1;
  29005. }
  29006. if ($signing_domain eq $sender_domain) { $signing_domain = '*' }
  29007. push(@options, [$sender_pattern, $signing_domain, $selector]);
  29008. }
  29009. }
  29010. defined $ln || $! == 0 or die "Error reading from $keysfile: $!";
  29011. $inp->close or die "Error closing $keysfile: $!";
  29012. #
  29013. # prepare by_sender signature options lookup table when non-default
  29014. # signing is required (e.g. third-party signatures)
  29015. #
  29016. my $in_options = 0;
  29017. for my $opt (@options) {
  29018. my($sender_pattern, $signing_domain, $selector) = @$opt;
  29019. if ($signing_domain eq '*') {
  29020. # implies author domain signature, no need for special options
  29021. } else {
  29022. $sender_pattern =~ s/\*{2,}/*/gs; # collapse successive wildcards
  29023. $sender_pattern =~ # '*' is a wildcard, quote the rest
  29024. s{ ([@#/.^$|*+?(){}\[\]\\]) }{ $1 eq '*' ? '.*' : '\\'.$1 }gex;
  29025. $sender_pattern = '^' . $sender_pattern . '\\z'; # implicit anchors
  29026. # remove trailing first, leading next, preferring /^.*\z/ -> /^/, not /\z/
  29027. $sender_pattern =~ s/\.\*\\z\z//s; # remove trailing anchor if redundant
  29028. $sender_pattern =~ s/^\^\.\*//s; # remove leading anchor if redundant
  29029. $sender_pattern = '(?:)' if $sender_pattern eq ''; # just in case
  29030. $signing_domain = undef if $signing_domain eq '';
  29031. $selector = undef if $selector eq '';
  29032. # case insensitive matching for compatibility with dkim-milter
  29033. push(@opt_re, [ qr/$sender_pattern/is =>
  29034. ( !defined($signing_domain) ||
  29035. keys(%{$domain_selectors{$signing_domain}})==1
  29036. ? { d => $signing_domain }
  29037. : { d => $signing_domain, s => $selector } ) ]);
  29038. if (!$in_options) {
  29039. printf("\n%s\n", '@dkim_signature_options_bysender_maps = (new_RE(');
  29040. $in_options = 1;
  29041. }
  29042. printf(" [ %-30s => { d=>%s%s} ],\n",
  29043. 'qr/' . $sender_pattern . '/is',
  29044. !defined($signing_domain) ? 'undef' : "'".$signing_domain."'",
  29045. !defined($signing_domain) ||
  29046. keys %{$domain_selectors{$signing_domain}} == 1 ? ''
  29047. : !defined($selector) ? ', s=>undef' : ", s=>'".$selector."'");
  29048. }
  29049. }
  29050. printf("%s\n", '));') if $in_options;
  29051. # use Data::Dump (); Data::Dump::dump(@opt_re);
  29052. # unshift(@dkim_signature_options_bysender_maps,
  29053. # Amavis::Lookup::RE->new(@opt_re)) if @opt_re;
  29054. }
  29055. 1;
  29056. __DATA__
  29057. #
  29058. # =============================================================================
  29059. # This text section governs how a main per-message amavisd-new log entry (at
  29060. # log level 0) is formed (config variable $log_short_templ). Empty disables it.
  29061. [?%#D|#|Passed #
  29062. [? [:ccat|major] |#
  29063. OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
  29064. UNCHECKED|BANNED (%F)|INFECTED (%V)] {[:actions_performed]}#
  29065. , [? %p ||%p ][?%a||[?%l||LOCAL ][:client_addr_port] ][?%e||\[%e\] ]%s -> [%D|,]#
  29066. [? %q ||, quarantine: %q]#
  29067. [? %Q ||, Queue-ID: %Q]#
  29068. [? %m ||, Message-ID: %m]#
  29069. [? %r ||, Resent-Message-ID: %r]#
  29070. [? %i ||, mail_id: %i]#
  29071. , Hits: [:SCORE]#
  29072. , size: %z#
  29073. [? [:partition_tag] ||, pt: [:partition_tag]]#
  29074. [~[:remote_mta_smtp_response]|["^$"]||[", queued_as: "]]\
  29075. [remote_mta_smtp_response|[~%x|["queued as ([0-9A-Za-z]+)$"]|["%1"]|["%0"]]|/]#
  29076. #, Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
  29077. #, From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
  29078. #[? %#T ||, Tests: \[[%T|,]\]]#
  29079. [? [:dkim|sig_sd] ||, dkim_sd=[:dkim|sig_sd]]#
  29080. [? [:dkim|newsig_sd] ||, dkim_new=[:dkim|newsig_sd]]#
  29081. , %y ms#
  29082. ]
  29083. [?%#O|#|Blocked #
  29084. [? [:ccat|major|blocking] |#
  29085. OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
  29086. UNCHECKED|BANNED (%F)|INFECTED (%V)] {[:actions_performed]}#
  29087. , [? %p ||%p ][?%a||[?%l||LOCAL ][:client_addr_port] ][?%e||\[%e\] ]%s -> [%O|,]#
  29088. [? %q ||, quarantine: %q]#
  29089. [? %Q ||, Queue-ID: %Q]#
  29090. [? %m ||, Message-ID: %m]#
  29091. [? %r ||, Resent-Message-ID: %r]#
  29092. [? %i ||, mail_id: %i]#
  29093. , Hits: [:SCORE]#
  29094. , size: %z#
  29095. [? [:partition_tag] ||, pt: [:partition_tag]]#
  29096. #, Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
  29097. #, From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
  29098. #[? %#T ||, Tests: \[[%T|,]\]]#
  29099. [? [:dkim|sig_sd] ||, dkim_sd=[:dkim|sig_sd]]#
  29100. [? [:dkim|newsig_sd] ||, dkim_new=[:dkim|newsig_sd]]#
  29101. , %y ms#
  29102. ]
  29103. __DATA__
  29104. #
  29105. # =============================================================================
  29106. # This text section governs how a verbose per-message amavisd-new log entry
  29107. # is formed (config variable $log_verbose_templ). An empty text will prevent
  29108. # a verbose log entry, multiline text will produce multiple log entries, one
  29109. # for each nonempty line. Syntax is explained in the README.customize file.
  29110. [?%#D|#|Passed #
  29111. [? [:ccat|major] |#
  29112. OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
  29113. UNCHECKED|BANNED (%F)|INFECTED (%V)] {[:actions_performed]}#
  29114. , [? %p ||%p ][?%a||[?%l||LOCAL ][:client_addr_port] ][?%e||\[%e\] ]%s -> [%D|,]#
  29115. [? [:tls_in] ||, tls: [:tls_in]]#
  29116. [? %q ||, quarantine: %q]#
  29117. [? %Q ||, Queue-ID: %Q]#
  29118. [? %m ||, Message-ID: %m]#
  29119. [? %r ||, Resent-Message-ID: %r]#
  29120. , mail_id: %i#
  29121. #, secret_id: [:secret_id]#
  29122. , b: [:substr|[:b64urlenc|[:body_digest]]|0|9]#
  29123. , Hits: [:SCORE]#
  29124. , size: %z#
  29125. [? [:partition_tag] ||, pt: [:partition_tag]]#
  29126. [~[:remote_mta_smtp_response]|["^$"]||[", queued_as: "]]\
  29127. [remote_mta_smtp_response|[~%x|["queued as ([0-9A-Za-z]+)$"]|["%1"]|["%0"]]|/]#
  29128. , Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
  29129. , From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
  29130. [? [:dkim|author] || (dkim:AUTHOR)]#
  29131. [? [:useragent|name] ||, [:useragent|name]: [:uquote|[:useragent|body]]]#
  29132. , helo=[:client_helo]#
  29133. [? %#T ||, Tests: \[[%T|,]\]]#
  29134. [? [:banning_rule_key] ||, b.key=[:banning_rule_key]]#
  29135. [? [:banning_rule_comment] ||, b.com=[:banning_rule_comment]]#
  29136. [? [:banning_rule_rhs] ||, b.rhs=[:banning_rule_rhs]]#
  29137. [? [:banned_parts_as_attr] ||, b.parts=[:banned_parts_as_attr]]#
  29138. [:supplementary_info|SCTYPE|, shortcircuit=%%s]#
  29139. [:supplementary_info|AUTOLEARN|, autolearn=%%s]#
  29140. [:supplementary_info|AUTOLEARNSCORE|, autolearnscore=%%s]#
  29141. [? [:supplementary_info|LANGUAGES] ||, languages=[:uquote|[:supplementary_info|LANGUAGES]]]#
  29142. [? [:supplementary_info|RELAYCOUNTRY] ||, relaycountry=[:uquote|[:supplementary_info|RELAYCOUNTRY]]]#
  29143. [? [:supplementary_info|ASN] ||, asn=[:uquote|[:supplementary_info|ASN] [:supplementary_info|ASNCIDR]]]#
  29144. #[? [:supplementary_info|DCCB] ||, dcc=[:supplementary_info|DCCB]:[:uquote|[:supplementary_info|DCCR]]]#
  29145. #[? [:supplementary_info|DCCREP] ||, dcc_rep=[:supplementary_info|DCCREP]]#
  29146. #[:supplementary_info|AWLSIGNERMEAN|, signer_avg=%%s]#
  29147. #[? [:dkim|domain] ||, dkim_d=[:dkim|domain]]#
  29148. [? [:dkim|identity] ||, dkim_i=[:dkim|identity]]#
  29149. [? [:dkim|sig_sd] ||, dkim_sd=[:dkim|sig_sd]]#
  29150. [? [:dkim|newsig_sd] ||, dkim_new=[:dkim|newsig_sd]]#
  29151. [? [:rusage|ru_maxrss] ||, rss=[:rusage|ru_maxrss]]#
  29152. , %y ms#
  29153. ]
  29154. [?%#O|#|Blocked #
  29155. [? [:ccat|major|blocking] |#
  29156. OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER-[:ccat|minor]|SPAMMY|SPAM|\
  29157. UNCHECKED|BANNED (%F)|INFECTED (%V)] {[:actions_performed]}#
  29158. , [? %p ||%p ][?%a||[?%l||LOCAL ][:client_addr_port] ][?%e||\[%e\] ]%s -> [%O|,]#
  29159. [? [:tls_in] ||, tls: [:tls_in]]#
  29160. [? %q ||, quarantine: %q]#
  29161. [? %Q ||, Queue-ID: %Q]#
  29162. [? %m ||, Message-ID: %m]#
  29163. [? %r ||, Resent-Message-ID: %r]#
  29164. , mail_id: %i#
  29165. #, secret_id: [:secret_id]#
  29166. , b: [:substr|[:b64urlenc|[:body_digest]]|0|9]#
  29167. , Hits: [:SCORE]#
  29168. , size: %z#
  29169. [? [:partition_tag] ||, pt: [:partition_tag]]#
  29170. , Subject: [:dquote|[:mime2utf8|[:header_field|Subject]|100|1]]#
  29171. , From: [:uquote|[:mime2utf8|[:header_field|From]|100|1]]#
  29172. [? [:dkim|author] || (dkim:AUTHOR)]#
  29173. [? [:useragent|name] ||, [:useragent|name]: [:uquote|[:useragent|body]]]#
  29174. , helo=[:client_helo]#
  29175. [? %#T ||, Tests: \[[%T|,]\]]#
  29176. [? [:banning_rule_key] ||, b.key=[:banning_rule_key]]#
  29177. [? [:banning_rule_comment] ||, b.com=[:banning_rule_comment]]#
  29178. [? [:banning_rule_rhs] ||, b.rhs=[:banning_rule_rhs]]#
  29179. [? [:banned_parts_as_attr] ||, b.parts=[:banned_parts_as_attr]]#
  29180. [:supplementary_info|SCTYPE|, shortcircuit=%%s]#
  29181. [:supplementary_info|AUTOLEARN|, autolearn=%%s]#
  29182. [:supplementary_info|AUTOLEARNSCORE|, autolearnscore=%%s]#
  29183. [? [:supplementary_info|LANGUAGES] ||, languages=[:uquote|[:supplementary_info|LANGUAGES]]]#
  29184. [? [:supplementary_info|RELAYCOUNTRY] ||, relaycountry=[:uquote|[:supplementary_info|RELAYCOUNTRY]]]#
  29185. [? [:supplementary_info|ASN] ||, asn=[:uquote|[:supplementary_info|ASN] [:supplementary_info|ASNCIDR]]]#
  29186. #[? [:supplementary_info|DCCB] ||, dcc=[:supplementary_info|DCCB]:[:uquote|[:supplementary_info|DCCR]]]#
  29187. #[? [:supplementary_info|DCCREP] ||, dcc_rep=[:supplementary_info|DCCREP]]#
  29188. #[:supplementary_info|AWLSIGNERMEAN|, signer_avg=%%s]#
  29189. #[? [:dkim|domain] ||, dkim_d=[:dkim|domain]]#
  29190. [? [:dkim|identity] ||, dkim_i=[:dkim|identity]]#
  29191. [? [:dkim|sig_sd] ||, dkim_sd=[:dkim|sig_sd]]#
  29192. [? [:dkim|newsig_sd] ||, dkim_new=[:dkim|newsig_sd]]#
  29193. [? [:rusage|ru_maxrss] ||, rss=[:rusage|ru_maxrss]]#
  29194. , %y ms#
  29195. ]
  29196. __DATA__
  29197. #
  29198. # =============================================================================
  29199. # This text section governs how a main per-recipient amavisd-new log entry
  29200. # is formed (config variable $log_recip_templ). An empty text will prevent a
  29201. # log entry, multi-line text will produce multiple log entries, one for each
  29202. # nonempty line. Macro %. might be useful, it counts recipients starting
  29203. # from 1. Syntax is explained in the README.customize file.
  29204. # Long header fields will be automatically wrapped by the program.
  29205. #
  29206. [?%#D|#|Passed #
  29207. #([:ccat|name|main]) #
  29208. [? [:ccat|major] |OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER|SPAMMY|SPAM|\
  29209. UNCHECKED|BANNED (%F)|INFECTED (%V)]#
  29210. , %s -> [%D|,], Hits: %c#
  29211. , tag=[:tag_level], tag2=[:tag2_level], kill=[:kill_level]#
  29212. [~[:remote_mta_smtp_response]|["^$"]||\
  29213. ["queued as ([0-9A-Za-z]+)"]|[", queued_as: %1"]|[", fwd: %0"]]#
  29214. , %0/%1/%2/%k#
  29215. ]
  29216. [?%#O|#|Blocked #
  29217. #([:ccat|name|blocking]) #
  29218. [? [:ccat|major|blocking] |#
  29219. OTHER|CLEAN|MTA-BLOCKED|OVERSIZED|BAD-HEADER|SPAMMY|SPAM|\
  29220. UNCHECKED|BANNED (%F)|INFECTED (%V)]#
  29221. , %s -> [%O|,], Hits: %c#
  29222. , tag=[:tag_level], tag2=[:tag2_level], kill=[:kill_level]#
  29223. , %0/%1/%2/%k#
  29224. ]
  29225. __DATA__
  29226. #
  29227. # =============================================================================
  29228. # This is a template for (neutral: non-virus, non-spam, non-banned)
  29229. # DELIVERY STATUS NOTIFICATIONS to sender.
  29230. # For syntax and customization instructions see README.customize.
  29231. # The From, To and Date header fields will be provided automatically.
  29232. # Long header fields will be automatically wrapped by the program.
  29233. #
  29234. Subject: [?%#D|Undeliverable mail|Delivery status notification]\
  29235. [? [:ccat|major] |||, MTA-BLOCKED\
  29236. |, OVERSIZED message\
  29237. |, invalid header section[=explain_badh|1]\
  29238. [?[:ccat|minor]||: bad MIME|: unencoded 8-bit character\
  29239. |: improper use of control char|: all-whitespace header line\
  29240. |: header line longer than 998 characters|: header field syntax error\
  29241. |: missing required header field|: duplicate header field|]\
  29242. |, UNSOLICITED BULK EMAIL apparently from you\
  29243. |, UNSOLICITED BULK EMAIL apparently from you\
  29244. |, contents UNCHECKED\
  29245. |, BANNED contents type (%F)\
  29246. |, VIRUS in message apparently from you (%V)\
  29247. ]
  29248. Message-ID: <DSN%i@%h>
  29249. [? %#D |#|Your message WAS SUCCESSFULLY RELAYED to:[\n %D]
  29250. [~[:dsn_notify]|["\\bSUCCESS\\b"]|\
  29251. and you explicitly requested a delivery status notification on success.\n]\
  29252. ]
  29253. [? %#N |#|The message WAS NOT relayed to:[\n %N]
  29254. ]
  29255. [:wrap|78|||This [?%#D|nondelivery|delivery] report was \
  29256. generated by the program amavisd-new at host %h. \
  29257. Our internal reference code for your message is %n/%i]
  29258. # ccat_min 0: other, 1: bad MIME, 2: 8-bit char, 3: NUL/CR,
  29259. # 4: empty, 5: long, 6: syntax, 7: missing, 8: multiple
  29260. [? [:explain_badh] ||[? [:ccat|minor]
  29261. |INVALID HEADER
  29262. |INVALID HEADER: BAD MIME HEADER SECTION OR BAD MIME STRUCTURE
  29263. |INVALID HEADER: INVALID 8-BIT CHARACTERS IN HEADER SECTION
  29264. |INVALID HEADER: INVALID CONTROL CHARACTERS IN HEADER SECTION
  29265. |INVALID HEADER: FOLDED HEADER FIELD LINE MADE UP ENTIRELY OF WHITESPACE
  29266. |INVALID HEADER: HEADER LINE LONGER THAN RFC 5322 LIMIT OF 998 CHARACTERS
  29267. |INVALID HEADER: HEADER FIELD SYNTAX ERROR
  29268. |INVALID HEADER: MISSING REQUIRED HEADER FIELD
  29269. |INVALID HEADER: DUPLICATE HEADER FIELD
  29270. |INVALID HEADER
  29271. ]
  29272. [[:wrap|78| | |%X]\n]
  29273. ]\
  29274. #
  29275. [:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
  29276. [:wrap|78|| |From: [:header_field|From|100][?[:dkim|author]|| (dkim:AUTHOR)]]
  29277. [? [:header_field|Sender]|#|\
  29278. [:wrap|78|| |Sender: [:header_field|Sender|100]\
  29279. [?[:dkim|sender]|| (dkim:SENDER)]]]
  29280. [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29281. [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29282. [? %#X|#|[? [:useragent] |#|[:wrap|78|| |[:useragent]]]]
  29283. [? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
  29284. # ccat_min 0: other, 1: bad MIME, 2: 8-bit char, 3: NUL/CR,
  29285. # 4: empty, 5: long, 6: syntax, 7: missing, 8: multiple
  29286. [? [:explain_badh] ||[? [:ccat|minor]
  29287. |# 0: other
  29288. |# 1: bad MIME
  29289. |# 2: 8-bit char
  29290. WHAT IS AN INVALID CHARACTER IN A MAIL HEADER SECTION?
  29291. The RFC 5322 document specifies rules for forming internet messages.
  29292. It does not allow the use of characters with codes above 127 to be
  29293. used directly (non-encoded) in a mail header section.
  29294. If such characters (e.g. with diacritics) from ISO Latin or other
  29295. alphabets need to be included in a header section, these characters
  29296. need to be properly encoded according to RFC 2047. Such encoding
  29297. is often done transparently by mail reader (MUA), but if automatic
  29298. encoding is not available (e.g. by some older MUA) it is a user's
  29299. responsibility to avoid using such characters in a header section,
  29300. or to encode them manually. Typically the offending header fields
  29301. in this category are 'Subject', 'Organization', and comment fields
  29302. or display names in e-mail addresses of 'From', 'To' or 'Cc'.
  29303. Sometimes such invalid header fields are inserted automatically
  29304. by some MUA, MTA, content filter, or other mail handling service.
  29305. If this is the case, such service needs to be fixed or properly
  29306. configured. Typically the offending header fields in this category
  29307. are 'Date', 'Received', 'X-Mailer', 'X-Priority', 'X-Scanned', etc.
  29308. If you don't know how to fix or avoid the problem, please report it
  29309. to _your_ postmaster or system manager.
  29310. #
  29311. [~[:useragent]|^X-Mailer:\\s*Microsoft Outlook Express 6\\.00|["
  29312. If using Microsoft Outlook Express as your MUA, make sure its
  29313. settings under:
  29314. Tools -> Options -> Send -> Mail Sending Format -> Plain & HTML
  29315. are: "MIME format" MUST BE selected,
  29316. and "Allow 8-bit characters in headers" MUST NOT be enabled!
  29317. "]]#
  29318. |# 3: NUL/CR
  29319. IMPROPER USE OF CONTROL CHARACTER IN A MESSAGE HEADER SECTION
  29320. The RFC 5322 document specifies rules for forming internet messages.
  29321. It does not allow the use of control characters NUL and bare CR
  29322. to be used directly in a mail header section.
  29323. |# 4: empty
  29324. IMPROPERLY FOLDED HEADER FIELD LINE MADE UP ENTIRELY OF WHITESPACE
  29325. The RFC 5322 document specifies rules for forming internet messages.
  29326. In section '3.2.2. Folding white space and comments' it explicitly
  29327. prohibits folding of header fields in such a way that any line of a
  29328. folded header field is made up entirely of white-space characters
  29329. (control characters SP and HTAB) and nothing else.
  29330. |# 5: long
  29331. HEADER LINE LONGER THAN RFC 5322 LIMIT OF 998 CHARACTERS
  29332. The RFC 5322 document specifies rules for forming internet messages.
  29333. Section '2.1.1. Line Length Limits' prohibits each line of a header
  29334. section to be more than 998 characters in length (excluding the CRLF).
  29335. |# 6: syntax
  29336. |# 7: missing
  29337. MISSING REQUIRED HEADER FIELD
  29338. The RFC 5322 document specifies rules for forming internet messages.
  29339. Section '3.6. Field Definitions' specifies that certain header fields
  29340. are required (origination date field and the "From:" originator field).
  29341. |# 8: multiple
  29342. DUPLICATE HEADER FIELD
  29343. The RFC 5322 document specifies rules for forming internet messages.
  29344. Section '3.6. Field Definitions' specifies that certain header fields
  29345. must not occur more than once in a message header section.
  29346. |# other
  29347. ]]#
  29348. __DATA__
  29349. #
  29350. # =============================================================================
  29351. # This is a template for VIRUS/BANNED SENDER NOTIFICATIONS.
  29352. # For syntax and customization instructions see README.customize.
  29353. # The From, To and Date header fields will be provided automatically.
  29354. # Long header fields will be automatically wrapped by the program.
  29355. #
  29356. Subject: [? [:ccat|major]
  29357. |Clean message from you\
  29358. |Clean message from you\
  29359. |Clean message from you (MTA blocked)\
  29360. |OVERSIZED message from you\
  29361. |BAD-HEADER in message from you\
  29362. |Spam claiming to be from you\
  29363. |Spam claiming to be from you\
  29364. |A message with UNCHECKED contents from you\
  29365. |BANNED contents from you (%F)\
  29366. |VIRUS in message apparently from you (%V)\
  29367. ]
  29368. [? %m |#|In-Reply-To: %m]
  29369. Message-ID: <VS%i@%h>
  29370. [? [:ccat|major] |Clean|Clean|MTA-BLOCKED|OVERSIZED|INVALID HEADER|\
  29371. Spammy|Spam|UNCHECKED contents|BANNED CONTENTS ALERT|VIRUS ALERT]
  29372. Our content checker found
  29373. [? %#V |#|[:wrap|78| | |[? %#V |viruses|virus|viruses]: %V]]
  29374. [? %#F |#|[:wrap|78| | |banned [? %#F |names|name|names]: %F]]
  29375. [? %#X |#|[[:wrap|78| | |%X]\n]]
  29376. in email presumably from you %s
  29377. to the following [? %#R |recipients|recipient|recipients]:[
  29378. -> %R]
  29379. Our internal reference code for your message is %n/%i
  29380. [? %a |#|[:wrap|78|| |First upstream SMTP client IP address: \[%a\] %g]]
  29381. [? %e |#|[:wrap|78|| |According to a 'Received:' trace,\
  29382. the message apparently originated at: \[%e\], %t]]
  29383. [:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
  29384. [:wrap|78|| |From: [:header_field|From|100][?[:dkim|author]|| (dkim:AUTHOR)]]
  29385. [? [:header_field|Sender]|#|\
  29386. [:wrap|78|| |Sender: [:header_field|Sender|100]\
  29387. [?[:dkim|sender]|| (dkim:SENDER)]]]
  29388. [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29389. [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29390. [? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
  29391. [? %#D |Delivery of the email was stopped!
  29392. ]#
  29393. [? %#V ||Please check your system for viruses,
  29394. or ask your system administrator to do so.
  29395. ]#
  29396. [? %#V |[? %#F ||#
  29397. The message [?%#D|has been blocked|triggered this warning] because it contains a component
  29398. (as a MIME part or nested within) with declared name
  29399. or MIME type or contents type violating our access policy.
  29400. To transfer contents that may be considered risky or unwanted
  29401. by site policies, or simply too large for mailing, please consider
  29402. publishing your content on the web, and only sending an URL of the
  29403. document to the recipient.
  29404. Depending on the recipient and sender site policies, with a little
  29405. effort it might still be possible to send any contents (including
  29406. viruses) using one of the following methods:
  29407. - encrypted using pgp, gpg or other encryption methods;
  29408. - wrapped in a password-protected or scrambled container or archive
  29409. (e.g.: zip -e, arj -g, arc g, rar -p, or other methods)
  29410. Note that if the contents is not intended to be secret, the
  29411. encryption key or password may be included in the same message
  29412. for recipient's convenience.
  29413. We are sorry for inconvenience if the contents was not malicious.
  29414. The purpose of these restrictions is to cut the most common propagation
  29415. methods used by viruses and other malware. These often exploit automatic
  29416. mechanisms and security holes in more popular mail readers (Microsoft
  29417. mail readers and browsers are a common target). By requiring an explicit
  29418. and decisive action from the recipient to decode mail, the danger of
  29419. automatic malware propagation is largely reduced.
  29420. #
  29421. # Details of our mail restrictions policy are available at ...
  29422. ]]#
  29423. __DATA__
  29424. #
  29425. # =============================================================================
  29426. # This is a template for non-spam (e.g. VIRUS,...) ADMINISTRATOR NOTIFICATIONS.
  29427. # For syntax and customization instructions see README.customize.
  29428. # Long header fields will be automatically wrapped by the program.
  29429. #
  29430. From: %f
  29431. Date: %d
  29432. Subject: [? [:ccat|major] |Clean mail|Clean mail|MTA-blocked mail|\
  29433. OVERSIZED mail|INVALID HEADER in mail|Spammy|Spam|UNCHECKED contents in mail|\
  29434. BANNED contents (%F) in mail|VIRUS (%V) in mail]\
  29435. FROM [?%l||LOCAL ][?%a||[:client_addr_port] ]%s
  29436. To: [? %#T |undisclosed-recipients:;|[%T|, ]]
  29437. [? %#C |#|Cc: [%C|, ]]
  29438. Message-ID: <VA%i@%h>
  29439. [? %#V |No viruses were found.
  29440. |A virus was found: %V
  29441. |Two viruses were found:\n %V
  29442. |%#V viruses were found:\n %V
  29443. ]
  29444. [? %#F |#|[:wrap|78|| |Banned [?%#F|names|name|names]: %F]]
  29445. [? %#X |#|Bad header:[\n[:wrap|78| | |%X]]]
  29446. [? %#W |#\
  29447. |Scanner detecting a virus: %W
  29448. |Scanners detecting a virus: %W
  29449. ]
  29450. Content type: [:ccat|name|main]#
  29451. [? [:ccat|is_blocked_by_nonmain] ||, blocked for [:ccat|name]]
  29452. Internal reference code for the message is %n/%i
  29453. [? %a |#|[:wrap|78|| |First upstream SMTP client IP address: \[%a\] %g]]
  29454. [? %e |#|[:wrap|78|| |According to a 'Received:' trace,\
  29455. the message apparently originated at: \[%e\], %t]]
  29456. [:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
  29457. [:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
  29458. [? [:header_field|Sender]|#|\
  29459. [:wrap|78|| |Sender: [:header_field|Sender]\
  29460. [?[:dkim|sender]|| (dkim:SENDER)]]]
  29461. [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29462. [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29463. [? %j |#|[:wrap|78|| |Subject: %j]]
  29464. [? %q |Not quarantined.|The message has been quarantined as: %q]
  29465. [? %#S |Notification to sender will not be mailed.
  29466. ]#
  29467. [? %#D |#|The message WILL BE relayed to:[\n%D]
  29468. ]
  29469. [? %#N |#|The message WAS NOT relayed to:[\n%N]
  29470. ]
  29471. [? %#V |#|[? %#v |#|Virus scanner output:[\n %v]
  29472. ]]
  29473. __DATA__
  29474. #
  29475. # =============================================================================
  29476. # This is a template for VIRUS/BANNED/BAD-HEADER RECIPIENTS NOTIFICATIONS.
  29477. # For syntax and customization instructions see README.customize.
  29478. # Long header fields will be automatically wrapped by the program.
  29479. #
  29480. From: %f
  29481. Date: %d
  29482. Subject: [? [:ccat|major] |Clean mail|Clean mail|MTA-blocked mail|\
  29483. OVERSIZED mail|INVALID HEADER in mail|Spammy|Spam|UNCHECKED contents in mail|\
  29484. BANNED contents (%F) in mail|VIRUS (%V) in mail] TO YOU from %s
  29485. [? [:header_field|To] |To: undisclosed-recipients:;|To: [:header_field|To]]
  29486. [? [:header_field|Cc] |#|Cc: [:header_field|Cc]]
  29487. Message-ID: <VR%i@%h>
  29488. [? %#V |[? %#F ||BANNED CONTENTS ALERT]|VIRUS ALERT]
  29489. Our content checker found
  29490. [? %#V |#|[:wrap|78| | |[?%#V|viruses|virus|viruses]: %V]]
  29491. [? %#F |#|[:wrap|78| | |banned [?%#F|names|name|names]: %F]]
  29492. [? %#X |#|[[:wrap|78| | |%X]\n]]
  29493. in an email to you [? %#V |from:|from probably faked sender:]
  29494. %o
  29495. [? %#V |#|claiming to be: %s]
  29496. Content type: [:ccat|name|main]#
  29497. [? [:ccat|is_blocked_by_nonmain] ||, blocked for [:ccat|name]]
  29498. Our internal reference code for your message is %n/%i
  29499. [? %a |#|[:wrap|78|| |First upstream SMTP client IP address: \[%a\] %g]]
  29500. [? %e |#|[:wrap|78|| |According to a 'Received:' trace,\
  29501. the message apparently originated at: \[%e\], %t]]
  29502. [:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
  29503. [:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
  29504. [? [:header_field|Sender]|#|\
  29505. [:wrap|78|| |Sender: [:header_field|Sender]\
  29506. [?[:dkim|sender]|| (dkim:SENDER)]]]
  29507. [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29508. [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29509. [? [:useragent] |#|[:wrap|78|| |[:useragent]]]
  29510. [? %j |#|[:wrap|78|| |Subject: %j]]
  29511. [? %q |Not quarantined.|The message has been quarantined as: %q]
  29512. Please contact your system administrator for details.
  29513. __DATA__
  29514. #
  29515. # =============================================================================
  29516. # This is a template for spam SENDER NOTIFICATIONS.
  29517. # For syntax and customization instructions see README.customize.
  29518. # The From, To and Date header fields will be provided automatically.
  29519. # Long header fields will be automatically wrapped by the program.
  29520. #
  29521. Subject: Considered UNSOLICITED BULK EMAIL, apparently from you
  29522. [? %m |#|In-Reply-To: %m]
  29523. Message-ID: <SS%i@%h>
  29524. A message from %s[
  29525. to: %R]
  29526. was considered unsolicited bulk e-mail (UBE).
  29527. Our internal reference code for your message is %n/%i
  29528. The message carried your return address, so it was either a genuine mail
  29529. from you, or a sender address was faked and your e-mail address abused
  29530. by third party, in which case we apologize for undesired notification.
  29531. We do try to minimize backscatter for more prominent cases of UBE and
  29532. for infected mail, but for less obvious cases some balance between
  29533. losing genuine mail and sending undesired backscatter is sought,
  29534. and there can be some collateral damage on either side.
  29535. [? %a |#|[:wrap|78|| |First upstream SMTP client IP address: \[%a\] %g]]
  29536. [? %e |#|[:wrap|78|| |According to a 'Received:' trace,\
  29537. the message apparently originated at: \[%e\], %t]]
  29538. [:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
  29539. [:wrap|78|| |From: [:header_field|From|100][?[:dkim|author]|| (dkim:AUTHOR)]]
  29540. [? [:header_field|Sender]|#|\
  29541. [:wrap|78|| |Sender: [:header_field|Sender|100]\
  29542. [?[:dkim|sender]|| (dkim:SENDER)]]]
  29543. [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29544. [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29545. # [? [:useragent] |#|[:wrap|78|| |[:useragent]]]
  29546. [? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
  29547. [? %#X |#|\n[[:wrap|78|| |%X]\n]]
  29548. [? %#D |Delivery of the email was stopped!
  29549. ]#
  29550. #
  29551. # Spam scanner report:
  29552. # [%A
  29553. # ]\
  29554. __DATA__
  29555. #
  29556. # =============================================================================
  29557. # This is a template for spam ADMINISTRATOR NOTIFICATIONS.
  29558. # For syntax and customization instructions see README.customize.
  29559. # Long header fields will be automatically wrapped by the program.
  29560. #
  29561. From: %f
  29562. Date: %d
  29563. Subject: Spam FROM [?%l||LOCAL ][?%a||[:client_addr_port] ]%s
  29564. To: [? %#T |undisclosed-recipients:;|[%T|, ]]
  29565. [? %#C |#|Cc: [%C|, ]]
  29566. Message-ID: <SA%i@%h>
  29567. Content type: [:ccat|name|main]#
  29568. [? [:ccat|is_blocked_by_nonmain] ||, blocked for [:ccat|name]]
  29569. Internal reference code for the message is %n/%i
  29570. [? %a |#|[:wrap|78|| |First upstream SMTP client IP address: \[%a\] %g]]
  29571. [? %e |#|[:wrap|78|| |According to a 'Received:' trace,\
  29572. the message apparently originated at: \[%e\], %t]]
  29573. [:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
  29574. [:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
  29575. [? [:header_field|Sender]|#|\
  29576. [:wrap|78|| |Sender: [:header_field|Sender]\
  29577. [?[:dkim|sender]|| (dkim:SENDER)]]]
  29578. [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29579. [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29580. [? [:useragent] |#|[:wrap|78|| |[:useragent]]]
  29581. [? %j |#|[:wrap|78|| |Subject: %j]]
  29582. [? %q |Not quarantined.|The message has been quarantined as: %q]
  29583. [? %#D |#|The message WILL BE relayed to:[\n%D]
  29584. ]
  29585. [? %#N |#|The message WAS NOT relayed to:[\n%N]
  29586. ]
  29587. Spam scanner report:
  29588. [%A
  29589. ]\
  29590. __DATA__
  29591. #
  29592. # =============================================================================
  29593. # This is a template for the plain text part of a RELEASE FROM A QUARANTINE,
  29594. # applicable if a chosen release format is 'attach' (not 'resend').
  29595. #
  29596. From: %f
  29597. Date: %d
  29598. Subject: \[released message\] %j
  29599. To: [? %#T |undisclosed-recipients:;|[%T|, ]]
  29600. [? %#C |#|Cc: [%C|, ]]
  29601. Message-ID: <QRA%i@%h>
  29602. Please find attached a message which was held in a quarantine,
  29603. and has now been released.
  29604. [:wrap|78|| |Return-Path: %s[?[:dkim|envsender]|| (OK)]]
  29605. [:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
  29606. [? [:header_field|Sender]|#|\
  29607. [:wrap|78|| |Sender: [:header_field|Sender]\
  29608. [?[:dkim|sender]|| (dkim:SENDER)]]]
  29609. # [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29610. # [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29611. # [? [:useragent] |#|[:wrap|78|| |[:useragent]]]
  29612. [? %j |#|[:wrap|78|| |Subject: %j]]
  29613. Our internal reference code for the message is %n/%i
  29614. #
  29615. [~[:report_format]|["^attach$"]|["[? [:attachment_password] |#|
  29616. Contents of the attached mail message may pose a threat to your computer or
  29617. could be a social engineering deception, so it should be handled cautiously.
  29618. To prevent undesired automatic opening, the attached original mail message
  29619. has been wrapped in a password-protected ZIP archive.
  29620. Here is the password that allows opening of the attached archive:
  29621. [:attachment_password]
  29622. Note that the attachment is not strongly encrypted and the password
  29623. is not a strong secret (being displayed in this non-encrypted text),
  29624. so this attachment is not suitable for guarding a secret contents.
  29625. The sole purpose of this password protection it to prevent undesired
  29626. accidental or automatic opening of a message, either by some filtering
  29627. software, a virus scanner, or by a mail reader.
  29628. ]"]|]#
  29629. __DATA__
  29630. #
  29631. # =============================================================================
  29632. # This is a template for the plain text part of a problem/feedback report,
  29633. # with either the original message included in-line, or attached,
  29634. # or the message is structured as a FEEDBACK REPORT NOTIFICATIONS format.
  29635. # See RFC 5965 - "An Extensible Format for Email Feedback Reports".
  29636. #
  29637. From: %f
  29638. Date: %d
  29639. Subject: Fw: %j
  29640. To: [? %#T |undisclosed-recipients:;|[%T|, ]]
  29641. [? %#C |#|Cc: [%C|, ]]
  29642. Message-ID: <ARF%i@%h>
  29643. #Auto-Submitted: auto-generated
  29644. This is an e-mail [:feedback_type] report for a message \
  29645. [? %a |\nreceived on %d,|received from\nIP address [:client_addr_port] on %d,]
  29646. [:wrap|78|| |Return-Path: %s]
  29647. [:wrap|78|| |From: [:header_field|From][?[:dkim|author]|| (dkim:AUTHOR)]]
  29648. [? [:header_field|Sender]|#|[:wrap|78|| |Sender: [:header_field|Sender]]]
  29649. [? %m |#|[:wrap|78|| |Message-ID: %m]]
  29650. [? %r |#|[:wrap|78|| |Resent-Message-ID: %r]]
  29651. [? %j |#|[:wrap|78|| |Subject: [:header_field|Subject|100]]]
  29652. [?[:dkim|author]|#|
  29653. A first-party DKIM or DomainKeys signature is valid, d=[:dkim|author].]
  29654. Reporting-MTA: %h
  29655. Our internal reference code for the message is %n/%i
  29656. [~[:report_format]|["^(arf|attach|dsn)$"]|["\
  29657. A complete original message is attached.
  29658. [~[:report_format]|["^arf$"]|\
  29659. For more information on the ARF format please see RFC 5965.
  29660. ]"]|["\
  29661. A complete original message in its pristine form follows:
  29662. "]]#
  29663. __DATA__
  29664. #
  29665. # =============================================================================
  29666. # This is a template for the plain text part of an auto response (e.g.
  29667. # vacation, out-of-office), see RFC 3834.
  29668. #
  29669. From: %f
  29670. Date: %d
  29671. To: [? %#T |undisclosed-recipients:;|[%T|, ]]
  29672. [? %#C |#|Cc: [%C|, ]]
  29673. Reply-To: postmaster@%h
  29674. Message-ID: <ARE%i@%h>
  29675. Auto-Submitted: auto-replied
  29676. [:wrap|76||\t|Subject: Auto: autoresponse to: %s]
  29677. [? %m |#|In-Reply-To: %m]
  29678. Precedence: junk
  29679. This is an auto-response to a message \
  29680. [? %a |\nreceived on %d,|received from\nIP address \[%a\] on %d,]
  29681. envelope sender: %s
  29682. (author) From: [:rfc2822_from]
  29683. [? %j |#|[:wrap|78|| |Subject: %j]]
  29684. [?[:dkim|author]|#|
  29685. A first-party DKIM or DomainKeys signature is valid, d=[:dkim|author].]