diff -urN exim-4.43-orig/OS/Makefile-Base exim-4.43/OS/Makefile-Base --- exim-4.43-orig/OS/Makefile-Base Tue Oct 5 10:32:08 2004 +++ exim-4.43/OS/Makefile-Base Wed Oct 6 11:49:02 2004 @@ -285,14 +285,14 @@ # Targets for final binaries; the main one has a build number which is # updated each time. We don't bother with that for the auxiliaries. -OBJ_EXIM = acl.o child.o crypt16.o daemon.o dbfn.o debug.o deliver.o \ +OBJ_EXIM = acl.o bmi_spam.o child.o crypt16.o daemon.o dbfn.o debug.o deliver.o demime.o \ directory.o dns.o drtables.o enq.o exim.o expand.o filter.o \ filtertest.o globals.o \ - header.o host.o ip.o log.o lss.o match.o moan.o \ + header.o host.o ip.o log.o lss.o malware.o match.o mime.o moan.o \ os.o parse.o queue.o \ - rda.o readconf.o receive.o retry.o rewrite.o rfc2047.o \ - route.o search.o sieve.o smtp_in.o smtp_out.o spool_in.o spool_out.o \ - store.o string.o tls.o tod.o transport.o tree.o verify.o \ + rda.o readconf.o receive.o regex.o retry.o rewrite.o rfc2047.o \ + route.o search.o sieve.o smtp_in.o smtp_out.o spam.o spf.o spool_in.o spool_mbox.o spool_out.o \ + srs.o store.o string.o tls.o tnef.o tod.o transport.o tree.o verify.o \ local_scan.o $(EXIM_PERL) exim: pcre/libpcre.a lookups/lookups.a auths/auths.a \ @@ -499,12 +499,14 @@ # Dependencies for the "ordinary" exim modules acl.o: $(HDRS) acl.c +bmi_spam.o: $(HDRS) bmi_spam.c child.o: $(HDRS) child.c crypt16.o: $(HDRS) crypt16.c daemon.o: $(HDRS) daemon.c dbfn.o: $(HDRS) dbfn.c debug.o: $(HDRS) debug.c deliver.o: $(HDRS) deliver.c +demime.o: $(HDRS) demime.c directory.o: $(HDRS) directory.c dns.o: $(HDRS) dns.c enq.o: $(HDRS) enq.c @@ -518,7 +520,9 @@ ip.o: $(HDRS) ip.c log.o: $(HDRS) log.c lss.o: $(HDRS) lss.c +malware.o: $(HDRS) malware.c match.o: $(HDRS) match.c +mime.o: $(HDRS) mime.c moan.o: $(HDRS) moan.c os.o: $(HDRS) os.c parse.o: $(HDRS) parse.c @@ -526,6 +530,7 @@ rda.o: $(HDRS) rda.c readconf.o: $(HDRS) readconf.c receive.o: $(HDRS) receive.c +regex.o: $(HDRS) regex.c retry.o: $(HDRS) retry.c rewrite.o: $(HDRS) rewrite.c rfc2047.o: $(HDRS) rfc2047.c @@ -534,11 +539,16 @@ sieve.o: $(HDRS) sieve.c smtp_in.o: $(HDRS) smtp_in.c smtp_out.o: $(HDRS) smtp_out.c +spam.o: $(HDRS) spam.c +spf.o: $(HDRS) spf.c spool_in.o: $(HDRS) spool_in.c +spool_mbox.o: $(HDRS) spool_mbox.c spool_out.o: $(HDRS) spool_out.c +srs.o: $(HDRS) srs.c store.o: $(HDRS) store.c string.o: $(HDRS) string.c tls.o: $(HDRS) tls.c tls-gnu.c tls-openssl.c +tnef.o: $(HDRS) tnef.c tod.o: $(HDRS) tod.c transport.o: $(HDRS) transport.c tree.o: $(HDRS) tree.c diff -urN exim-4.43-orig/OS/os.h-Linux exim-4.43/OS/os.h-Linux --- exim-4.43-orig/OS/os.h-Linux Tue Oct 5 10:32:08 2004 +++ exim-4.43/OS/os.h-Linux Wed Oct 6 15:00:52 2004 @@ -35,3 +35,8 @@ extern struct ip_address_item *os_find_running_interfaces_linux(void); /* End */ + +/* Fudge added because this Linux doesn't appear to have a definition +for ip_options in /usr/include/linux/ip.h. */ + +#define ip_options options diff -urN exim-4.43-orig/README.EXISCAN exim-4.43/README.EXISCAN --- exim-4.43-orig/README.EXISCAN Thu Jan 1 01:00:00 1970 +++ exim-4.43/README.EXISCAN Wed Oct 6 11:49:02 2004 @@ -0,0 +1 @@ +Please refer to doc/exiscan-acl-spec.txt diff -urN exim-4.43-orig/doc/exiscan-acl-examples.txt exim-4.43/doc/exiscan-acl-examples.txt --- exim-4.43-orig/doc/exiscan-acl-examples.txt Thu Jan 1 01:00:00 1970 +++ exim-4.43/doc/exiscan-acl-examples.txt Wed Oct 6 11:49:02 2004 @@ -0,0 +1,440 @@ +-------------------------------------------------------------- +exiscan-acl example configurations / FAQ +-------------------------------------------------------------- + +Author: Tom Kistner + +The exiscan website is at http://duncanthrax.net/exiscan/. You +will find the latest patch versions, as well as links to the +mailing list and its archives there. + +This document shows some example configuration snippets: + +1. Basic sitewide virus and spam filtering by rejecting + matching messages after DATA. +2. Adding a cryptographic "checks done" header that will + prevent re-scanning when the message re-visits one of your + mail servers, and the body size did not change. +3. Marking spam-suspicious messages with extra headers and a + tag in the subject. +4. Having more than one spam threshold to act on. +5. Redirecting matching messages to special accounts while + preserving envelope recipient information. +6. A multi-profile configuration for sites where different + "customers" (or users) have different content scanning + preferences. + +These examples serve as a guideline and should give you some +pointers that can help you to create your own configuration. +Please do not copy these examples verbatim. You really need to +know what you are doing. The content scanning topic is really +complex and you can screw up your mail server easily if you do +not get it "right". + +I recommend to read the exiscan documentation on the above +mentioned website before trying to make sense of the following +examples. + +Each example shows part of a DATA ACL definition, unless +otherwise noted. + +-------------------------------------------------------------- +1. Basic setup for simple site-wide filtering +-------------------------------------------------------------- +The following example only shows the most basic use of the +exiscan content filtering features. You should see it as a +base that you can build on. However, it may be all you need +for smaller systems with only a few users. + +/* ----------------- +# Do not scan messages submitted from our own hosts +# and locally submitted messages. Since the DATA ACL +# is not called for messages not submitted via SMTP +# protocols, we do not need to check for an empty +# host field. +accept hosts = 127.0.0.1:+relay_from_hosts + +# Unpack MIME containers and reject file extensions +# used by worms. Note that the extension list may be +# incomplete. +deny message = $found_extension files are not accepted here + demime = com:vbs:bat:pif:scr + +# Reject messages that have serious MIME errors. +# This calls the demime condition again, but it +# will return cached results. +deny message = Serious MIME defect detected ($demime_reason) + demime = * + condition = ${if >{$demime_errorlevel}{2}{1}{0}} + +# Reject messages containing malware. +deny message = This message contains malware ($malware_name) + malware = * + +# Reject spam messages. Remember to tweak your +# site-wide SA profile. Do not spam-scan messages +# larger than eighty kilobytes. +deny message = Classified as spam (score $spam_score) + condition = ${if <{$message_size}{80k}{1}{0}} + spam = nobody + +# Finally accept all other messages that have +# made it to this point +accept +------------------ */ + + + +-------------------------------------------------------------- +2. Adding a cryptographic "scanning done" header +-------------------------------------------------------------- + +If you have a mail setup where the same message may pass your +server twice (redirects from other servers), or you have +multiple mail servers, you may want to make sure that each +message is only checked once, to save processing time. Here is +how to do it: + +At the very beginning of your DATA ACL, put this: + +/* ----------------- +# Check our crytographic header. If it matches, accept +# the message. +accept condition = ${if eq {${hmac{md5}\ + {mysecret}\ + {$body_linecount}}}\ + {$h_X-Scan-Signature:} {1}{0}} +------------------ */ + +At the end, just before the final "accept" verb, put this: + +/* ----------------- +# Add the cryptographic header. +warn message = X-Scan-Signature: ${hmac{md5}{mysecret}\ + {$body_linecount}} +------------------ */ + +Notice the two "mysecret" strings? Replace them with your own +secret, and don't tell anyone :) The hash also includes the +number of lines in the message body, to protect against +message "modifications". + + +-------------------------------------------------------------- +3. Marking Spam messages with extra headers and subject tag +-------------------------------------------------------------- + +Since the false positive rate with spam scanning is high +compared to virus scanning, it is wise to implement a scheme +with two thresholds, where you reject messages with high +scores and just mark messages with lower scores. End users can +then set up filters in their Mail User Agents (MUAs). Since +many MUAs can not filter on custom headers, it can be +necessary to put a "spam tag" in the subject line. Since it is +not (yet) possible to remove headers in Exims DATA ACL, we +must do this in a system filter. Please see the Exim docs on +how to set up a system filter. + +The following example will unconditionally put two spam +information headers in each message, if it is smaller than +eighty kilobytes: + +/* ----------------- +# Always put X-Spam-Score header in the message. +# It looks like this: +# X-Spam-Score: 6.6 (++++++) +# When a MUA cannot match numbers, it can match for an +# equivalent number of '+' signs. +# The 'true' makes sure that the header is always put +# in, no matter what the score. +warn message = X-Spam-Score: $spam_score ($spam_bar) + condition = ${if <{$message_size}{80k}{1}{0}} + spam = nobody:true + +# Always put X-Spam-Report header in the message. +# This is a multiline header that informs the user +# which tests a message has "hit", and how much a +# test has contributed to the score. +warn message = X-Spam-Report: $spam_report + condition = ${if <{$message_size}{80k}{1}{0}} + spam = nobody:true +------------------ */ + +For the subject tag, we prepare a new subject header in the +ACL, then swap it with the original Subject in the system +filter. + +In the DATA ACL, put this: +/* ----------------- +warn message = X-New-Subject: *SPAM* $h_subject: + spam = nobody +------------------ */ + +In the system filter, put this: +/* ----------------- +if "${if def:header_X-New-Subject: {there}}" is there +then + headers remove Subject + headers add "Subject: $h_X-New-Subject:" + headers remove X-New-Subject +endif +------------------ */ + + +-------------------------------------------------------------- +4. Defining multiple spam thresholds with different actions +-------------------------------------------------------------- +If you want to mark messages if they exceed your threshold, +but also have a higher "cutoff" threshold where you reject +messages, use the example above, plus this part: + +/* ----------------- +deny message = Spam score too high ($spam_score) + condition = ${if <{$message_size}{80k}{1}{0}} + spam = nobody:true + condition = ${if >{$spam_score_int}{100}{1}{0}} +------------------ */ + +The last condition is only true if the spam score exceeds 10.0 +points (Keep in mind that $spam_score_int is the messages +score multiplied by ten). + + + +-------------------------------------------------------------- +5. Redirect infected or spam messages to special accounts +-------------------------------------------------------------- +Sometimes it is desirable not to reject messages, but to stop +them for inspection, and then decide wether to delete, bounce +or pass them. + +There are multiple ways to achieve this. The simplest way is +to freeze suspicious messages, and then thaw or bounce them +after a review. Here is a simple example that will freeze spam +suspicious messages when they exceed the SA threshold: + +/* ----------------- +warn log_message = frozen by spam scanner, score $spam_score + spam = nobody + control = freeze +------------------ */ + +Another way is to redirect suspicious messages to special +postmaster accounts, where they can be reviewed. This involves +setting up a router for these special accounts that acts on a +header set in the DATA ACL. + +This is the DATA ACL entry: + +/* ----------------- +warn message = X-Redirect-To: spambox@mycompany.com + spam = nobody +------------------ */ + +This puts the target address in a special header, which can in +turn be read with this router: + +/* ----------------- +scan_redirect: + driver = redirect + condition = ${if def:h_X-Redirect-To: {1}{0}} + headers_add = X-Original-Recipient: $local_part@$domain + data = $h_X-Redirect-To: + headers_remove = X-Redirect-To + redirect_router = my_second_router +------------------ */ + +This router should probably be your very first one, and you +need to edit the last line (redirect_router = ) to replace +"my_second_router" with the name of your original first +router. Note that the original message recipient is saved in +the "X-Original-Recipient" header, and the X-Redirect-To +header line is removed. + + +-------------------------------------------------------------- +6. Having multiple content scanning profiles for several + users or domains. +-------------------------------------------------------------- +This is one of the most often asked questions, and it also has +the most complicated answer. To understand the difficulties, +you should first remember that the exiscan facilities are run +in the DATA ACL. This ACL is called ONCE per message, after +the sending server has transmitted the end-of-data marker. +This gives us the very cool possibility to reject unwanted +messages with a 5xx error code in response. The big drawback +is that a message can have multiple recipients, and you can +only reject or accept a message for ALL recipients, not +individual ones. + +I will first sum up the possible solutions to this dilemma: + + a. Make sure that each incoming message can have only one + envelope recipient. This is brutal, but effective and + reliably solves the problem on your end. :) Drawback: + Incoming mail to multiple recipients is slowed down. The + exact time depends on the retry strategies of the sending + hosts. + + b. Offer a limited number of "profiles" that your customers + can subscribe to. Then, similar to a.), only accept + recipients with the same profile in a single "batch", and + defer the others. This does improve on the drawback of + a.) a bit. + + c. Do scanning as usual, but never reject messages in the + DATA ACL. Instead put appropriate information in extra + headers and query those in routers or transports later. + Drawback: You'll have to send bounces yourself, and your + queue will fill up with frozen bounces. Advantage: clean + solution, protocol-wise. + +As you see, you can't have your cake and eat it too. Now lets +get into the details of each possible solution. + +a.) Making sure each incoming message that will be scanned + only has one recipient. + + To use this scheme, you must make sure that you do not use + it on your +relay_from_hosts and authenticated senders. + Both of these may be MUAs who cannot cope with such a + thing. + + Here is a RCPT ACL that implements the behaviour + (shortened, do not copy 1:1!): + + /* ------------ + acl_check_rcpt: + + # accept local, relay-allowed + # and authenticated sources + + accept hosts = : + deny local_parts = ^.*[@%!/|] + accept hosts = 127.0.0.1:+relay_from_hosts + accept authenticated = * + + # the following treat non-local, + # non-authenticated sources + + defer message = only one recipient at a time + condition = ${if def:acl_m0 {1}{0}} + + # [ .. ] + # put RBLs etc. here + # [ .. ] + + accept domains = +local_domains + endpass + message = unknown user + verify = recipient + set acl_m0 = $local_part@$domain + + accept domains = +relay_to_domains + endpass + message = unrouteable address + verify = recipient + set acl_m0 = $domain + + deny message = relay not permitted + ------------ */ + + The lines which contain acl_m0 are the important ones. The + $acl_m0 variable gets set when a remote server + successfully sends one RCPT. Subsequent RCPT commands are + deferred if this variable is set. The $acl_m0 variable now + contains the single recipient domain, which you can use in + the DATA ACL to determine the scanning profile. + + This scheme is only recommended for small servers with a + low number of possible recipients, where recipients do not + belong to the same organization. An example would be a + multiuser shell server. + + +b.) Having several scanning profiles that "customers" can + choose from. + + Suppose you want to offer three profiles. Lets call them + "reject-aggressive", "reject-conservative", and "warn + -only". Customers can select one of the profiles for each + of their domains. So you end up with a mapping like this: + + domain-a.com: reject-aggressive + domain-b.org: warn-only + domain-c.net: reject-aggressive + domain-d.com: reject-conservative + [ .. ] + + Suppose you put that in a file called /etc/exim/scanprefs + + Now we make a scheme similar to a.), but we do allow more + than one recipient if they have the same scanning profile + than the first recipient. + + Here is a RCPT ACL that implements the behaviour + (shortened, do not copy 1:1!): + + /* ------------ + acl_check_rcpt: + + # accept local, relay-allowed and authenticated sources + + accept hosts = : + deny local_parts = ^.*[@%!/|] + accept hosts = 127.0.0.1:+relay_from_hosts + accept authenticated = * + + # the following treat non-local, non-authenticated sources + + defer message = try this address in the next batch + condition = ${if eq {${acl_m0}}\ + {${lookup{$domain}\ + lsearch{/etc/exim/scanprefs}}}\ + {0}{1}} + + # [ .. ] + # put RBLs etc. here + # [ .. ] + + accept domains = +local_domains + endpass + message = unknown user + verify = recipient + set acl_m0 = $local_part@$domain + + accept domains = +relay_to_domains + endpass + message = unrouteable address + verify = recipient + set acl_m0 = ${lookup{$domain}\ + lsearch{/etc/exim/scanprefs}} + + deny message = relay not permitted + ------------ */ + + Now a recipient address get deferred if its scan profile + does not match the current batch profile. The $acl_m0 + variable contains the name of the profile, that can be + used for processing in the DATA ACL. + + This scheme works pretty well if you keep the number of + possible profiles low, since that will prevent + fragmentation of RCPT blocks. + + +c.) Classic content scanning without the possibility of + rejects after DATA. + + This emulates the "classic" content scanning in routers + and transports. The difference is that we still do the + scan in the DATA ACL, but put the outcome of each facility + in message headers, that can the be evaluated in special + routers, individually for each recipient. + + A special approach can be taken for spam scanning, since + the $spam_score_int variable is also available in routers + and transports (it gets written to the spool files), so + you do not need to put that information in a header, but + rather act on $spam_score_int directly. + diff -urN exim-4.43-orig/doc/exiscan-acl-spec.txt exim-4.43/doc/exiscan-acl-spec.txt --- exim-4.43-orig/doc/exiscan-acl-spec.txt Thu Jan 1 01:00:00 1970 +++ exim-4.43/doc/exiscan-acl-spec.txt Wed Oct 6 15:54:41 2004 @@ -0,0 +1,1282 @@ +-------------------------------------------------------------- +The exiscan-acl patch for exim4 - Documentation +-------------------------------------------------------------- +(c) Tom Kistner 2003-???? +License: GPL + +The exiscan-acl patch adds content scanning to the exim4 ACL +system. It supports the following scanning features: + + - MIME ACL that is called for all MIME parts in + incoming MIME messages. + - Antivirus using 3rd party scanners. + - Anti-spam using SpamAssassin. + - Anti-spam using Brightmail Antispam. + - SPF support via the libspf2 library + - SRS support via the libsrs_alt library + - Regular expression match against headers, bodies, raw + MIME parts and decoded MIME parts. + +These features are hooked into exim by extending exim's ACL +system. The patch adds expansion variables and ACL conditions. +These conditions are designed to be used in the acl_smtp_data +ACL. It is run when the sending host has completed the DATA +phase and is waiting for our final response to his end-of-data +marker. This allows us to reject messages containing +unwanted content at that stage. + +Support for Brightmail AntiSpam requires special compile-time +flags. Please refer to chapter 7 for details. + +The default exim configure file contains commented +configuration examples for some features of exiscan-acl. + + +0. Overall concept / Overview +-------------------------------------------------------------- + +The exiscan-acl patch extends Exims with mechanisms to +deal with the message body content. Most of these additions +affect the ACL system. The exiscan patch adds + +- A new ACL, called 'acl_smtp_mime' (Please see detailed + chapter on this one below). +- ACL conditions and modifiers + o malware (attach 3rd party virus/malware scanner) + o spam (attach SpamAssassin) + o regex (match regex against message, linewise) + o decode (decode MIME part to disk) + o mime_regex (match regex against decoded MIME part) + o control = fakereject (reject but really accept a message) + o spf (do SPF checks) +- expansion variables + (see chapters below for names and explanations) +- configuration options in section 1 of Exim's configure file. + o av_scanner (type and options of the AV scanner) + o spamd_address (network address / socket of spamd daemon). + +All facilites work on a MBOX copy of the message that is +temporarily spooled up in a file called: + + /scan//.eml + +The .eml extension is a friendly hint to virus scanners that +they can expect an MBOX-like structure inside that file. The +file is only spooled up once, when the first exiscan facility +is called. Subsequent calls to exiscan conditions will just +open the file again. The directory is recursively removed +when the acl_smtp_data has finished running. When the MIME +ACL decodes files, they will be put into that same folder by +default. + + +1. The acl_smtp_mime MIME ACL +-------------------------------------------------------------- + +Note: if you are not familiar with exims ACL system, please go +read the documentation on it, otherwise this chapter will not +make much sense to you. + +Here are the facts on acl_smtp_mime: + + - It is called once for each MIME part of a message, + including multipart types, in the sequence of their + position in the message. + + - It is called just before the acl_smtp_data ACL. They share + a result code (the one assed to the remote system after + DATA). When a call to acl_smtp_mime does not yield + "accept", ACL processing is aborted and the respective + result code is sent to the remote mailer. This means that + the acl_smtp_data is NOT called any more. + + - It is ONLY called if the message has a MIME-Version header. + + - MIME parts will NOT be dumped to disk by default, you have + to call the "decode" condition to do that (see further + below). + + - For RFC822 attachments (these are messages attached to + messages, with a content-type of 'message/rfc822'), + the ACL is called again in the same manner as + for the "primary" message, only that the $mime_is_rfc822 + expansion variable is set (see below). These messages + are always decoded to disk before being checked, but + the files are unlinked once the check is done. + +To activate acl_smtp_mime, you need to add assign it the name +of an ACL entry in section 1 of the config file, and then +write that ACL in the ACL section, like: + + /* --------------- + + # -- section 1 ---- + [ ... ] + acl_smtp_mime = my_mime_acl + [ ... ] + + # -- acl section ---- + begin acl + + [ ... ] + + my_mime_acl: + + < ACL logic > + + [ ... ] + + ---------------- */ + +The following list describes all expansion variables that are +available in the MIME ACL: + + $mime_content_type + ------------------ + A very important variable. If the MIME part has a "Content + -Type:" header, this variable will contain its value, + lowercased, and WITHOUT any options (like "name" or + "charset", see below for these). Here are some examples of + popular MIME types, as they may appear in this variable: + + text/plain + text/html + application/octet-stream + image/jpeg + audio/midi + + If the MIME part has no "Content-Type:" header, this + variable is the empty string. + + + $mime_filename + -------------- + Another important variable, possibly the most important one. + It contains a proposed filename for an attachment, if one + was found in either the "Content-Type:" or "Content + -Disposition" headers. The filename will be RFC2047 + decoded, however NO additional sanity checks are done. See + instructions on "decode" further below. If no filename was + found, this variable is the empty string. + + + $mime_charset + ------------- + Contains the charset identifier, if one was found in the + "Content-Type:" header. Examples for charset identifiers are + + us-ascii + gb2312 (Chinese) + iso-8859-1 + + Please note that this value will NOT be normalized, so you + should do matches case-insensitively. + + + $mime_boundary + -------------- + If the current part is a multipart (see $mime_is_multipart) + below, it SHOULD have a boundary string. It is stored in + this variable. If the current part has no boundary parameter + in the "Content-Type:" header, this variable contains the + empty string. + + + $mime_content_disposition + ------------------------- + Contains the normalized content of the "Content + -Disposition:" header. You can expect strings like + "attachment" or "inline" here. + + + $mime_content_transfer_encoding + ------------------------------- + Contains the normalized content of the "Content + -transfer-encoding:" header. This is a symbolic name for + an encoding type. Typical values are "base64" and "quoted + -printable". + + + $mime_content_id + ---------------- + Contains the normalized content of the "Content + -ID:" header. This is a unique ID that can be used to + reference a part from another part. + + + $mime_content_description + ------------------------- + Contains the normalized content of the "Content + -Description:" header. It can contain a human-readable + description of the parts content. Some implementations will + repeat the filename for attachments here, but they are + usually only used for display purposes. + + + $mime_part_count + ---------------- + This is a counter that is raised for each processed MIME + part. It starts at zero for the very first part (which is + usually a multipart). The counter is per-message, so it is + reset when processing RFC822 attachments (see + $mime_is_rfc822). The counter stays set after acl_smtp_mime + is complete, so you can use it in the DATA ACL to determine + the number of MIME parts of a message. For non-MIME + messages, this variable will contain the value -1. + + + $mime_is_multipart + ------------------ + A "helper" flag that is true (1) when the current + part has the main type "multipart", for example + "multipart/alternative" or "multipart/mixed". Since + multipart entities only serve as containers for other parts, + you may not want to carry out specific actions on them. + + + $mime_is_coverletter + -------------------- + This flag attempts to differentiate the "cover letter" of an + e-mail from attached data. It can be used to clamp down on + "flashy" or unneccessarily encoded content in the + coverletter, while not restricting attachments at all. + + It returns 1 (true) for a MIME part believed to be part of + the coverletter, 0 (false) for an attachment. At + present, the algorithm is as follows: + + 1. The outermost MIME part of a message always coverletter. + 2. If a multipart/alternative or multipart/related MIME part + is coverletter, so are all MIME subparts within that + multipart. + 3. If any other multipart is coverletter, the first subpart + is coverletter, the rest are attachments. + 4. All parts contained within an attachment multipart are + attachments. + + As an example, the following will ban "HTML mail" (including + that sent with alternative plain text), while allowing HTML + files to be attached: + + /* ---------------------- + deny message = HTML mail is not accepted here + condition = $mime_is_coverletter + condition = ${if eq{$mime_content_type}{text/html}{1}{0}} + ----------------------- */ + + + $mime_is_rfc822 + --------------- + This flag is true (1) if the current part is NOT a part of + the checked message itself, but part of an attached message. + Attached message decoding is fully recursive. + + + $mime_decoded_filename + ---------------------- + This variable is only set after the "decode" condition (see + below) has been successfully run. It contains the full path + and file name of the file containing the decoded data. + + + $mime_content_size + ------------------ + This variable is only set after the "decode" condition (see + below) has been successfully run. It contains the size of + the decoded part in kilobytes(!). The size is always + rounded up to full kilobytes, so only a completely empty + part will have a mime_content_size of 0. + + +The expansion variables only reflect the content of the MIME +headers for each part. To actually decode the part to disk, +you can use the "decode" condition. The general syntax is + +decode = [//] + +The right hand side is expanded before use. After expansion, +the value can + + - be '0' or 'false', in which case no decoding is done. + - be the string 'default'. In that case, the file will be + put in the temporary "default" directory + /scan// + with a sequential file name, consisting of the message id + and a sequence number. The full path and name is available + in $mime_decoded_filename after decoding. + - start with a slash. If the full name is an existing + directory, it will be used as a replacement for the + "default" directory. The filename will then also be + sequentially assigned. If the name does not exist, it will + be used as the full path and file name. + - not start with a slash. It will then be used as the + filename, and the default path will be used. + +You can easily decode a file with its original, proposed +filename using "decode = $mime_filename". However, you should +keep in mind that $mime_filename might contain anything. If +you place files outside of the default path, they will not be +automatically unlinked. + +The MIME ACL also supports the regex= and mime_regex= +conditions. You can use those to match regular expressions +against raw and decoded MIME parts, respectively. Read the +next section for more information on these conditions. + + + +2. Match message or MIME parts against regular expressions +-------------------------------------------------------------- + +The "regex" condition takes one or more regular expressions as +arguments and matches them against the full message (when +called in the DATA ACL) or a raw MIME part (when called in the +MIME ACL). The "regex" condition matches linewise, with a +maximum line length of 32k characters. That means you can't +have multiline matches with the "regex" condition. + +The "mime_regex" can only be called in the MIME ACL. It +matches up to 32k of decoded content (the whole content at +once, not linewise). If the part has not been decoded with the +"decode" condition earlier in the ACL, it is decoded +automatically when "mime_regex" is executed (using default +path and filename values). If the decoded data is larger +than 32k, only the first 32k characters will be +matched. + +The regular expressions are passed as a colon-separated list. +To include a literal colon, you must double it. Since the +whole right-hand side string is expanded before being used, +you must also escape dollar ($) signs with backslashes. + +Here is a simple example: + +/* ---------------------- +deny message = contains blacklisted regex ($regex_match_string) + regex = [Mm]ortgage : URGENT BUSINESS PROPOSAL +----------------------- */ + +The conditions returns true if one of the regular +expressions has matched. The $regex_match_string expansion +variable is then set up and contains the matching regular +expression. + +Warning: With large messages, these conditions can be fairly +CPU-intensive. + + + +3. Antispam measures with SpamAssassin +-------------------------------------------------------------- + +The "spam" ACL condition calls SpamAssassin's "spamd" daemon +to get a spam-score and a report for the message. You must +first install SpamAssassin. You can get it +at http://www.spamassassin.org, or, if you have a working +Perl installation, you can use CPAN by calling + +perl -MCPAN -e 'install Mail::SpamAssassin' + +SpamAssassin has its own set of configuration files. Please +review its documentation to see how you can tweak it. The +default installation should work nicely, however. + +After having installed and configured SpamAssassin, start the +"spamd" daemon. By default, it listens on 127.0.0.1, TCP port +783. If you use another host or port for spamd, you must set +the spamd_address option in Section 1 of the exim +configuration as follows (example): + +spamd_address = 127.0.0.1 783 + +As of version 2.60, spamd also supports communication over UNIX +sockets. If you want to use these, supply spamd_address with +an absolute file name instead of a address/port pair, like: + +spamd_address = /var/run/spamd_socket + +If you use the above mentioned default, you do NOT need to set +this option. + +You can also have multiple spamd servers to improve +scalability. These can also reside on other hardware reachable +over the network. To specify multiple spamd servers, just put +multiple address/port pairs in the spamd_address option, +separated with colons: + +spamd_address = 192.168.2.10 783 : 192.168.2.11 783 : 192.168.2.12 783 + +Up to 32 spamd servers are supported. The servers will be +queried in a random fashion. When a server fails to respond +to the connection attempt, all other servers are tried +until one succeeds. If no server responds, the "spam" +condition will return DEFER. IMPORTANT: It is not possible +to use the UNIX socket connection method with +multiple spamd servers. + +To use the antispam facility, put the "spam" condition in a +DATA ACL block. Here is a very simple example: + +/* --------------- +deny message = This message was classified as SPAM + spam = joe +---------------- */ + +On the right-hand side of the spam condition, you can put the +username that SpamAssassin should scan for. That allows you to +use per-domain or per-user antispam profiles. The right-hand +side is expanded before being used, so you can put lookups or +conditions there. When the right-hand side evaluates to "0" or +"false", no scanning will be done and the condition will fail +immediately. + +If you do not want to scan for a particular user, but rather +use the SpamAssassin system-wide default profile, you can scan +for an unknown user, or simply use "nobody". + +The "spam" condition will return true if the threshold +specified in the user's SpamAssassin profile has been matched +or exceeded. If you want to use the spam condition for its +side effects (see the variables below), you can make it always +return "true" by appending ":true" to the username. + +When the condition is run, it sets up the following expansion +variables: + + $spam_score The spam score of the message, for example + "3.4" or "30.5". This is useful for + inclusion in log or reject messages. + + $spam_score_int The spam score of the message, multiplied + by ten, as an integer value. For example + "34" or "305". This is useful for numeric + comparisons in conditions. See further + below for a more complicated example. This + variable is special, since it is written + to the spool file, so it can be used + during the whole life of the message on + your exim system, even in routers + or transports. + + $spam_bar A string consisting of a number of '+' or + '-' characters, representing the + spam_score value. A spam score of "4.4" + would have a spam_bar of '++++'. This is + useful for inclusion in warning headers, + since MUAs can match on such strings. + + $spam_report A multiline text table, containing the + full SpamAssassin report for the message. + Useful for inclusion in headers or reject + messages. + +The spam condition caches its results. If you call it again +with the same user name, it will not really scan again, but +rather return the same values as before. + +The spam condition will return DEFER if there is any error +while running the message through SpamAssassin. If you want to +treat DEFER as FAIL (to pass on to the next ACL statement +block), append '/defer_ok' to the right hand side of the spam +condition, like this: + +/* --------------- +deny message = This message was classified as SPAM + spam = joe/defer_ok +---------------- */ + +This will cause messages to be accepted even if there is a +problem with spamd. + +Finally, here is a commented example on how to use the spam +condition: + +/* ---------------- +# put headers in all messages (no matter if spam or not) +warn message = X-Spam-Score: $spam_score ($spam_bar) + spam = nobody:true +warn message = X-Spam-Report: $spam_report + spam = nobody:true + +# add second subject line with *SPAM* marker when message +# is over threshold +warn message = Subject: *SPAM* $h_Subject + spam = nobody + +# reject spam at high scores (> 12) +deny message = This message scored $spam_score spam points. + spam = nobody:true + condition = ${if >{$spam_score_int}{120}{1}{0}} +----------------- */ + + + +4. The "malware" facility + Scan messages for viruses using an external virus scanner +-------------------------------------------------------------- + +This facility lets you connect virus scanner software to exim. +It supports a "generic" interface to scanners called via the +shell, and specialized interfaces for "daemon" type virus +scanners, who are resident in memory and thus are much faster. + +To use this facility, you MUST set the "av_scanner" option in +section 1 of the exim config file. It specifies the scanner +type to use, and any additional options it needs to run. The +basic syntax is as follows: + + av_scanner = :::[...] + +The following scanner-types are supported in this release: + + sophie Sophie is a daemon that uses Sophos' libsavi + library to scan for viruses. You can get Sophie + at http://www.vanja.com/tools/sophie/. The only + option for this scanner type is the path to the + UNIX socket that Sophie uses for client + communication. The default path is + /var/run/sophie, so if you are using this, you + can omit the option. Example: + + av_scanner = sophie:/tmp/sophie + + aveserver This is the scanner daemon of Kaspersky Version + 5. You can get a trial version at + http://www.kaspersky.com. This scanner type + takes one option, which is the path to the + daemon's UNIX socket. The default is + "/var/run/aveserver". Example: + + av_scanner = aveserver:/var/run/aveserver + + + kavdaemon This is the scanner daemon of Kaspersky Version + 4. This version of the Kaspersky scanner is + outdated. Please upgrade (see "aveserver" + above). This scanner type takes one option, + which is the path to the daemon's UNIX socket. + The default is "/var/run/AvpCtl". Example: + + av_scanner = kavdaemon:/opt/AVP/AvpCtl + + + clamd Another daemon type scanner, this one is GPL and + free. Get it at http://clamav.elektrapro.com/. + Clamd does not seem to unpack MIME containers, + so it is recommended to use the demime facility + with it. It takes one option: either the path + and name of a UNIX socket file, or a + hostname/port pair, separated by space. If + unset, the default is "/tmp/clamd". Example: + + av_scanner = clamd:192.168.2.100 1234 + or + av_scanner = clamd:/opt/clamd/socket + + + fsecure This is for the F-Secure (www.f-secure.com) + scanner. It takes one argument which is the + path to a UNIX socket. If omitted, it + defaults to + + /var/run/.fsav + + Example: + + av_scanner = fsecure:/path/to/.fsav + + Thanks to Johan Thelmen for + contributing the code for this scanner. + + + drweb This one is for the DrWeb (http://www.sald.com/) + daemon. It takes one argument, either a full + path to a UNIX socket, or an IP address and port + separated by whitespace. If you omit the + argument, the default + + /usr/local/drweb/run/drwebd.sock + + is used. Example: + + av_scanner = drweb:192.168.2.20 31337 + or + av_scanner = drweb:/var/run/drwebd.sock + + Thanks to Alex Miller for + contributing the code for this scanner. + + + mksd Yet another daemon type scanner, aimed mainly at + Polish users, though some parts of documentation + are now avaliable in English. You can get it at + http://linux.mks.com.pl/. The only option for + this scanner type is the maximum number of + processes used simultaneously to scan the + attachments, provided that the demime facility + is employed and also mksd has been run with + at least the same number of child processes. + You can safely omit this option, the default + value is 1. Example: + + av_scanner = mksd:2 + + + cmdline This is the keyword for the generic command line + scanner interface. It can be used to attach + virus scanners that are invoked on the shell. + This scanner type takes 3 mantadory options: + + - full path and name of the scanner binary, with + all command line options and a placeholder + (%s) for the directory to scan. + + - A regular expression to match against the + STDOUT and STDERR output of the virus scanner. + If the expression matches, a virus was found. + You must make absolutely sure that this + expression only matches on "virus found". This + is called the "trigger" expression. + + - Another regular expression, containing exactly + ONE pair of braces, to match the name of the + virus found in the scanners output. This is + called the "name" expression. + + Example: + + Sophos Sweep reports a virus on a line like + this: + + Virus 'W32/Magistr-B' found in file ./those.bat + + For the "trigger" expression, we just use the + "found" word. For the "name" expression, we want + to get the W32/Magistr-B string, so we can match + for the single quotes left and right of it, + resulting in the regex '(.*)' (WITH the quotes!) + + Altogether, this makes the configuration + setting: + + av_scanner = cmdline:\ + /path/to/sweep -all -rec -archive %s:\ + found:'(.+)' + +When av_scanner is correcly set, you can use the "malware" +condition in the DATA ACL. The condition takes a right-hand +argument that is expanded before use. It can then be one of + + - "true", "*", or "1", in which case the message is scanned + for viruses. The condition will succeed if a virus was + found, or fail otherwise. This is the recommended usage. + + - "false" or "0", in which case no scanning is done and the + condition will fail immediately. + + - a regular expression, in which case the message is scanned + for viruses. The condition will succeed if a virus found + found and its name matches the regular expression. This + allows you to take special actions on certain types of + viruses. + +When a virus was found, the condition sets up an expansion +variable called $malware_name that contains the name of the +virus found. You should use it in a "message" modifier that +contains the error returned to the sender. + +The malware condition caches its results, so when you use it +multiple times, the actual scanning process is only carried +out once. + +If your virus scanner cannot unpack MIME and TNEF containers +itself, you should use the demime condition prior to the +malware condition. + +Here is a simple example: + +/* ---------------------- +deny message = This message contains malware ($malware_name) + demime = * + malware = * +---------------------- */ + +Like with the "spam" condition, you can append '/defer_ok' to +the malware condition to accept messages even if there is a +problem with the virus scanner, like this: + +/* ---------------------- +deny message = This message contains malware ($malware_name) + demime = * + malware = */defer_ok +---------------------- */ + +ADVANCED TIP: When the av_scanner option starts with a dollar +($) sign, it is expanded before being used. This is useful if +you want to use multiple scanners. You can then set + +/* ---------------------- +av_scanner = $acl_m0 +---------------------- */ + +and use these ACL blocks to scan with both sophie and +aveserver scanners: + +/* ---------------------- +deny message = This message contains malware ($malware_name) + set acl_m0 = sophie + malware = * + +deny message = This message contains malware ($malware_name) + set acl_m0 = aveserver + malware = * +---------------------- */ + +However, when av_scanner is expanded, the result caching of +the malware condition is not used, so each malware condition +call results in a new scan of the message. + + + +5. The "demime" facility + MIME unpacking, sanity checking and file extension blocking +-------------------------------------------------------------- + +* This facility provides a simpler interface to MIME decoding +* than the MIME ACL functionality. It is kept in exiscan for +* backward compatability. + +The demime facility unpacks MIME containers in the message. It +detects errors in MIME containers and can match file +extensions found in the message against a list. Using this +facility will produce additional files in the temporary scan +directory that contain the unpacked MIME parts of the message. +If you do antivirus scanning, it is recommened to use the +"demime" condition before the antivirus ("malware") condition. + +The condition name of this facility is "demime". On the right +hand side, you can pass a colon-separated list of file +extensions that it should match against. If one of the file +extensions is found, the condition will return "OK" (or +"true"), otherwise it will return FAIL (or "false"). If there +was any TEMPORARY error while demimeing (mostly "disk full"), +the condition will return DEFER, and the message will be +temporarily rejected. + +The right-hand side gets "expanded" before being treated as a +list, so you can have conditions and lookups there. If it +expands to an empty string, "false", or zero ("0"), no +demimeing is done and the conditions returns FALSE. + +A short example: + +/* ------------ +deny message = Found blacklisted file attachment + demime = vbs:com:bat:pif:prf:lnk +--------------- */ + +When the condition is run, it sets up the following expansion +variables: + + $demime_errorlevel When an error was detected in a MIME + container, this variable contains the + "severity" of the error, as an integer + number. The higher the value, the + more severe the error. If this + variable is unset or zero, no error has + occured. + + $demime_reason When $demime_errorlevel is greater than + zero, this variable contains a human + -readable text string describing the + MIME error that occured. + + $found_extension When the "demime" condition returns + "true", this variable contains the file + extension it has found. + +Both $demime_errorlevel and $demime_reason are set with the +first call of the "demime" condition, and are not changed on +subsequent calls. + +If do not want to check for any file extensions, but rather +use the demime facility for unpacking or error checking +purposes, just pass "*" as the right-hand side value. + +Here is a more elaborate example on how to use this facility: + +/* ----------------- +# Reject messages with serious MIME container errors +deny message = Found MIME error ($demime_reason). + demime = * + condition = ${if >{$demime_errorlevel}{2}{1}{0}} + +# Reject known virus spreading file extensions. +# Accepting these is pretty much braindead. +deny message = contains $found_extension file (blacklisted). + demime = com:vbs:bat:pif:scr + +# Freeze .exe and .doc files. Postmaster can +# examine them and eventually thaw them up. +deny log_message = Another $found_extension file. + demime = exe:doc + control = freeze +--------------------- */ + + + +6. The "fakereject" control statement + Reject a message while really accepting it. +-------------------------------------------------------------- + +When you put "control = fakereject" in an ACL statement, the +following will happen: If exim would have accepted the +message, it will tell the remote host that it did not, with a +message of: + +550-FAKE_REJECT id=xxxxxx-xxxxxx-xx +550-Your message has been rejected but is being kept for evaluation. +550 If it was a legit message, it may still be delivered to the target recipient(s). + +But exim will go on to treat the message as if it had accepted +it. This should be used with extreme caution, please look into +the examples document for possible usage. + + + +7. Brighmail AntiSpam (BMI) suppport +-------------------------------------------------------------- + +Brightmail AntiSpam is a commercial package. Please see +http://www.brightmail.com for more information on +the product. For the sake of clarity, we'll refer to it as +"BMI" from now on. + + +0) BMI concept and implementation overview + +In contrast to how spam-scanning with SpamAssassin is +implemented in exiscan-acl, BMI is more suited for per +-recipient scanning of messages. However, each messages is +scanned only once, but multiple "verdicts" for multiple +recipients can be returned from the BMI server. The exiscan +implementation passes the message to the BMI server just +before accepting it. It then adds the retrieved verdicts to +the messages header file in the spool. These verdicts can then +be queried in routers, where operation is per-recipient +instead of per-message. To use BMI, you need to take the +following steps: + + 1) Compile Exim with BMI support + 2) Set up main BMI options (top section of exim config file) + 3) Set up ACL control statement (ACL section of the config + file) + 4) Set up your routers to use BMI verdicts (routers section + of the config file). + 5) (Optional) Set up per-recipient opt-in information. + +These four steps are explained in more details below. + +1) Adding support for BMI at compile time + + To compile with BMI support, you need to link Exim against + the Brighmail client SDK, consisting of a library + (libbmiclient_single.so) and a header file (bmi_api.h). + You'll also need to explicitly set a flag in the Makefile to + include BMI support in the Exim binary. Both can be achieved + with these 2 lines in Local/Makefile: + + CFLAGS=-DBRIGHTMAIL -I/path/to/the/dir/with/the/includefile + EXTRALIBS_EXIM=-L/path/to/the/dir/with/the/library -lbmiclient_single + + If you use other CFLAGS or EXTRALIBS_EXIM settings then + merge the content of these lines with them. + + Note for BMI6.x users: You'll also have to add -lxml2_single + to the EXTRALIBS_EXIM line. Users of 5.5x do not need to do + this. + + You should also include the location of + libbmiclient_single.so in your dynamic linker configuration + file (usually /etc/ld.so.conf) and run "ldconfig" + afterwards, or else the produced Exim binary will not be + able to find the library file. + + +2) Setting up BMI support in the exim main configuration + + To enable BMI support in the main exim configuration, you + should set the path to the main BMI configuration file with + the "bmi_config_file" option, like this: + + bmi_config_file = /opt/brightmail/etc/brightmail.cfg + + This must go into section 1 of exims configuration file (You + can put it right on top). If you omit this option, it + defaults to /opt/brightmail/etc/brightmail.cfg. + + Note for BMI6.x users: This file is in XML format in V6.xx + and its name is /opt/brightmail/etc/bmiconfig.xml. So BMI + 6.x users MUST set the bmi_config_file option. + + +3) Set up ACL control statement + + To optimize performance, it makes sense only to process + messages coming from remote, untrusted sources with the BMI + server. To set up a messages for processing by the BMI + server, you MUST set the "bmi_run" control statement in any + ACL for an incoming message. You will typically do this in + an "accept" block in the "acl_check_rcpt" ACL. You should + use the "accept" block(s) that accept messages from remote + servers for your own domain(s). Here is an example that uses + the "accept" blocks from exims default configuration file: + + + accept domains = +local_domains + endpass + verify = recipient + control = bmi_run + + accept domains = +relay_to_domains + endpass + verify = recipient + control = bmi_run + + If bmi_run is not set in any ACL during reception of the + message, it will NOT be passed to the BMI server. + + +4) Setting up routers to use BMI verdicts + + When a message has been run through the BMI server, one or + more "verdicts" are present. Different recipients can have + different verdicts. Each recipient is treated individually + during routing, so you can query the verdicts by recipient + at that stage. From Exims view, a verdict can have the + following outcomes: + + o deliver the message normally + o deliver the message to an alternate location + o do not deliver the message + + To query the verdict for a recipient, the implementation + offers the following tools: + + + - Boolean router preconditions. These can be used in any + router. For a simple implementation of BMI, these may be + all that you need. The following preconditions are + available: + + o bmi_deliver_default + + This precondition is TRUE if the verdict for the + recipient is to deliver the message normally. If the + message has not been processed by the BMI server, this + variable defaults to TRUE. + + o bmi_deliver_alternate + + This precondition is TRUE if the verdict for the + recipient is to deliver the message to an alternate + location. You can get the location string from the + $bmi_alt_location expansion variable if you need it. See + further below. If the message has not been processed by + the BMI server, this variable defaults to FALSE. + + o bmi_dont_deliver + + This precondition is TRUE if the verdict for the + recipient is NOT to deliver the message to the + recipient. You will typically use this precondition in a + top-level blackhole router, like this: + + # don't deliver messages handled by the BMI server + bmi_blackhole: + driver = redirect + bmi_dont_deliver + data = :blackhole: + + This router should be on top of all others, so messages + that should not be delivered do not reach other routers + at all. If the message has not been processed by + the BMI server, this variable defaults to FALSE. + + + - A list router precondition to query if rules "fired" on + the message for the recipient. Its name is "bmi_rule". You + use it by passing it a colon-separated list of rule + numbers. You can use this condition to route messages that + matched specific rules. Here is an example: + + # special router for BMI rule #5, #8 and #11 + bmi_rule_redirect: + driver = redirect + bmi_rule = 5:8:11 + data = postmaster@mydomain.com + + + - Expansion variables. Several expansion variables are set + during routing. You can use them in custom router + conditions, for example. The following variables are + available: + + o $bmi_base64_verdict + + This variable will contain the BASE64 encoded verdict + for the recipient being routed. You can use it to add a + header to messages for tracking purposes, for example: + + localuser: + driver = accept + check_local_user + headers_add = X-Brightmail-Verdict: $bmi_base64_verdict + transport = local_delivery + + If there is no verdict available for the recipient being + routed, this variable contains the empty string. + + o $bmi_base64_tracker_verdict + + This variable will contain a BASE64 encoded subset of + the verdict information concerning the "rules" that + fired on the message. You can add this string to a + header, commonly named "X-Brightmail-Tracker". Example: + + localuser: + driver = accept + check_local_user + headers_add = X-Brightmail-Tracker: $bmi_base64_tracker_verdict + transport = local_delivery + + If there is no verdict available for the recipient being + routed, this variable contains the empty string. + + o $bmi_alt_location + + If the verdict is to redirect the message to an + alternate location, this variable will contain the + alternate location string returned by the BMI server. In + its default configuration, this is a header-like string + that can be added to the message with "headers_add". If + there is no verdict available for the recipient being + routed, or if the message is to be delivered normally, + this variable contains the empty string. + + o $bmi_deliver + + This is an additional integer variable that can be used + to query if the message should be delivered at all. You + should use router preconditions instead if possible. + + $bmi_deliver is '0': the message should NOT be delivered. + $bmi_deliver is '1': the message should be delivered. + + + IMPORTANT NOTE: Verdict inheritance. + The message is passed to the BMI server during message + reception, using the target addresses from the RCPT TO: + commands in the SMTP transaction. If recipients get expanded + or re-written (for example by aliasing), the new address(es) + inherit the verdict from the original address. This means + that verdicts also apply to all "child" addresses generated + from top-level addresses that were sent to the BMI server. + + +5) Using per-recipient opt-in information (Optional) + + The BMI server features multiple scanning "profiles" for + individual recipients. These are usually stored in a LDAP + server and are queried by the BMI server itself. However, + you can also pass opt-in data for each recipient from the + MTA to the BMI server. This is particularly useful if you + already look up recipient data in exim anyway (which can + also be stored in a SQL database or other source). This + implementation enables you to pass opt-in data to the BMI + server in the RCPT ACL. This works by setting the + 'bmi_optin' modifier in a block of that ACL. If should be + set to a list of comma-separated strings that identify the + features which the BMI server should use for that particular + recipient. Ideally, you would use the 'bmi_optin' modifier + in the same ACL block where you set the 'bmi_run' control + flag. Here is an example that will pull opt-in data for each + recipient from a flat file called + '/etc/exim/bmi_optin_data'. + + The file format: + + user1@mydomain.com: : + user2@thatdomain.com: + + + The example: + + accept domains = +relay_to_domains + endpass + verify = recipient + bmi_optin = ${lookup{$local_part@$domain}lsearch{/etc/exim/bmi_optin_data}} + control = bmi_run + + Of course, you can also use any other lookup method that + exim supports, including LDAP, Postgres, MySQL, Oracle etc., + as long as the result is a list of colon-separated opt-in + strings. + + For a list of available opt-in strings, please contact your + Brightmail representative. + + + + +8. Sender Policy Framework (SPF) support +-------------------------------------------------------------- + +To learn more about SPF, visit http://spf.pobox.com. This +document does not explain the SPF fundamentals, you should +read and understand the implications of deploying SPF on your +system before doing so. + +SPF support is added via the libspf2 library. Visit + + http://www.libspf2.org/ + +to obtain a copy, then compile and install it. By default, +this will put headers in /usr/local/include and the static +library in /usr/local/lib. + +To compile exim with SPF support, set these additional flags in +Local/Makefile: + +CFLAGS=-DSPF -I/usr/local/include +EXTRALIBS_EXIM=-L/usr/local/lib -lspf2 + +This assumes that the libspf2 files are installed in +their default locations. + +You can now run SPF checks in incoming SMTP by using the "spf" +ACL condition in either the MAIL, RCPT or DATA ACLs. When +using it in the RCPT ACL, you can make the checks dependend on +the RCPT address (or domain), so you can check SPF records +only for certain target domains. This gives you the +possibility to opt-out certain customers that do not want +their mail to be subject to SPF checking. + +The spf condition takes a list of strings on its right-hand +side. These strings describe the outcome of the SPF check for +which the spf condition should succeed. Valid strings are: + + o pass The SPF check passed, the sending host + is positively verified by SPF. + o fail The SPF check failed, the sending host + is NOT allowed to send mail for the domain + in the envelope-from address. + o softfail The SPF check failed, but the queried + domain can't absolutely confirm that this + is a forgery. + o none The queried domain does not publish SPF + records. + o neutral The SPF check returned a "neutral" state. + This means the queried domain has published + a SPF record, but wants to allow outside + servers to send mail under its domain as well. + o err_perm This indicates a syntax error in the SPF + record of the queried domain. This should be + treated like "none". + o err_temp This indicates a temporary error during all + processing, including exim's SPF processing. + You may defer messages when this occurs. + +You can prefix each string with an exclamation mark to invert +is meaning, for example "!fail" will match all results but +"fail". The string list is evaluated left-to-right, in a +short-circuit fashion. When a string matches the outcome of +the SPF check, the condition succeeds. If none of the listed +strings matches the outcome of the SPF check, the condition +fails. + +Here is a simple example to fail forgery attempts from domains +that publish SPF records: + +/* ----------------- +deny message = $sender_host_address is not allowed to send mail from $sender_address_domain + spf = fail +--------------------- */ + +You can also give special treatment to specific domains: + +/* ----------------- +deny message = AOL sender, but not from AOL-approved relay. + sender_domains = aol.com + spf = fail:neutral +--------------------- */ + +Explanation: AOL publishes SPF records, but is liberal and +still allows non-approved relays to send mail from aol.com. +This will result in a "neutral" state, while mail from genuine +AOL servers will result in "pass". The example above takes +this into account and treats "neutral" like "fail", but only +for aol.com. Please note that this violates the SPF draft. + +When the spf condition has run, it sets up several expansion +variables. + + $spf_header_comment + This contains a human-readable string describing the outcome + of the SPF check. You can add it to a custom header or use + it for logging purposes. + + $spf_received + This contains a complete SPF-Received: header that can be + added to the message. Please note that according to the SPF + draft, this header must be added at the top of the header + list. Please see section 10 on how you can do this. + + $spf_result + This contains the outcome of the SPF check in string form, + one of pass, fail, softfail, none, neutral, err_perm or + err_temp. + + $spf_smtp_comment + This contains a string that can be used in a SMTP response + to the calling party. Useful for "fail". + + + +9. SRS (Sender Rewriting Scheme) Support +-------------------------------------------------------------- + +Exiscan currently includes SRS support via Miles Wilton's +libsrs_alt library. His patch to link to that library is +included in exiscan. The current version of the supported +library is 0.5. + +In order to use SRS, you must get a copy of libsrs_alt from + +http://srs.mirtol.com/ + +Unpack the tarball, then refer to MTAs/README.EXIM +to proceed. + +The SRS support is subject to change and should be considered +experimental. SRS itself is still subject to changes. + + + +10. Selecting header positions when adding headers in ACLs +--------------------------------------------------------------\ + +Exim introduced selectable header positions in Version 4.43. +One option allows you to add your header after all Received: +headers. For optimal SPF draft/RFC2822 compliance, exiscan-acl +adds one other option, called "spf-received". It adds your +header before the first header which is NOT Received: or +Resent-*:, seen from the top of the header stack. + +Example: + +warn message = :spf_received:$spf_received + +-------------------------------------------------------------- +End of file +-------------------------------------------------------------- diff -urN exim-4.43-orig/exim_monitor/em_globals.c exim-4.43/exim_monitor/em_globals.c --- exim-4.43-orig/exim_monitor/em_globals.c Tue Oct 5 10:32:08 2004 +++ exim-4.43/exim_monitor/em_globals.c Wed Oct 6 11:49:02 2004 @@ -42,6 +42,10 @@ uschar *action_required; uschar *alternate_config = NULL; +#ifdef BRIGHTMAIL +int bmi_run = 0; +uschar *bmi_verdicts = NULL; +#endif int body_max = 20000; uschar *exim_path = US BIN_DIRECTORY "/exim" @@ -130,6 +134,8 @@ BOOL deliver_manual_thaw = FALSE; BOOL dont_deliver = FALSE; +BOOL fake_reject = FALSE; + header_line *header_last = NULL; header_line *header_list = NULL; @@ -139,6 +145,7 @@ BOOL local_error_message = FALSE; uschar *local_scan_data = NULL; +uschar *spam_score_int = NULL; BOOL log_timezone = FALSE; int message_age = 0; uschar *message_id; diff -urN exim-4.43-orig/scripts/MakeLinks exim-4.43/scripts/MakeLinks --- exim-4.43-orig/scripts/MakeLinks Tue Oct 5 10:32:08 2004 +++ exim-4.43/scripts/MakeLinks Wed Oct 6 11:49:02 2004 @@ -173,19 +173,27 @@ # but local_scan.c does not, because its location is taken from the build-time # configuration. Likewise for the os.c file, which gets build dynamically. +ln -s ../src/bmi_spam.h bmi_spam.h ln -s ../src/dbfunctions.h dbfunctions.h ln -s ../src/dbstuff.h dbstuff.h +ln -s ../src/demime.h demime.h ln -s ../src/exim.h exim.h ln -s ../src/functions.h functions.h ln -s ../src/globals.h globals.h ln -s ../src/local_scan.h local_scan.h ln -s ../src/macros.h macros.h +ln -s ../src/mime.h mime.h ln -s ../src/mytypes.h mytypes.h ln -s ../src/osfunctions.h osfunctions.h +ln -s ../src/spam.h spam.h +ln -s ../src/spf.h spf.h +ln -s ../src/srs.h srs.h ln -s ../src/store.h store.h ln -s ../src/structs.h structs.h +ln -s ../src/tnef.h tnef.h ln -s ../src/acl.c acl.c +ln -s ../src/bmi_spam.c bmi_spam.c ln -s ../src/buildconfig.c buildconfig.c ln -s ../src/child.c child.c ln -s ../src/crypt16.c crypt16.c @@ -193,6 +201,7 @@ ln -s ../src/dbfn.c dbfn.c ln -s ../src/debug.c debug.c ln -s ../src/deliver.c deliver.c +ln -s ../src/demime.c demime.c ln -s ../src/directory.c directory.c ln -s ../src/dns.c dns.c ln -s ../src/drtables.c drtables.c @@ -211,7 +220,9 @@ ln -s ../src/ip.c ip.c ln -s ../src/log.c log.c ln -s ../src/lss.c lss.c +ln -s ../src/malware.c malware.c ln -s ../src/match.c match.c +ln -s ../src/mime.c mime.c ln -s ../src/moan.c moan.c ln -s ../src/parse.c parse.c ln -s ../src/perl.c perl.c @@ -219,6 +230,7 @@ ln -s ../src/rda.c rda.c ln -s ../src/readconf.c readconf.c ln -s ../src/receive.c receive.c +ln -s ../src/regex.c regex.c ln -s ../src/retry.c retry.c ln -s ../src/rewrite.c rewrite.c ln -s ../src/rfc2047.c rfc2047.c @@ -227,13 +239,18 @@ ln -s ../src/sieve.c sieve.c ln -s ../src/smtp_in.c smtp_in.c ln -s ../src/smtp_out.c smtp_out.c +ln -s ../src/spam.c spam.c +ln -s ../src/spf.c spf.c ln -s ../src/spool_in.c spool_in.c +ln -s ../src/spool_mbox.c spool_mbox.c ln -s ../src/spool_out.c spool_out.c +ln -s ../src/srs.c srs.c ln -s ../src/store.c store.c ln -s ../src/string.c string.c ln -s ../src/tls.c tls.c ln -s ../src/tls-gnu.c tls-gnu.c ln -s ../src/tls-openssl.c tls-openssl.c +ln -s ../src/tnef.c tnef.c ln -s ../src/tod.c tod.c ln -s ../src/transport.c transport.c ln -s ../src/tree.c tree.c diff -urN exim-4.43-orig/src/acl.c exim-4.43/src/acl.c --- exim-4.43-orig/src/acl.c Tue Oct 5 10:32:08 2004 +++ exim-4.43/src/acl.c Wed Oct 6 14:55:05 2004 @@ -7,8 +7,13 @@ /* Code for handling Access Control Lists (ACLs) */ +/* This file has been modified by the exiscan-acl patch. */ + #include "exim.h" +#ifdef BRIGHTMAIL +#include "bmi_spam.h" +#endif /* Default callout timeout */ @@ -32,19 +37,33 @@ /* ACL condition and modifier codes - keep in step with the table that follows. */ -enum { ACLC_ACL, ACLC_AUTHENTICATED, ACLC_CONDITION, ACLC_CONTROL, ACLC_DELAY, +enum { ACLC_ACL, ACLC_AUTHENTICATED, +#ifdef BRIGHTMAIL + ACLC_BMI_OPTIN, +#endif + ACLC_CONDITION, ACLC_CONTROL, ACLC_DECODE, ACLC_DELAY, ACLC_DEMIME, ACLC_DNSLISTS, ACLC_DOMAINS, ACLC_ENCRYPTED, ACLC_ENDPASS, ACLC_HOSTS, - ACLC_LOCAL_PARTS, ACLC_LOG_MESSAGE, ACLC_LOGWRITE, ACLC_MESSAGE, - ACLC_RECIPIENTS, ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET, ACLC_VERIFY }; + ACLC_LOCAL_PARTS, ACLC_LOG_MESSAGE, ACLC_LOGWRITE, ACLC_MALWARE, ACLC_MESSAGE, ACLC_MIME_REGEX, + ACLC_RECIPIENTS, ACLC_REGEX, ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET, ACLC_SPAM, +#ifdef SPF + ACLC_SPF, +#endif + ACLC_VERIFY }; /* ACL conditions/modifiers: "delay", "control", "endpass", "message", "log_message", "logwrite", and "set" are modifiers that look like conditions but always return TRUE. They are used for their side effects. */ -static uschar *conditions[] = { US"acl", US"authenticated", US"condition", - US"control", US"delay", US"dnslists", US"domains", US"encrypted", - US"endpass", US"hosts", US"local_parts", US"log_message", US"logwrite", - US"message", US"recipients", US"sender_domains", US"senders", US"set", +static uschar *conditions[] = { US"acl", US"authenticated", +#ifdef BRIGHTMAIL + US"bmi_optin", +#endif + US"condition", US"control", US"decode", US"delay", US"demime", US"dnslists", US"domains", US"encrypted", + US"endpass", US"hosts", US"local_parts", US"log_message", US"logwrite", US"malware", + US"message", US"mime_regex", US"recipients", US"regex", US"sender_domains", US"senders", US"set", US"spam", +#ifdef SPF + US"spf", +#endif US"verify" }; /* Flags to indicate for which conditions /modifiers a string expansion is done @@ -54,9 +73,14 @@ static uschar cond_expand_at_top[] = { TRUE, /* acl */ FALSE, /* authenticated */ +#ifdef BRIGHTMAIL + TRUE, /* bmi_optin */ +#endif TRUE, /* condition */ TRUE, /* control */ + TRUE, /* decode */ TRUE, /* delay */ + TRUE, /* demime */ TRUE, /* dnslists */ FALSE, /* domains */ FALSE, /* encrypted */ @@ -65,11 +89,18 @@ FALSE, /* local_parts */ TRUE, /* log_message */ TRUE, /* logwrite */ + TRUE, /* malware */ TRUE, /* message */ + TRUE, /* mime_regex */ FALSE, /* recipients */ + TRUE, /* regex */ FALSE, /* sender_domains */ FALSE, /* senders */ TRUE, /* set */ + TRUE, /* spam */ +#ifdef SPF + TRUE, /* spf */ +#endif TRUE /* verify */ }; @@ -78,9 +109,14 @@ static uschar cond_modifiers[] = { FALSE, /* acl */ FALSE, /* authenticated */ +#ifdef BRIGHTMAIL + TRUE, /* bmi_optin */ +#endif FALSE, /* condition */ TRUE, /* control */ + FALSE, /* decode */ TRUE, /* delay */ + FALSE, /* demime */ FALSE, /* dnslists */ FALSE, /* domains */ FALSE, /* encrypted */ @@ -89,11 +125,18 @@ FALSE, /* local_parts */ TRUE, /* log_message */ TRUE, /* log_write */ + FALSE, /* malware */ TRUE, /* message */ + FALSE, /* mime_regex */ FALSE, /* recipients */ + FALSE, /* regex */ FALSE, /* sender_domains */ FALSE, /* senders */ TRUE, /* set */ + FALSE, /* spam */ +#ifdef SPF + FALSE, /* spf */ +#endif FALSE /* verify */ }; @@ -102,15 +145,45 @@ static unsigned int cond_forbids[] = { 0, /* acl */ + (1<next) { @@ -1325,6 +1456,12 @@ case ACLC_CONTROL: switch (decode_control(arg, &p, where, log_msgptr)) { +#ifdef BRIGHTMAIL + case CONTROL_BMI_RUN: + bmi_run = 1; + break; +#endif + case CONTROL_ERROR: return ERROR; @@ -1340,10 +1477,18 @@ smtp_enforce_sync = TRUE; break; + case CONTROL_FAKEREJECT: + fake_reject = TRUE; + break; + case CONTROL_NO_ENFORCE_SYNC: smtp_enforce_sync = FALSE; break; + case CONTROL_NO_MBOX_UNSPOOL: + no_mbox_unspool = TRUE; + break; + case CONTROL_NO_MULTILINE: no_multiline_responses = TRUE; break; @@ -1397,7 +1542,76 @@ break; case ACLC_DNSLISTS: - rc = verify_check_dnsbl(&arg); + rc = verify_check_dnsbl(&arg); + break; + +#ifdef BRIGHTMAIL + case ACLC_BMI_OPTIN: + { + int old_pool = store_pool; + store_pool = POOL_PERM; + bmi_current_optin = string_copy(arg); + store_pool = old_pool; + } + break; +#endif + + case ACLC_DECODE: + rc = mime_decode(&arg); + break; + + case ACLC_MIME_REGEX: + rc = mime_regex(&arg); + break; + + case ACLC_DEMIME: + rc = demime(&arg); + break; + +#ifdef SPF + case ACLC_SPF: + rc = spf_process(&arg, sender_address); + break; +#endif + + case ACLC_MALWARE: + { + /* Seperate the regular expression and any optional parameters. */ + uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size); + /* Run the malware backend. */ + rc = malware(&ss); + /* Modify return code based upon the existance of options. */ + while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)) + != NULL) { + if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) + { + /* FAIL so that the message is passed to the next ACL */ + rc = FAIL; + } + } + } + break; + + case ACLC_SPAM: + { + /* Seperate the regular expression and any optional parameters. */ + uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size); + /* Run the spam backend. */ + rc = spam(&ss); + /* Modify return code based upon the existance of options. */ + while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)) + != NULL) { + if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) + { + /* FAIL so that the message is passed to the next ACL */ + rc = FAIL; + } + } + } + break; + + case ACLC_REGEX: + rc = regex(&arg); break; case ACLC_DOMAINS: diff -urN exim-4.43-orig/src/auths/pwcheck.c exim-4.43/src/auths/pwcheck.c --- exim-4.43-orig/src/auths/pwcheck.c Tue Oct 5 10:32:08 2004 +++ exim-4.43/src/auths/pwcheck.c Wed Oct 6 11:49:02 2004 @@ -1,7 +1,7 @@ /* SASL server API implementation * Rob Siemborski * Tim Martin - * $Id: checkpw.c,v 1.49 2002/03/07 19:14:04 ken3 Exp $ + * $Id: pwcheck.c,v 1.1.1.1 2004/06/24 07:46:30 tkistner Exp $ */ /* * Copyright (c) 2001 Carnegie Mellon University. All rights reserved. diff -urN exim-4.43-orig/src/bmi_spam.c exim-4.43/src/bmi_spam.c --- exim-4.43-orig/src/bmi_spam.c Thu Jan 1 01:00:00 1970 +++ exim-4.43/src/bmi_spam.c Wed Oct 6 11:49:02 2004 @@ -0,0 +1,474 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner + patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2003-???? */ +/* License: GPL */ + +/* Code for calling Brightmail AntiSpam. */ + +#include "exim.h" +#include "bmi_spam.h" + +#ifdef BRIGHTMAIL + +uschar *bmi_current_optin = NULL; + +uschar *bmi_process_message(header_line *header_list, int data_fd) { + BmiSystem *system = NULL; + BmiMessage *message = NULL; + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + const BmiVerdict *verdict = NULL; + FILE *data_file; + uschar data_buffer[4096]; + uschar localhost[] = "127.0.0.1"; + uschar *host_address; + uschar *verdicts = NULL; + int i,j; + + err = bmiInitSystem(BMI_VERSION, (char *)bmi_config_file, &system); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: could not initialize Brightmail system.", (int)err_loc, (int)err_type); + return NULL; + } + + err = bmiInitMessage(system, &message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: could not initialize Brightmail message.", (int)err_loc, (int)err_type); + bmiFreeSystem(system); + return NULL; + } + + /* Send IP address of sending host */ + if (sender_host_address == NULL) + host_address = localhost; + else + host_address = sender_host_address; + err = bmiProcessConnection((char *)host_address, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiProcessConnection() failed (IP %s).", (int)err_loc, (int)err_type, (char *)host_address); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send envelope sender address */ + err = bmiProcessFROM((char *)sender_address, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiProcessFROM() failed (address %s).", (int)err_loc, (int)err_type, (char *)sender_address); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send envelope recipients */ + for(i=0;ibmi_optin != NULL) && (Ustrlen(r->bmi_optin) > 1)) { + debug_printf("passing bmiOptin string: %s\n", r->bmi_optin); + bmiOptinInit(&optin); + err = bmiOptinMset(optin, r->bmi_optin, ':'); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + log_write(0, LOG_PANIC|LOG_MAIN, + "bmi warning: [loc %d type %d]: bmiOptinMSet() failed (address '%s', string '%s').", (int)err_loc, (int)err_type, (char *)r->address, (char *)r->bmi_optin); + if (optin != NULL) + bmiOptinFree(optin); + optin = NULL; + }; + }; + + err = bmiAccumulateTO((char *)r->address, optin, message); + + if (optin != NULL) + bmiOptinFree(optin); + + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiAccumulateTO() failed (address %s).", (int)err_loc, (int)err_type, (char *)r->address); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + }; + err = bmiEndTO(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndTO() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send message headers */ + while (header_list != NULL) { + /* skip deleted headers */ + if (header_list->type == '*') { + header_list = header_list->next; + continue; + }; + err = bmiAccumulateHeaders((const char *)header_list->text, header_list->slen, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiAccumulateHeaders() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + header_list = header_list->next; + }; + err = bmiEndHeaders(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndHeaders() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send body */ + data_file = fdopen(data_fd,"r"); + do { + j = fread(data_buffer, 1, sizeof(data_buffer), data_file); + if (j > 0) { + err = bmiAccumulateBody((const char *)data_buffer, j, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiAccumulateBody() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + }; + } while (j > 0); + err = bmiEndBody(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndBody() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + + /* End message */ + err = bmiEndMessage(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndMessage() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* get store for the verdict string */ + verdicts = store_get(1); + *verdicts = '\0'; + + for ( err = bmiAccessFirstVerdict(message, &verdict); + verdict != NULL; + err = bmiAccessNextVerdict(message, verdict, &verdict) ) { + char *verdict_str; + + err = bmiCreateStrFromVerdict(verdict,&verdict_str); + if (!store_extend(verdicts, Ustrlen(verdicts)+1, Ustrlen(verdicts)+1+strlen(verdict_str)+1)) { + /* can't allocate more store */ + return NULL; + }; + if (*verdicts != '\0') + Ustrcat(verdicts, US ":"); + Ustrcat(verdicts, US verdict_str); + bmiFreeStr(verdict_str); + }; + + DEBUG(D_receive) debug_printf("bmi verdicts: %s\n", verdicts); + + if (Ustrlen(verdicts) == 0) + return NULL; + else + return verdicts; +} + + +int bmi_get_delivery_status(uschar *base64_verdict) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + int rc = 1; /* deliver by default */ + + /* always deliver when there is no verdict */ + if (base64_verdict == NULL) + return 1; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return 1; + }; + + err = bmiVerdictError(verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + /* deliver normally due to error */ + rc = 1; + } + else if (bmiVerdictDestinationIsDefault(verdict) == BMI_TRUE) { + /* deliver normally */ + rc = 1; + } + else if (bmiVerdictAccessDestination(verdict) == NULL) { + /* do not deliver */ + rc = 0; + } + else { + /* deliver to alternate location */ + rc = 1; + }; + + bmiFreeVerdict(verdict); + return rc; +} + + +uschar *bmi_get_alt_location(uschar *base64_verdict) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + uschar *rc = NULL; + + /* always deliver when there is no verdict */ + if (base64_verdict == NULL) + return NULL; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return NULL; + }; + + err = bmiVerdictError(verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + /* deliver normally due to error */ + rc = NULL; + } + else if (bmiVerdictDestinationIsDefault(verdict) == BMI_TRUE) { + /* deliver normally */ + rc = NULL; + } + else if (bmiVerdictAccessDestination(verdict) == NULL) { + /* do not deliver */ + rc = NULL; + } + else { + /* deliver to alternate location */ + rc = store_get(strlen(bmiVerdictAccessDestination(verdict))+1); + Ustrcpy(rc, bmiVerdictAccessDestination(verdict)); + rc[strlen(bmiVerdictAccessDestination(verdict))] = '\0'; + }; + + bmiFreeVerdict(verdict); + return rc; +} + +uschar *bmi_get_base64_verdict(uschar *bmi_local_part, uschar *bmi_domain) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + const BmiRecipient *recipient = NULL; + const char *verdict_str = NULL; + uschar *verdict_ptr; + uschar *verdict_buffer = NULL; + int sep = 0; + + /* return nothing if there are no verdicts available */ + if (bmi_verdicts == NULL) + return NULL; + + /* allocate room for the b64 verdict string */ + verdict_buffer = store_get(Ustrlen(bmi_verdicts)+1); + + /* loop through verdicts */ + verdict_ptr = bmi_verdicts; + while ((verdict_str = (const char *)string_nextinlist(&verdict_ptr, &sep, + verdict_buffer, + Ustrlen(bmi_verdicts)+1)) != NULL) { + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(verdict_str, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, verdict_str); + return NULL; + }; + + /* loop through rcpts for this verdict */ + for ( recipient = bmiVerdictAccessFirstRecipient(verdict); + recipient != NULL; + recipient = bmiVerdictAccessNextRecipient(verdict, recipient)) { + uschar *rcpt_local_part; + uschar *rcpt_domain; + + /* compare address against our subject */ + rcpt_local_part = (unsigned char *)bmiRecipientAccessAddress(recipient); + rcpt_domain = Ustrchr(rcpt_local_part,'@'); + if (rcpt_domain == NULL) { + rcpt_domain = US""; + } + else { + *rcpt_domain = '\0'; + rcpt_domain++; + }; + + if ( (strcmpic(rcpt_local_part, bmi_local_part) == 0) && + (strcmpic(rcpt_domain, bmi_domain) == 0) ) { + /* found verdict */ + bmiFreeVerdict(verdict); + return (uschar *)verdict_str; + }; + }; + + bmiFreeVerdict(verdict); + }; + + return NULL; +} + + +uschar *bmi_get_base64_tracker_verdict(uschar *base64_verdict) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + uschar *rc = NULL; + + /* always deliver when there is no verdict */ + if (base64_verdict == NULL) + return NULL; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return NULL; + }; + + /* create old tracker string from verdict */ + err = bmiCreateOldStrFromVerdict(verdict, &rc); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateOldStrFromVerdict() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return NULL; + }; + + bmiFreeVerdict(verdict); + return rc; +} + + +int bmi_check_rule(uschar *base64_verdict, uschar *option_list) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + int rc = 0; + uschar *rule_num; + uschar *rule_ptr; + uschar rule_buffer[32]; + int sep = 0; + + + /* no verdict -> no rule fired */ + if (base64_verdict == NULL) + return 0; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return 0; + }; + + err = bmiVerdictError(verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + /* error -> no rule fired */ + bmiFreeVerdict(verdict); + return 0; + } + + /* loop through numbers */ + rule_ptr = option_list; + while ((rule_num = string_nextinlist(&rule_ptr, &sep, + rule_buffer, 32)) != NULL) { + int rule_int = -1; + + /* try to translate to int */ + sscanf(rule_num, "%d", &rule_int); + if (rule_int > 0) { + debug_printf("checking rule #%d\n", rule_int); + /* check if rule fired on the message */ + if (bmiVerdictRuleFired(verdict, rule_int) == BMI_TRUE) { + debug_printf("rule #%d fired\n", rule_int); + rc = 1; + break; + }; + }; + }; + + bmiFreeVerdict(verdict); + return rc; +}; + +#endif diff -urN exim-4.43-orig/src/bmi_spam.h exim-4.43/src/bmi_spam.h --- exim-4.43-orig/src/bmi_spam.h Thu Jan 1 01:00:00 1970 +++ exim-4.43/src/bmi_spam.h Wed Oct 6 11:49:02 2004 @@ -0,0 +1,26 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner + patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2003-???? */ +/* License: GPL */ + +/* Code for calling Brightmail AntiSpam. */ + +#ifdef BRIGHTMAIL + +#include + +extern uschar *bmi_process_message(header_line *, int); +extern uschar *bmi_get_base64_verdict(uschar *, uschar *); +extern uschar *bmi_get_base64_tracker_verdict(uschar *); +extern int bmi_get_delivery_status(uschar *); +extern uschar *bmi_get_alt_location(uschar *); +extern int bmi_check_rule(uschar *,uschar *); + +extern uschar *bmi_current_optin; + +#endif diff -urN exim-4.43-orig/src/config.h.defaults exim-4.43/src/config.h.defaults --- exim-4.43-orig/src/config.h.defaults Tue Oct 5 10:32:08 2004 +++ exim-4.43/src/config.h.defaults Wed Oct 6 11:49:02 2004 @@ -50,7 +50,7 @@ #define HAVE_CRYPT16 #define HAVE_SA_LEN #define HEADERS_CHARSET "ISO-8859-1" -#define HEADER_ADD_BUFFER_SIZE 8192 +#define HEADER_ADD_BUFFER_SIZE (8192 * 4) #define HEADER_MAXSIZE (1024*1024) #define INPUT_DIRECTORY_MODE 0750 @@ -104,7 +104,7 @@ #define SPOOL_DIRECTORY #define SPOOL_DIRECTORY_MODE 0750 #define SPOOL_MODE 0640 -#define STRING_SPRINTF_BUFFER_SIZE 8192 +#define STRING_SPRINTF_BUFFER_SIZE (8192 * 4) #define SUPPORT_A6 #define SUPPORT_CRYPTEQ diff -urN exim-4.43-orig/src/configure.default exim-4.43/src/configure.default --- exim-4.43-orig/src/configure.default Tue Oct 5 10:32:08 2004 +++ exim-4.43/src/configure.default Wed Oct 6 11:49:02 2004 @@ -108,6 +108,26 @@ # You should not change that setting until you understand how ACLs work. +# The following ACL entries are used if you want to do content scanning with +# the exiscan-acl patch. When you uncomment one of these lines, you must also +# review the respective entries in the ACL section further below. + +# acl_smtp_mime = acl_check_mime +# acl_smtp_data = acl_check_content + +# This configuration variable defines the virus scanner that is used with +# the 'malware' ACL condition of the exiscan acl-patch. If you do not use +# virus scanning, leave it commented. Please read doc/exiscan-acl-readme.txt +# for a list of supported scanners. + +# av_scanner = sophie:/var/run/sophie + +# The following setting is only needed if you use the 'spam' ACL condition +# of the exiscan-acl patch. It specifies on which host and port the SpamAssassin +# "spamd" daemon is listening. If you do not use this condition, or you use +# the default of "127.0.0.1 783", you can omit this option. + +# spamd_address = 127.0.0.1 783 # Specify the domain you want to be added to all unqualified addresses # here. An unqualified address is one that does not contain an "@" character @@ -342,6 +362,56 @@ deny message = relay not permitted +# These access control lists are used for content scanning with the exiscan-acl +# patch. You must also uncomment the entries for acl_smtp_data and acl_smtp_mime +# (scroll up), otherwise the ACLs will not be used. IMPORTANT: the default entries here +# should be treated as EXAMPLES. You MUST read the file doc/exiscan-acl-spec.txt +# to fully understand what you are doing ... + +acl_check_mime: + + # Decode MIME parts to disk. This will support virus scanners later. + warn decode = default + + # File extension filtering. + deny message = Blacklisted file extension detected + condition = ${if match \ + {${lc:$mime_filename}} \ + {\N(\.exe|\.pif|\.bat|\.scr|\.lnk|\.com)$\N} \ + {1}{0}} + + # Reject messages that carry chinese character sets. + # WARNING: This is an EXAMPLE. + deny message = Sorry, noone speaks chinese here + condition = ${if eq{$mime_charset}{gb2312}{1}{0}} + + accept + +acl_check_content: + + # Reject virus infested messages. + deny message = This message contains malware ($malware_name) + malware = * + + # Always add X-Spam-Score and X-Spam-Report headers, using SA system-wide settings + # (user "nobody"), no matter if over threshold or not. + warn message = X-Spam-Score: $spam_score ($spam_bar) + spam = nobody:true + warn message = X-Spam-Report: $spam_report + spam = nobody:true + + # Add X-Spam-Flag if spam is over system-wide threshold + warn message = X-Spam-Flag: YES + spam = nobody + + # Reject spam messages with score over 10, using an extra condition. + deny message = This message scored $spam_score points. Congratulations! + spam = nobody:true + condition = ${if >{$spam_score_int}{100}{1}{0}} + + # finally accept all the rest + accept + ###################################################################### # ROUTERS CONFIGURATION # diff -urN exim-4.43-orig/src/deliver.c exim-4.43/src/deliver.c --- exim-4.43-orig/src/deliver.c Tue Oct 5 10:32:08 2004 +++ exim-4.43/src/deliver.c Wed Oct 6 11:49:02 2004 @@ -10,6 +10,9 @@ #include "exim.h" +#ifdef BRIGHTMAIL +#include "bmi_spam.h" +#endif /* Data block for keeping track of subprocesses for parallel remote delivery. */ @@ -154,6 +157,13 @@ deliver_domain = addr->domain; self_hostname = addr->self_hostname; +#ifdef BRIGHTMAIL +bmi_deliver = 1; /* deliver by default */ +bmi_alt_location = NULL; +bmi_base64_verdict = NULL; +bmi_base64_tracker_verdict = NULL; +#endif + /* If there's only one address we can set everything. */ if (addr->next == NULL) @@ -203,6 +213,19 @@ deliver_localpart_suffix = addr->parent->suffix; } } + +#ifdef BRIGHTMAIL + /* Set expansion variables related to Brightmail AntiSpam */ + bmi_base64_verdict = bmi_get_base64_verdict(deliver_localpart_orig, deliver_domain_orig); + bmi_base64_tracker_verdict = bmi_get_base64_tracker_verdict(bmi_base64_verdict); + /* get message delivery status (0 - don't deliver | 1 - deliver) */ + bmi_deliver = bmi_get_delivery_status(bmi_base64_verdict); + /* if message is to be delivered, get eventual alternate location */ + if (bmi_deliver == 1) { + bmi_alt_location = bmi_get_alt_location(bmi_base64_verdict); + }; +#endif + } /* For multiple addresses, don't set local part, and leave the domain and diff -urN exim-4.43-orig/src/demime.c exim-4.43/src/demime.c --- exim-4.43-orig/src/demime.c Thu Jan 1 01:00:00 1970 +++ exim-4.43/src/demime.c Wed Oct 6 11:49:02 2004 @@ -0,0 +1,1276 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* This file is part of the exiscan-acl content scanner +patch. It is NOT part of the standard exim distribution. */ + +/* Copyright (c) Tom Kistner 2003-???? */ +/* License: GPL */ + +/* Code for unpacking MIME containers. Called from acl.c. */ + +#include "exim.h" +#include "demime.h" + +uschar demime_reason_buffer[1024]; +struct file_extension *file_extensions = NULL; + +int demime(uschar **listptr) { + int sep = 0; + uschar *list = *listptr; + uschar *option; + uschar option_buffer[64]; + unsigned long long mbox_size; + FILE *mbox_file; + uschar defer_error_buffer[1024]; + int demime_rc = 0; + + /* reset found_extension variable */ + found_extension = NULL; + + /* try to find 1st option */ + if ((option = string_nextinlist(&list, &sep, + option_buffer, + sizeof(option_buffer))) != NULL) { + + /* parse 1st option */ + if ( (Ustrcmp(option,"false") == 0) || (Ustrcmp(option,"0") == 0) ) { + /* explicitly no demimeing */ + return FAIL; + }; + } + else { + /* no options -> no demimeing */ + return FAIL; + }; + + /* make sure the eml mbox file is spooled up */ + mbox_file = spool_mbox(&mbox_size); + + if (mbox_file == NULL) { + /* error while spooling */ + log_write(0, LOG_MAIN|LOG_PANIC, + "demime acl condition: error while creating mbox spool file"); + return DEFER; + }; + + /* call demimer if not already done earlier */ + if (!demime_ok) + demime_rc = mime_demux(mbox_file, defer_error_buffer); + + fclose(mbox_file); + + if (demime_rc == DEFER) { + /* temporary failure (DEFER => DEFER) */ + log_write(0, LOG_MAIN, + "demime acl condition: %s", defer_error_buffer); + return DEFER; + }; + + /* set demime_ok to avoid unpacking again */ + demime_ok = 1; + + /* check for file extensions, if there */ + while (option != NULL) { + struct file_extension *this_extension = file_extensions; + + /* Look for the wildcard. If it is found, we always return true. + The user must then use a custom condition to evaluate demime_errorlevel */ + if (Ustrcmp(option,"*") == 0) { + found_extension = NULL; + return OK; + }; + + /* loop thru extension list */ + while (this_extension != NULL) { + if (strcmpic(option, this_extension->file_extension_string) == 0) { + /* found one */ + found_extension = this_extension->file_extension_string; + return OK; + }; + this_extension = this_extension->next; + }; + + /* grab next extension from option list */ + option = string_nextinlist(&list, &sep, + option_buffer, + sizeof(option_buffer)); + }; + + /* nothing found */ + return FAIL; +} + + +/************************************************* +* unpack TNEF in given directory * +*************************************************/ + +int mime_unpack_tnef(uschar *directory) { + uschar filepath[1024]; + int n; + struct dirent *entry; + DIR *tempdir; + + /* open the dir */ + tempdir = opendir(CS directory); + if (tempdir == NULL) { + return -2; + }; + + /* loop thru dir */ + n = 0; + do { + entry = readdir(tempdir); + /* break on end of list */ + if (entry == NULL) break; + snprintf(CS filepath,1024,"%s/%s",directory,entry->d_name); + if ( (Ustrcmp(entry->d_name,"..") != 0) && (Ustrcmp(entry->d_name,".") != 0) && (Ustrcmp(entry->d_name,"winmail.dat") == 0) ) { + TNEF_set_path(CS directory); + n = TNEF_main(CS filepath); + }; + } while (1); + + closedir(tempdir); + return 0; +} + + +/************************************************* +* small hex_str -> integer conversion function * +*************************************************/ + +/* needed for quoted-printable +*/ + +unsigned int mime_hstr_i(uschar *cptr) { + unsigned int i, j = 0; + + while (cptr && *cptr && isxdigit(*cptr)) { + i = *cptr++ - '0'; + if (9 < i) i -= 7; + j <<= 4; + j |= (i & 0x0f); + } + + return(j); +} + + +/************************************************* +* decode quoted-printable chars * +*************************************************/ + +/* gets called when we hit a = + returns: new pointer position + result code in c: + -2 - decode error + -1 - soft line break, no char + 0-255 - char to write +*/ + +uschar *mime_decode_qp(uschar *qp_p,int *c) { + uschar hex[] = {0,0,0}; + int nan = 0; + uschar *initial_pos = qp_p; + + /* advance one char */ + qp_p++; + + REPEAT_FIRST: + if ( (*qp_p == '\t') || (*qp_p == ' ') || (*qp_p == '\r') ) { + /* tab or whitespace may follow + just ignore it, but remember + that this is not a valid hex + encoding any more */ + nan = 1; + qp_p++; + goto REPEAT_FIRST; + } + else if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F')) || (('a' <= *qp_p) && (*qp_p <= 'f')) ) { + /* this is a valid hex char, if nan is unset */ + if (nan) { + /* this is illegal */ + *c = -2; + return initial_pos; + } + else { + hex[0] = *qp_p; + qp_p++; + }; + } + else if (*qp_p == '\n') { + /* hit soft line break already, continue */ + *c = -1; + return qp_p; + } + else { + /* illegal char here */ + *c = -2; + return initial_pos; + }; + + if ( (('0' <= *qp_p) && (*qp_p <= '9')) || (('A' <= *qp_p) && (*qp_p <= 'F')) || (('a' <= *qp_p) && (*qp_p <= 'f')) ) { + if (hex[0] > 0) { + hex[1] = *qp_p; + /* do hex conversion */ + *c = mime_hstr_i(hex); + qp_p++; + return qp_p; + } + else { + /* huh ? */ + *c = -2; + return initial_pos; + }; + } + else { + /* illegal char */ + *c = -2; + return initial_pos; + }; + +} + + +/************************************************* +* open new dump file * +*************************************************/ + +/* open new dump file + returns: -2 soft error + or file #, FILE * in f +*/ + +int mime_get_dump_file(uschar *extension, FILE **f, uschar *info) { + uschar file_name[1024]; + int result; + unsigned int file_nr; + uschar default_extension[] = ".com"; + uschar *p; + + if (extension == NULL) + extension = default_extension; + + /* scan the proposed extension. + if it is longer than 4 chars, or + contains exotic chars, use the default extension */ + +/* if (Ustrlen(extension) > 4) { + extension = default_extension; + }; +*/ + + p = extension+1; + + while (*p != 0) { + *p = (uschar)tolower((uschar)*p); + if ( (*p < 97) || (*p > 122) ) { + extension = default_extension; + break; + }; + p++; + }; + + /* find a new file to write to */ + file_nr = 0; + do { + struct stat mystat; + + snprintf(CS file_name,1024,"%s/scan/%s/%s-%05u%s",spool_directory,message_id,message_id,file_nr,extension); + file_nr++; + if (file_nr >= MIME_SANITY_MAX_DUMP_FILES) { + /* max parts reached */ + mime_trigger_error(MIME_ERRORLEVEL_TOO_MANY_PARTS); + break; + }; + result = stat(CS file_name,&mystat); + } + while(result != -1); + + *f = fopen(CS file_name,"w+"); + if (*f == NULL) { + /* cannot open new dump file, disk full ? -> soft error */ + snprintf(CS info, 1024,"unable to open dump file"); + return -2; + }; + + return file_nr; +} + + +/************************************************* +* Find a string in a mime header * +*************************************************/ + +/* Find a string in a mime header, and optionally fill in + the value associated with it into *value + + returns: 0 - nothing found + 1 - found param + 2 - found param + value +*/ + +int mime_header_find(uschar *header, uschar *param, uschar **value) { + uschar *needle; + + needle = strstric(header,param,FALSE); + if (needle != NULL) { + if (value != NULL) { + needle += Ustrlen(param); + if (*needle == '=') { + uschar *value_start; + uschar *value_end; + + value_start = needle + 1; + value_end = strstric(value_start,US";",FALSE); + if (value_end != NULL) { + /* allocate mem for value */ + *value = (uschar *)malloc((value_end - value_start)+1); + if (*value == NULL) + return 0; + + Ustrncpy(*value,value_start,(value_end - value_start)); + (*value)[(value_end - value_start)] = '\0'; + return 2; + }; + }; + }; + return 1; + }; + return 0; +} + + +/************************************************* +* Read a line of MIME input * +*************************************************/ +/* returns status code, one of + MIME_READ_LINE_EOF 0 + MIME_READ_LINE_OK 1 + MIME_READ_LINE_OVERFLOW 2 + + In header mode, the line will be "cooked". +*/ + +int mime_read_line(FILE *f, int mime_demux_mode, uschar *buffer, long *num_copied) { + int c = EOF; + int done = 0; + int header_value_mode = 0; + int header_open_brackets = 0; + + *num_copied = 0; + + while(!done) { + + c = fgetc(f); + if (c == EOF) break; + + /* --------- header mode -------------- */ + if (mime_demux_mode == MIME_DEMUX_MODE_MIME_HEADERS) { + + /* always skip CRs */ + if (c == '\r') continue; + + if (c == '\n') { + if ((*num_copied) > 0) { + /* look if next char is '\t' or ' ' */ + c = fgetc(f); + if (c == EOF) break; + if ( (c == '\t') || (c == ' ') ) continue; + ungetc(c,f); + }; + /* end of the header, terminate with ';' */ + c = ';'; + done = 1; + }; + + /* skip control characters */ + if (c < 32) continue; + + /* skip whitespace + tabs */ + if ( (c == ' ') || (c == '\t') ) + continue; + + if (header_value_mode) { + /* --------- value mode ----------- */ + /* skip quotes */ + if (c == '"') continue; + + /* leave value mode on ';' */ + if (c == ';') { + header_value_mode = 0; + }; + /* -------------------------------- */ + } + else { + /* -------- non-value mode -------- */ + if (c == '\\') { + /* quote next char. can be used + to escape brackets. */ + c = fgetc(f); + if (c == EOF) break; + } + else if (c == '(') { + header_open_brackets++; + continue; + } + else if ((c == ')') && header_open_brackets) { + header_open_brackets--; + continue; + } + else if ( (c == '=') && !header_open_brackets ) { + /* enter value mode */ + header_value_mode = 1; + }; + + /* skip chars while we are in a comment */ + if (header_open_brackets > 0) + continue; + /* -------------------------------- */ + }; + } + /* ------------------------------------ */ + else { + /* ----------- non-header mode -------- */ + /* break on '\n' */ + if (c == '\n') + done = 1; + /* ------------------------------------ */ + }; + + /* copy the char to the buffer */ + buffer[*num_copied] = (uschar)c; + /* raise counter */ + (*num_copied)++; + + /* break if buffer is full */ + if (*num_copied > MIME_SANITY_MAX_LINE_LENGTH-1) { + done = 1; + }; + } + + /* 0-terminate */ + buffer[*num_copied] = '\0'; + + if (*num_copied > MIME_SANITY_MAX_LINE_LENGTH-1) + return MIME_READ_LINE_OVERFLOW; + else + if (c == EOF) + return MIME_READ_LINE_EOF; + else + return MIME_READ_LINE_OK; +} + + +/************************************************* +* Check for a MIME boundary * +*************************************************/ + +/* returns: 0 - no boundary found + 1 - start boundary found + 2 - end boundary found +*/ + +int mime_check_boundary(uschar *line, struct boundary *boundaries) { + struct boundary *thisboundary = boundaries; + uschar workbuf[MIME_SANITY_MAX_LINE_LENGTH+1]; + unsigned int i,j=0; + + /* check for '--' first */ + if (Ustrncmp(line,"--",2) == 0) { + + /* strip tab and space */ + for (i = 2; i < Ustrlen(line); i++) { + if ((line[i] != ' ') && (line[i] != '\t')) { + workbuf[j] = line[i]; + j++; + }; + }; + workbuf[j+1]='\0'; + + while(thisboundary != NULL) { + if (Ustrncmp(workbuf,thisboundary->boundary_string,Ustrlen(thisboundary->boundary_string)) == 0) { + if (Ustrncmp(&workbuf[Ustrlen(thisboundary->boundary_string)],"--",2) == 0) { + /* final boundary found */ + return 2; + }; + return 1; + }; + thisboundary = thisboundary->next; + }; + }; + + return 0; +} + + +/************************************************* +* Check for start of a UUENCODE block * +*************************************************/ + +/* returns 0 for no hit, + >0 for hit +*/ + +int mime_check_uu_start(uschar *line, uschar *uu_file_extension, int *has_tnef) { + + if ( (strncmpic(line,US"begin ",6) == 0)) { + uschar *uu_filename = &line[6]; + + /* skip perms, if present */ + Ustrtoul(&line[6],&uu_filename,10); + + /* advance one char */ + uu_filename++; + + /* This should be the filename. + Check if winmail.dat is present, + which indicates TNEF. */ + if (strncmpic(uu_filename,US"winmail.dat",11) == 0) { + *has_tnef = 1; + }; + + /* reverse to dot if present, + copy up to 4 chars for the extension */ + if (Ustrrchr(uu_filename,'.') != NULL) + uu_filename = Ustrrchr(uu_filename,'.'); + + return sscanf(CS uu_filename, "%4[.0-9A-Za-z]",CS uu_file_extension); + } + else { + /* nothing found */ + return 0; + }; +} + + +/************************************************* +* Decode a uu line * +*************************************************/ + +/* returns number of decoded bytes + -2 for soft errors +*/ + +int warned_about_uudec_line_sanity_1 = 0; +int warned_about_uudec_line_sanity_2 = 0; +long uu_decode_line(uschar *line, uschar **data, long line_len, uschar *info) { + uschar *p; + long num_decoded = 0; + uschar tmp_c; + uschar *work; + int uu_decoded_line_len, uu_encoded_line_len; + + /* allocate memory for data and work buffer */ + *data = (uschar *)malloc(line_len); + if (*data == NULL) { + snprintf(CS info, 1024,"unable to allocate %lu bytes",line_len); + return -2; + }; + + work = (uschar *)malloc(line_len); + if (work == NULL) { + snprintf(CS info, 1024,"unable to allocate %lu bytes",line_len); + return -2; + }; + + memcpy(work,line,line_len); + + /* First char is line length + This is microsofts way of getting it. Scary. */ + if (work[0] < 32) { + /* ignore this line */ + return 0; + } + else { + uu_decoded_line_len = uudec[work[0]]; + }; + + p = &work[1]; + + while (*p > 32) { + *p = uudec[*p]; + p++; + }; + + uu_encoded_line_len = (p - &work[1]); + p = &work[1]; + + /* check that resulting line length is a multiple of 4 */ + if ( ( uu_encoded_line_len % 4 ) != 0) { + if (!warned_about_uudec_line_sanity_1) { + mime_trigger_error(MIME_ERRORLEVEL_UU_MISALIGNED); + warned_about_uudec_line_sanity_1 = 1; + }; + return -1; + }; + + /* check that the line length matches */ + if ( ( (((uu_encoded_line_len/4)*3)-2) > uu_decoded_line_len ) || (((uu_encoded_line_len/4)*3) < uu_decoded_line_len) ) { + if (!warned_about_uudec_line_sanity_2) { + mime_trigger_error(MIME_ERRORLEVEL_UU_LINE_LENGTH); + warned_about_uudec_line_sanity_2 = 1; + }; + return -1; + }; + + while ( ((p - &work[1]) < uu_encoded_line_len) && (num_decoded < uu_decoded_line_len)) { + + /* byte 0 ---------------------- */ + if ((p - &work[1] + 1) >= uu_encoded_line_len) { + return 0; + } + + (*data)[num_decoded] = *p; + (*data)[num_decoded] <<= 2; + + tmp_c = *(p+1); + tmp_c >>= 4; + (*data)[num_decoded] |= tmp_c; + + num_decoded++; + p++; + + /* byte 1 ---------------------- */ + if ((p - &work[1] + 1) >= uu_encoded_line_len) { + return 0; + } + + (*data)[num_decoded] = *p; + (*data)[num_decoded] <<= 4; + + tmp_c = *(p+1); + tmp_c >>= 2; + (*data)[num_decoded] |= tmp_c; + + num_decoded++; + p++; + + /* byte 2 ---------------------- */ + if ((p - &work[1] + 1) >= uu_encoded_line_len) { + return 0; + } + + (*data)[num_decoded] = *p; + (*data)[num_decoded] <<= 6; + + (*data)[num_decoded] |= *(p+1); + + num_decoded++; + p+=2; + + }; + + return uu_decoded_line_len; +} + + +/************************************************* +* Decode a b64 or qp line * +*************************************************/ + +/* returns number of decoded bytes + -1 for hard errors + -2 for soft errors +*/ + +int warned_about_b64_line_length = 0; +int warned_about_b64_line_sanity = 0; +int warned_about_b64_illegal_char = 0; +int warned_about_qp_line_sanity = 0; +long mime_decode_line(int mime_demux_mode,uschar *line, uschar **data, long max_data_len, uschar *info) { + uschar *p; + long num_decoded = 0; + int offset = 0; + uschar tmp_c; + + /* allocate memory for data */ + *data = (uschar *)malloc(max_data_len); + if (*data == NULL) { + snprintf(CS info, 1024,"unable to allocate %lu bytes",max_data_len); + return -2; + }; + + if (mime_demux_mode == MIME_DEMUX_MODE_BASE64) { + /* ---------------------------------------------- */ + + /* NULL out trailing '\r' and '\n' chars */ + while (Ustrrchr(line,'\r') != NULL) { + *(Ustrrchr(line,'\r'