From: Philip Hazel Date: Thu, 7 Oct 2004 13:10:00 +0000 (+0000) Subject: Start X-Git-Tag: exim-4_50~147 X-Git-Url: https://vcs.fsf.org/?p=exim.git;a=commitdiff_plain;h=0756eb3cb50d73a77b486e47528f7cb1bffdb299 Start --- diff --git a/src/src/auths/Makefile b/src/src/auths/Makefile new file mode 100644 index 000000000..e3f73cab3 --- /dev/null +++ b/src/src/auths/Makefile @@ -0,0 +1,42 @@ +# $Cambridge: exim/src/src/auths/Makefile,v 1.1 2004/10/07 13:10:00 ph10 Exp $ + +# Make file for building a library containing all the available authorization +# methods, and calling it auths.a. In addition, there are functions that are +# of general use in several methods; these are in separate modules so they are +# linked in only when needed. This Makefile is called from the main make file, +# after cd'ing to the auths subdirectory. When the relevant AUTH_ macros are +# defined, the equivalent modules herein is not included in the final binary. + +OBJ = b64encode.o b64decode.o call_pam.o call_pwcheck.o call_radius.o \ + xtextencode.o xtextdecode.o get_data.o get_no64_data.o md5.o \ + cram_md5.o cyrus_sasl.o plaintext.o pwcheck.o sha1.o auth-spa.o spa.o + +auths.a: $(OBJ) + /bin/rm -f auths.a + $(AR) auths.a $(OBJ) + $(RANLIB) $@ + /bin/rm -rf ../drtables.o + +.SUFFIXES: .o .c +.c.o:; $(CC) -c $(CFLAGS) $(INCLUDE) $*.c + +auth-spa.o: $(HDRS) auth-spa.c +b64encode.o: $(HDRS) b64encode.c +b64decode.o: $(HDRS) b64decode.c +call_pam.o: $(HDRS) call_pam.c +call_pwcheck.o: $(HDRS) call_pwcheck.c pwcheck.h +call_radius.o: $(HDRS) call_radius.c +get_data.o: $(HDRS) get_data.c +get_no64_data.o: $(HDRS) get_no64_data.c +md5.o: $(HDRS) md5.c +pwcheck.o: $(HDRS) pwcheck.c pwcheck.h +sha1.o: $(HDRS) sha1.c +xtextencode.o: $(HDRS) xtextencode.c +xtextdecode.o: $(HDRS) xtextdecode.c + +cram_md5.o: $(HDRS) cram_md5.c cram_md5.h +cyrus_sasl.o: $(HDRS) cyrus_sasl.c cyrus_sasl.h +plaintext.o: $(HDRS) plaintext.c plaintext.h +spa.o: $(HDRS) spa.c spa.h + +# End diff --git a/src/src/auths/README b/src/src/auths/README new file mode 100644 index 000000000..190505f25 --- /dev/null +++ b/src/src/auths/README @@ -0,0 +1,97 @@ +$Cambridge: exim/src/src/auths/README,v 1.1 2004/10/07 13:10:00 ph10 Exp $ + +AUTHS + +The modules in this directory are in support of various authentication +functions. Some of them, such as the base64 encoding/decoding and MD5 +computation, are just functions that might be used by several authentication +mechanisms. Others are the SMTP AUTH mechanisms themselves, included in the +final binary if the relevant AUTH_XXX value is set in Local/Makefile. The +general functions are in separate modules so that they get included in the +final binary only if they are actually called from somewhere. + +GENERAL FUNCTIONS + +The API for each of these functions is documented with the function's code. + + auth_b64encode encode in base 64 + auth_b64decode decode from base 64 + auth_call_pam do PAM authentication (if build with SUPPORT_PAM) + auth_get_data issue SMTP AUTH challenge and read response + auth_xtextencode encode as xtext + auth_xtextdecode decode from xtext + +INTERFACE TO SMTP AUTHENTICATION MECHANISMS + +These are general SSL mechanisms, adapted for use with SMTP. Each +authentication mechanism has three functions, for initialization, server +authentication, and client authentication. + +INITIALIZATION + +The initialization function is called when the configuration is read, and can +check for incomplete or illegal settings. It has one argument, a pointer to the +instance block for this configured mechanism. It must set the flags called +"server" and "client" in the generic auth_instance block to indicate whether +the server and/or client functions are available for this authenticator. +Typically this depends on whether server or client configuration options have +been set, but it is also possible to have an authenticator that has only one of +the server or client functions. + +SERVER AUTHENTICATION + +The second function performs authentication as a server. It receives a pointer +to the instance block, and its second argument is the remainder of the data +from the AUTH command. The numeric variable maximum setting (expand_nmax) is +set to zero, with $0 initialized as unset. The authenticator may set up numeric +variables according to its specification; it should leave expand_nmax set at +the end so that they can be used for the expansion of the generic server_set_id +option, which happens centrally. + +This function has access to the SMTP input and output so that it can write +intermediate responses and read more data if necessary. There is a packaged +function in auth_get_data() which outputs a challenge and reads a response. + +The yield of a server authentication check must be one of: + + OK success + DEFER couldn't complete the check + FAIL authentication failed + CANCELLED authentication forced to fail by "*" response to challenge + BAD64 bad base64 data received + UNEXPECTED unexpected data received + +In the case of DEFER, auth_defer_msg should point to an error message. + +CLIENT AUTHENTICATION + +The third function performs authentication as a client. It receives a pointer +to the instance block, and four further arguments: + + The smtp_inblock item for the connection to the remote host. + + The normal command-reading timeout value. + + A pointer to a buffer, to be used for receiving responses. It is done this + way so that the buffer is available for logging etc. in the calling + function in cases of error. + + The size of the buffer. + +The yield of a client authentication check must be one of: + + OK success + FAIL_SEND error after writing a command; errno is set + FAIL failed after reading a response; + either errno is set (for timeouts, I/O failures) or + the buffer contains the SMTP response line + FORCEFAIL failed without reading a response (often "fail" in expansion) + ERROR local problem (typically expansion error); message in buffer + +To communicate with the remote host the client should call +smtp_write_command(). If this yields FALSE, the authenticator should return +FAIL. After a successful write, the response is received by a call to +smtp_read_response(), which should use the buffer handed to the client function +as an argument. + +**** diff --git a/src/src/auths/auth-spa.c b/src/src/auths/auth-spa.c new file mode 100644 index 000000000..c6f716551 --- /dev/null +++ b/src/src/auths/auth-spa.c @@ -0,0 +1,1528 @@ +/* $Cambridge: exim/src/src/auths/auth-spa.c,v 1.1 2004/10/07 13:10:00 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * This file provides the necessary methods for authenticating with + * Microsoft's Secure Password Authentication. + + * All the original code used here was torn by Marc Prud'hommeaux out of the + * Samba project (by Andrew Tridgell, Jeremy Allison, and others). + + * Tom Kistner provided additional code, adding spa_build_auth_challenge() to + * support server authentication mode. + + * Mark Lyda provided a patch to solve this problem: + + - Exim is indicating in its Authentication Request message (Type 1) that it + can transmit text in either Unicode or OEM format. + + - Microsoft's SMTP server (smtp.email.msn.com) is responding in its + Challenge message (Type 2) that it will be expecting the OEM format. + + - Exim does not pay attention to the text format requested by Microsoft's + SMTP server and, instead, defaults to using the Unicode format. + + * References: + * http://www.innovation.ch/java/ntlm.html + * http://www.kuro5hin.org/story/2002/4/28/1436/66154 + + * It seems that some systems have existing but different definitions of some + * of the following types. I received a complaint about "int16" causing + * compilation problems. So I (PH) have renamed them all, to be on the safe + * side, by adding 'x' on the end. + + * typedef signed short int16; + * typedef unsigned short uint16; + * typedef unsigned uint32; + * typedef unsigned char uint8; + + * The API is extremely simple: + * 1. Form a SPA authentication request based on the username + * and (optional) domain + * 2. Send the request to the server and get an SPA challenge + * 3. Build the challenge response and send it back. + * + * Example usage is as + * follows: + * +int main (int argc, char ** argv) +{ + SPAAuthRequest request; + SPAAuthChallenge challenge; + SPAAuthResponse response; + char msgbuf[2048]; + char buffer[512]; + char *username, *password, *domain, *challenge_str; + + if (argc < 3) + { + printf ("Usage: %s [SPA Challenge]\n", + argv [0]); + exit (1); + } + + username = argv [1]; + password = argv [2]; + domain = 0; + + spa_build_auth_request (&request, username, domain); + + spa_bits_to_base64 (msgbuf, (unsigned char*)&request, + spa_request_length(&request)); + + printf ("SPA Login request for username=%s:\n %s\n", + argv [1], msgbuf); + + if (argc < 4) + { + printf ("Run: %s [NTLM Challenge] " \ + "to complete authenitcation\n", argv [0]); + exit (0); + } + + challenge_str = argv [3]; + + spa_build_auth_response (&challenge, &response, username, password); + spa_bits_to_base64 (msgbuf, (unsigned char*)&response, + spa_request_length(&response)); + + printf ("SPA Response to challenge:\n %s\n for " \ + "username=%s, password=%s:\n %s\n", + argv[3], argv [1], argv [2], msgbuf); + return 0; +} + * + * + * All the client code used here was torn by Marc Prud'hommeaux out of the + * Samba project (by Andrew Tridgell, Jeremy Allison, and others). + * Previous comments are below: + */ + +/* + Unix SMB/Netbios implementation. + Version 1.9. + + a partial implementation of DES designed for use in the + SMB authentication protocol + + Copyright (C) Andrew Tridgell 1998 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + + +/* NOTES: + + This code makes no attempt to be fast! In fact, it is a very + slow implementation + + This code is NOT a complete DES implementation. It implements only + the minimum necessary for SMB authentication, as used by all SMB + products (including every copy of Microsoft Windows95 ever sold) + + In particular, it can only do a unchained forward DES pass. This + means it is not possible to use this code for encryption/decryption + of data, instead it is only useful as a "hash" algorithm. + + There is no entry point into this code that allows normal DES operation. + + I believe this means that this code does not come under ITAR + regulations but this is NOT a legal opinion. If you are concerned + about the applicability of ITAR regulations to this code then you + should confirm it for yourself (and maybe let me know if you come + up with a different answer to the one above) +*/ + +#define DEBUG(a,b) ; + +extern int DEBUGLEVEL; + +#include /* For size_t */ +#include "auth-spa.h" +#include +#include +#include +#include +#include +#include +#include + +#ifndef _AIX +typedef unsigned char uchar; +#endif + + +typedef int BOOL; +#define False 0 +#define True 1 + +#ifndef _BYTEORDER_H +#define _BYTEORDER_H + +#define RW_PCVAL(read,inbuf,outbuf,len) \ + { if (read) { PCVAL (inbuf,0,outbuf,len); } \ + else { PSCVAL(inbuf,0,outbuf,len); } } + +#define RW_PIVAL(read,big_endian,inbuf,outbuf,len) \ + { if (read) { if (big_endian) { RPIVAL(inbuf,0,outbuf,len); } else { PIVAL(inbuf,0,outbuf,len); } } \ + else { if (big_endian) { RPSIVAL(inbuf,0,outbuf,len); } else { PSIVAL(inbuf,0,outbuf,len); } } } + +#define RW_PSVAL(read,big_endian,inbuf,outbuf,len) \ + { if (read) { if (big_endian) { RPSVAL(inbuf,0,outbuf,len); } else { PSVAL(inbuf,0,outbuf,len); } } \ + else { if (big_endian) { RPSSVAL(inbuf,0,outbuf,len); } else { PSSVAL(inbuf,0,outbuf,len); } } } + +#define RW_CVAL(read, inbuf, outbuf, offset) \ + { if (read) { (outbuf) = CVAL (inbuf,offset); } \ + else { SCVAL(inbuf,offset,outbuf); } } + +#define RW_IVAL(read, big_endian, inbuf, outbuf, offset) \ + { if (read) { (outbuf) = ((big_endian) ? RIVAL(inbuf,offset) : IVAL (inbuf,offset)); } \ + else { if (big_endian) { RSIVAL(inbuf,offset,outbuf); } else { SIVAL(inbuf,offset,outbuf); } } } + +#define RW_SVAL(read, big_endian, inbuf, outbuf, offset) \ + { if (read) { (outbuf) = ((big_endian) ? RSVAL(inbuf,offset) : SVAL (inbuf,offset)); } \ + else { if (big_endian) { RSSVAL(inbuf,offset,outbuf); } else { SSVAL(inbuf,offset,outbuf); } } } + +#undef CAREFUL_ALIGNMENT + +/* we know that the 386 can handle misalignment and has the "right" + byteorder */ +#ifdef __i386__ +#define CAREFUL_ALIGNMENT 0 +#endif + +#ifndef CAREFUL_ALIGNMENT +#define CAREFUL_ALIGNMENT 1 +#endif + +#define CVAL(buf,pos) (((unsigned char *)(buf))[pos]) +#define PVAL(buf,pos) ((unsigned)CVAL(buf,pos)) +#define SCVAL(buf,pos,val) (CVAL(buf,pos) = (val)) + + +#if CAREFUL_ALIGNMENT + +#define SVAL(buf,pos) (PVAL(buf,pos)|PVAL(buf,(pos)+1)<<8) +#define IVAL(buf,pos) (SVAL(buf,pos)|SVAL(buf,(pos)+2)<<16) +#define SSVALX(buf,pos,val) (CVAL(buf,pos)=(val)&0xFF,CVAL(buf,pos+1)=(val)>>8) +#define SIVALX(buf,pos,val) (SSVALX(buf,pos,val&0xFFFF),SSVALX(buf,pos+2,val>>16)) +#define SVALS(buf,pos) ((int16x)SVAL(buf,pos)) +#define IVALS(buf,pos) ((int32x)IVAL(buf,pos)) +#define SSVAL(buf,pos,val) SSVALX((buf),(pos),((uint16x)(val))) +#define SIVAL(buf,pos,val) SIVALX((buf),(pos),((uint32x)(val))) +#define SSVALS(buf,pos,val) SSVALX((buf),(pos),((int16x)(val))) +#define SIVALS(buf,pos,val) SIVALX((buf),(pos),((int32x)(val))) + +#else /* CAREFUL_ALIGNMENT */ + +/* this handles things for architectures like the 386 that can handle + alignment errors */ +/* + WARNING: This section is dependent on the length of int16x and int32x + being correct +*/ + +/* get single value from an SMB buffer */ +#define SVAL(buf,pos) (*(uint16x *)((char *)(buf) + (pos))) +#define IVAL(buf,pos) (*(uint32x *)((char *)(buf) + (pos))) +#define SVALS(buf,pos) (*(int16x *)((char *)(buf) + (pos))) +#define IVALS(buf,pos) (*(int32x *)((char *)(buf) + (pos))) + +/* store single value in an SMB buffer */ +#define SSVAL(buf,pos,val) SVAL(buf,pos)=((uint16x)(val)) +#define SIVAL(buf,pos,val) IVAL(buf,pos)=((uint32x)(val)) +#define SSVALS(buf,pos,val) SVALS(buf,pos)=((int16x)(val)) +#define SIVALS(buf,pos,val) IVALS(buf,pos)=((int32x)(val)) + +#endif /* CAREFUL_ALIGNMENT */ + +/* macros for reading / writing arrays */ + +#define SMBMACRO(macro,buf,pos,val,len,size) \ +{ int l; for (l = 0; l < (len); l++) (val)[l] = macro((buf), (pos) + (size)*l); } + +#define SSMBMACRO(macro,buf,pos,val,len,size) \ +{ int l; for (l = 0; l < (len); l++) macro((buf), (pos) + (size)*l, (val)[l]); } + +/* reads multiple data from an SMB buffer */ +#define PCVAL(buf,pos,val,len) SMBMACRO(CVAL,buf,pos,val,len,1) +#define PSVAL(buf,pos,val,len) SMBMACRO(SVAL,buf,pos,val,len,2) +#define PIVAL(buf,pos,val,len) SMBMACRO(IVAL,buf,pos,val,len,4) +#define PCVALS(buf,pos,val,len) SMBMACRO(CVALS,buf,pos,val,len,1) +#define PSVALS(buf,pos,val,len) SMBMACRO(SVALS,buf,pos,val,len,2) +#define PIVALS(buf,pos,val,len) SMBMACRO(IVALS,buf,pos,val,len,4) + +/* stores multiple data in an SMB buffer */ +#define PSCVAL(buf,pos,val,len) SSMBMACRO(SCVAL,buf,pos,val,len,1) +#define PSSVAL(buf,pos,val,len) SSMBMACRO(SSVAL,buf,pos,val,len,2) +#define PSIVAL(buf,pos,val,len) SSMBMACRO(SIVAL,buf,pos,val,len,4) +#define PSCVALS(buf,pos,val,len) SSMBMACRO(SCVALS,buf,pos,val,len,1) +#define PSSVALS(buf,pos,val,len) SSMBMACRO(SSVALS,buf,pos,val,len,2) +#define PSIVALS(buf,pos,val,len) SSMBMACRO(SIVALS,buf,pos,val,len,4) + + +/* now the reverse routines - these are used in nmb packets (mostly) */ +#define SREV(x) ((((x)&0xFF)<<8) | (((x)>>8)&0xFF)) +#define IREV(x) ((SREV(x)<<16) | (SREV((x)>>16))) + +#define RSVAL(buf,pos) SREV(SVAL(buf,pos)) +#define RSVALS(buf,pos) SREV(SVALS(buf,pos)) +#define RIVAL(buf,pos) IREV(IVAL(buf,pos)) +#define RIVALS(buf,pos) IREV(IVALS(buf,pos)) +#define RSSVAL(buf,pos,val) SSVAL(buf,pos,SREV(val)) +#define RSSVALS(buf,pos,val) SSVALS(buf,pos,SREV(val)) +#define RSIVAL(buf,pos,val) SIVAL(buf,pos,IREV(val)) +#define RSIVALS(buf,pos,val) SIVALS(buf,pos,IREV(val)) + +/* reads multiple data from an SMB buffer (big-endian) */ +#define RPSVAL(buf,pos,val,len) SMBMACRO(RSVAL,buf,pos,val,len,2) +#define RPIVAL(buf,pos,val,len) SMBMACRO(RIVAL,buf,pos,val,len,4) +#define RPSVALS(buf,pos,val,len) SMBMACRO(RSVALS,buf,pos,val,len,2) +#define RPIVALS(buf,pos,val,len) SMBMACRO(RIVALS,buf,pos,val,len,4) + +/* stores multiple data in an SMB buffer (big-endian) */ +#define RPSSVAL(buf,pos,val,len) SSMBMACRO(RSSVAL,buf,pos,val,len,2) +#define RPSIVAL(buf,pos,val,len) SSMBMACRO(RSIVAL,buf,pos,val,len,4) +#define RPSSVALS(buf,pos,val,len) SSMBMACRO(RSSVALS,buf,pos,val,len,2) +#define RPSIVALS(buf,pos,val,len) SSMBMACRO(RSIVALS,buf,pos,val,len,4) + +#define DBG_RW_PCVAL(charmode,string,depth,base,read,inbuf,outbuf,len) \ + { RW_PCVAL(read,inbuf,outbuf,len) \ + DEBUG(5,("%s%04x %s: ", \ + tab_depth(depth), base,string)); \ + if (charmode) print_asc(5, (unsigned char*)(outbuf), (len)); else \ + { int idx; for (idx = 0; idx < len; idx++) { DEBUG(5,("%02x ", (outbuf)[idx])); } } \ + DEBUG(5,("\n")); } + +#define DBG_RW_PSVAL(charmode,string,depth,base,read,big_endian,inbuf,outbuf,len) \ + { RW_PSVAL(read,big_endian,inbuf,outbuf,len) \ + DEBUG(5,("%s%04x %s: ", \ + tab_depth(depth), base,string)); \ + if (charmode) print_asc(5, (unsigned char*)(outbuf), 2*(len)); else \ + { int idx; for (idx = 0; idx < len; idx++) { DEBUG(5,("%04x ", (outbuf)[idx])); } } \ + DEBUG(5,("\n")); } + +#define DBG_RW_PIVAL(charmode,string,depth,base,read,big_endian,inbuf,outbuf,len) \ + { RW_PIVAL(read,big_endian,inbuf,outbuf,len) \ + DEBUG(5,("%s%04x %s: ", \ + tab_depth(depth), base,string)); \ + if (charmode) print_asc(5, (unsigned char*)(outbuf), 4*(len)); else \ + { int idx; for (idx = 0; idx < len; idx++) { DEBUG(5,("%08x ", (outbuf)[idx])); } } \ + DEBUG(5,("\n")); } + +#define DBG_RW_CVAL(string,depth,base,read,inbuf,outbuf) \ + { RW_CVAL(read,inbuf,outbuf,0) \ + DEBUG(5,("%s%04x %s: %02x\n", \ + tab_depth(depth), base, string, outbuf)); } + +#define DBG_RW_SVAL(string,depth,base,read,big_endian,inbuf,outbuf) \ + { RW_SVAL(read,big_endian,inbuf,outbuf,0) \ + DEBUG(5,("%s%04x %s: %04x\n", \ + tab_depth(depth), base, string, outbuf)); } + +#define DBG_RW_IVAL(string,depth,base,read,big_endian,inbuf,outbuf) \ + { RW_IVAL(read,big_endian,inbuf,outbuf,0) \ + DEBUG(5,("%s%04x %s: %08x\n", \ + tab_depth(depth), base, string, outbuf)); } + +#endif /* _BYTEORDER_H */ + +void E_P16 (unsigned char *p14, unsigned char *p16); +void E_P24 (unsigned char *p21, unsigned char *c8, unsigned char *p24); +void D_P16 (unsigned char *p14, unsigned char *in, unsigned char *out); +void SMBOWFencrypt (uchar passwd[16], uchar * c8, uchar p24[24]); + +void mdfour (unsigned char *out, unsigned char *in, int n); + + +/* + * base64.c -- base-64 conversion routines. + * + * For license terms, see the file COPYING in this directory. + * + * This base 64 encoding is defined in RFC2045 section 6.8, + * "Base64 Content-Transfer-Encoding", but lines must not be broken in the + * scheme used here. + */ + +static const char base64digits[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +#define BAD -1 +static const char base64val[] = { + BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, + BAD, + BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, + BAD, + BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, 62, BAD, BAD, BAD, + 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, BAD, BAD, BAD, BAD, BAD, BAD, + BAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, BAD, BAD, BAD, BAD, BAD, + BAD, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, BAD, BAD, BAD, BAD, BAD +}; +#define DECODE64(c) (isascii(c) ? base64val[c] : BAD) + +void +spa_bits_to_base64 (unsigned char *out, const unsigned char *in, int inlen) +/* raw bytes in quasi-big-endian order to base 64 string (NUL-terminated) */ +{ + for (; inlen >= 3; inlen -= 3) + { + *out++ = base64digits[in[0] >> 2]; + *out++ = base64digits[((in[0] << 4) & 0x30) | (in[1] >> 4)]; + *out++ = base64digits[((in[1] << 2) & 0x3c) | (in[2] >> 6)]; + *out++ = base64digits[in[2] & 0x3f]; + in += 3; + } + if (inlen > 0) + { + unsigned char fragment; + + *out++ = base64digits[in[0] >> 2]; + fragment = (in[0] << 4) & 0x30; + if (inlen > 1) + fragment |= in[1] >> 4; + *out++ = base64digits[fragment]; + *out++ = (inlen < 2) ? '=' : base64digits[(in[1] << 2) & 0x3c]; + *out++ = '='; + } + *out = '\0'; +} + +int +spa_base64_to_bits (char *out, const char *in) +/* base 64 to raw bytes in quasi-big-endian order, returning count of bytes */ +{ + int len = 0; + register unsigned char digit1, digit2, digit3, digit4; + + if (in[0] == '+' && in[1] == ' ') + in += 2; + if (*in == '\r') + return (0); + + do + { + digit1 = in[0]; + if (DECODE64 (digit1) == BAD) + return (-1); + digit2 = in[1]; + if (DECODE64 (digit2) == BAD) + return (-1); + digit3 = in[2]; + if (digit3 != '=' && DECODE64 (digit3) == BAD) + return (-1); + digit4 = in[3]; + if (digit4 != '=' && DECODE64 (digit4) == BAD) + return (-1); + in += 4; + *out++ = (DECODE64 (digit1) << 2) | (DECODE64 (digit2) >> 4); + ++len; + if (digit3 != '=') + { + *out++ = + ((DECODE64 (digit2) << 4) & 0xf0) | (DECODE64 (digit3) >> 2); + ++len; + if (digit4 != '=') + { + *out++ = ((DECODE64 (digit3) << 6) & 0xc0) | DECODE64 (digit4); + ++len; + } + } + } + while (*in && *in != '\r' && digit4 != '='); + + return (len); +} + + +#define uchar unsigned char + +static uchar perm1[56] = { 57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4 +}; + +static uchar perm2[48] = { 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32 +}; + +static uchar perm3[64] = { 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7 +}; + +static uchar perm4[48] = { 32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1 +}; + +static uchar perm5[32] = { 16, 7, 20, 21, + 29, 12, 28, 17, + 1, 15, 23, 26, + 5, 18, 31, 10, + 2, 8, 24, 14, + 32, 27, 3, 9, + 19, 13, 30, 6, + 22, 11, 4, 25 +}; + + +static uchar perm6[64] = { 40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25 +}; + + +static uchar sc[16] = { 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 }; + +static uchar sbox[8][4][16] = { + {{14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7}, + {0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8}, + {4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0}, + {15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13}}, + + {{15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10}, + {3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5}, + {0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15}, + {13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9}}, + + {{10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8}, + {13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1}, + {13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7}, + {1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12}}, + + {{7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15}, + {13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9}, + {10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4}, + {3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14}}, + + {{2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9}, + {14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6}, + {4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14}, + {11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3}}, + + {{12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11}, + {10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8}, + {9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6}, + {4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13}}, + + {{4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1}, + {13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6}, + {1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2}, + {6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12}}, + + {{13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7}, + {1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2}, + {7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8}, + {2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11}} +}; + +static void +permute (char *out, char *in, uchar * p, int n) +{ + int i; + for (i = 0; i < n; i++) + out[i] = in[p[i] - 1]; +} + +static void +lshift (char *d, int count, int n) +{ + char out[64]; + int i; + for (i = 0; i < n; i++) + out[i] = d[(i + count) % n]; + for (i = 0; i < n; i++) + d[i] = out[i]; +} + +static void +concat (char *out, char *in1, char *in2, int l1, int l2) +{ + while (l1--) + *out++ = *in1++; + while (l2--) + *out++ = *in2++; +} + +static void +xor (char *out, char *in1, char *in2, int n) +{ + int i; + for (i = 0; i < n; i++) + out[i] = in1[i] ^ in2[i]; +} + +static void +dohash (char *out, char *in, char *key, int forw) +{ + int i, j, k; + char pk1[56]; + char c[28]; + char d[28]; + char cd[56]; + char ki[16][48]; + char pd1[64]; + char l[32], r[32]; + char rl[64]; + + permute (pk1, key, perm1, 56); + + for (i = 0; i < 28; i++) + c[i] = pk1[i]; + for (i = 0; i < 28; i++) + d[i] = pk1[i + 28]; + + for (i = 0; i < 16; i++) + { + lshift (c, sc[i], 28); + lshift (d, sc[i], 28); + + concat (cd, c, d, 28, 28); + permute (ki[i], cd, perm2, 48); + } + + permute (pd1, in, perm3, 64); + + for (j = 0; j < 32; j++) + { + l[j] = pd1[j]; + r[j] = pd1[j + 32]; + } + + for (i = 0; i < 16; i++) + { + char er[48]; + char erk[48]; + char b[8][6]; + char cb[32]; + char pcb[32]; + char r2[32]; + + permute (er, r, perm4, 48); + + xor (erk, er, ki[forw ? i : 15 - i], 48); + + for (j = 0; j < 8; j++) + for (k = 0; k < 6; k++) + b[j][k] = erk[j * 6 + k]; + + for (j = 0; j < 8; j++) + { + int m, n; + m = (b[j][0] << 1) | b[j][5]; + + n = (b[j][1] << 3) | (b[j][2] << 2) | (b[j][3] << 1) | b[j][4]; + + for (k = 0; k < 4; k++) + b[j][k] = (sbox[j][m][n] & (1 << (3 - k))) ? 1 : 0; + } + + for (j = 0; j < 8; j++) + for (k = 0; k < 4; k++) + cb[j * 4 + k] = b[j][k]; + permute (pcb, cb, perm5, 32); + + xor (r2, l, pcb, 32); + + for (j = 0; j < 32; j++) + l[j] = r[j]; + + for (j = 0; j < 32; j++) + r[j] = r2[j]; + } + + concat (rl, r, l, 32, 32); + + permute (out, rl, perm6, 64); +} + +static void +str_to_key (unsigned char *str, unsigned char *key) +{ + int i; + + key[0] = str[0] >> 1; + key[1] = ((str[0] & 0x01) << 6) | (str[1] >> 2); + key[2] = ((str[1] & 0x03) << 5) | (str[2] >> 3); + key[3] = ((str[2] & 0x07) << 4) | (str[3] >> 4); + key[4] = ((str[3] & 0x0F) << 3) | (str[4] >> 5); + key[5] = ((str[4] & 0x1F) << 2) | (str[5] >> 6); + key[6] = ((str[5] & 0x3F) << 1) | (str[6] >> 7); + key[7] = str[6] & 0x7F; + for (i = 0; i < 8; i++) + { + key[i] = (key[i] << 1); + } +} + + +static void +smbhash (unsigned char *out, unsigned char *in, unsigned char *key, int forw) +{ + int i; + char outb[64]; + char inb[64]; + char keyb[64]; + unsigned char key2[8]; + + str_to_key (key, key2); + + for (i = 0; i < 64; i++) + { + inb[i] = (in[i / 8] & (1 << (7 - (i % 8)))) ? 1 : 0; + keyb[i] = (key2[i / 8] & (1 << (7 - (i % 8)))) ? 1 : 0; + outb[i] = 0; + } + + dohash (outb, inb, keyb, forw); + + for (i = 0; i < 8; i++) + { + out[i] = 0; + } + + for (i = 0; i < 64; i++) + { + if (outb[i]) + out[i / 8] |= (1 << (7 - (i % 8))); + } +} + +void +E_P16 (unsigned char *p14, unsigned char *p16) +{ + unsigned char sp8[8] = { 0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 }; + smbhash (p16, sp8, p14, 1); + smbhash (p16 + 8, sp8, p14 + 7, 1); +} + +void +E_P24 (unsigned char *p21, unsigned char *c8, unsigned char *p24) +{ + smbhash (p24, c8, p21, 1); + smbhash (p24 + 8, c8, p21 + 7, 1); + smbhash (p24 + 16, c8, p21 + 14, 1); +} + +void +D_P16 (unsigned char *p14, unsigned char *in, unsigned char *out) +{ + smbhash (out, in, p14, 0); + smbhash (out + 8, in + 8, p14 + 7, 0); +} + +/**************************************************************************** + Like strncpy but always null terminates. Make sure there is room! + The variable n should always be one less than the available size. +****************************************************************************/ + +char * +StrnCpy (char *dest, const char *src, size_t n) +{ + char *d = dest; + if (!dest) + return (NULL); + if (!src) + { + *dest = 0; + return (dest); + } + while (n-- && (*d++ = *src++)); + *d = 0; + return (dest); +} + +size_t +skip_multibyte_char (char c) +{ + /* bogus if to get rid of unused compiler warning */ + if (c) + return 0; + else + return 0; +} + + +/******************************************************************* +safe string copy into a known length string. maxlength does not +include the terminating zero. +********************************************************************/ + +char * +safe_strcpy (char *dest, const char *src, size_t maxlength) +{ + size_t len; + + if (!dest) + { + DEBUG (0, ("ERROR: NULL dest in safe_strcpy\n")); + return NULL; + } + + if (!src) + { + *dest = 0; + return dest; + } + + len = strlen (src); + + if (len > maxlength) + { + DEBUG (0, ("ERROR: string overflow by %d in safe_strcpy [%.50s]\n", + (int) (len - maxlength), src)); + len = maxlength; + } + + memcpy (dest, src, len); + dest[len] = 0; + return dest; +} + + +void +strupper (char *s) +{ + while (*s) + { + { + size_t skip = skip_multibyte_char (*s); + if (skip != 0) + s += skip; + else + { + if (islower ((unsigned char)(*s))) + *s = toupper (*s); + s++; + } + } + } +} + + +/* + This implements the X/Open SMB password encryption + It takes a password, a 8 byte "crypt key" and puts 24 bytes of + encrypted password into p24 + */ + +void +spa_smb_encrypt (uchar * passwd, uchar * c8, uchar * p24) +{ + uchar p14[15], p21[21]; + + memset (p21, '\0', 21); + memset (p14, '\0', 14); + StrnCpy ((char *) p14, (char *) passwd, 14); + + strupper ((char *) p14); + E_P16 (p14, p21); + + SMBOWFencrypt (p21, c8, p24); + +#ifdef DEBUG_PASSWORD + DEBUG (100, ("spa_smb_encrypt: lm#, challenge, response\n")); + dump_data (100, (char *) p21, 16); + dump_data (100, (char *) c8, 8); + dump_data (100, (char *) p24, 24); +#endif +} + +/* Routines for Windows NT MD4 Hash functions. */ +static int +_my_wcslen (int16x * str) +{ + int len = 0; + while (*str++ != 0) + len++; + return len; +} + +/* + * Convert a string into an NT UNICODE string. + * Note that regardless of processor type + * this must be in intel (little-endian) + * format. + */ + +static int +_my_mbstowcs (int16x * dst, uchar * src, int len) +{ + int i; + int16x val; + + for (i = 0; i < len; i++) + { + val = *src; + SSVAL (dst, 0, val); + dst++; + src++; + if (val == 0) + break; + } + return i; +} + +/* + * Creates the MD4 Hash of the users password in NT UNICODE. + */ + +void +E_md4hash (uchar * passwd, uchar * p16) +{ + int len; + int16x wpwd[129]; + + /* Password cannot be longer than 128 characters */ + len = strlen ((char *) passwd); + if (len > 128) + len = 128; + /* Password must be converted to NT unicode */ + _my_mbstowcs (wpwd, passwd, len); + wpwd[len] = 0; /* Ensure string is null terminated */ + /* Calculate length in bytes */ + len = _my_wcslen (wpwd) * sizeof (int16x); + + mdfour (p16, (unsigned char *) wpwd, len); +} + +/* Does both the NT and LM owfs of a user's password */ +void +nt_lm_owf_gen (char *pwd, uchar nt_p16[16], uchar p16[16]) +{ + char passwd[130]; + + memset (passwd, '\0', 130); + safe_strcpy (passwd, pwd, sizeof (passwd) - 1); + + /* Calculate the MD4 hash (NT compatible) of the password */ + memset (nt_p16, '\0', 16); + E_md4hash ((uchar *) passwd, nt_p16); + +#ifdef DEBUG_PASSWORD + DEBUG (100, ("nt_lm_owf_gen: pwd, nt#\n")); + dump_data (120, passwd, strlen (passwd)); + dump_data (100, (char *) nt_p16, 16); +#endif + + /* Mangle the passwords into Lanman format */ + passwd[14] = '\0'; + strupper (passwd); + + /* Calculate the SMB (lanman) hash functions of the password */ + + memset (p16, '\0', 16); + E_P16 ((uchar *) passwd, (uchar *) p16); + +#ifdef DEBUG_PASSWORD + DEBUG (100, ("nt_lm_owf_gen: pwd, lm#\n")); + dump_data (120, passwd, strlen (passwd)); + dump_data (100, (char *) p16, 16); +#endif + /* clear out local copy of user's password (just being paranoid). */ + memset (passwd, '\0', sizeof (passwd)); +} + +/* Does the des encryption from the NT or LM MD4 hash. */ +void +SMBOWFencrypt (uchar passwd[16], uchar * c8, uchar p24[24]) +{ + uchar p21[21]; + + memset (p21, '\0', 21); + + memcpy (p21, passwd, 16); + E_P24 (p21, c8, p24); +} + +/* Does the des encryption from the FIRST 8 BYTES of the NT or LM MD4 hash. */ +void +NTLMSSPOWFencrypt (uchar passwd[8], uchar * ntlmchalresp, uchar p24[24]) +{ + uchar p21[21]; + + memset (p21, '\0', 21); + memcpy (p21, passwd, 8); + memset (p21 + 8, 0xbd, 8); + + E_P24 (p21, ntlmchalresp, p24); +#ifdef DEBUG_PASSWORD + DEBUG (100, ("NTLMSSPOWFencrypt: p21, c8, p24\n")); + dump_data (100, (char *) p21, 21); + dump_data (100, (char *) ntlmchalresp, 8); + dump_data (100, (char *) p24, 24); +#endif +} + + +/* Does the NT MD4 hash then des encryption. */ + +void +spa_smb_nt_encrypt (uchar * passwd, uchar * c8, uchar * p24) +{ + uchar p21[21]; + + memset (p21, '\0', 21); + + E_md4hash (passwd, p21); + SMBOWFencrypt (p21, c8, p24); + +#ifdef DEBUG_PASSWORD + DEBUG (100, ("spa_smb_nt_encrypt: nt#, challenge, response\n")); + dump_data (100, (char *) p21, 16); + dump_data (100, (char *) c8, 8); + dump_data (100, (char *) p24, 24); +#endif +} + +static uint32x A, B, C, D; + +static uint32x +F (uint32x X, uint32x Y, uint32x Z) +{ + return (X & Y) | ((~X) & Z); +} + +static uint32x +G (uint32x X, uint32x Y, uint32x Z) +{ + return (X & Y) | (X & Z) | (Y & Z); +} + +static uint32x +H (uint32x X, uint32x Y, uint32x Z) +{ + return X ^ Y ^ Z; +} + +static uint32x +lshift_a (uint32x x, int s) +{ + x &= 0xFFFFFFFF; + return ((x << s) & 0xFFFFFFFF) | (x >> (32 - s)); +} + +#define ROUND1(a,b,c,d,k,s) a = lshift_a(a + F(b,c,d) + X[k], s) +#define ROUND2(a,b,c,d,k,s) a = lshift_a(a + G(b,c,d) + X[k] + (uint32x)0x5A827999,s) +#define ROUND3(a,b,c,d,k,s) a = lshift_a(a + H(b,c,d) + X[k] + (uint32x)0x6ED9EBA1,s) + +/* this applies md4 to 64 byte chunks */ +static void +spa_mdfour64 (uint32x * M) +{ + int j; + uint32x AA, BB, CC, DD; + uint32x X[16]; + + for (j = 0; j < 16; j++) + X[j] = M[j]; + + AA = A; + BB = B; + CC = C; + DD = D; + + ROUND1 (A, B, C, D, 0, 3); + ROUND1 (D, A, B, C, 1, 7); + ROUND1 (C, D, A, B, 2, 11); + ROUND1 (B, C, D, A, 3, 19); + ROUND1 (A, B, C, D, 4, 3); + ROUND1 (D, A, B, C, 5, 7); + ROUND1 (C, D, A, B, 6, 11); + ROUND1 (B, C, D, A, 7, 19); + ROUND1 (A, B, C, D, 8, 3); + ROUND1 (D, A, B, C, 9, 7); + ROUND1 (C, D, A, B, 10, 11); + ROUND1 (B, C, D, A, 11, 19); + ROUND1 (A, B, C, D, 12, 3); + ROUND1 (D, A, B, C, 13, 7); + ROUND1 (C, D, A, B, 14, 11); + ROUND1 (B, C, D, A, 15, 19); + + ROUND2 (A, B, C, D, 0, 3); + ROUND2 (D, A, B, C, 4, 5); + ROUND2 (C, D, A, B, 8, 9); + ROUND2 (B, C, D, A, 12, 13); + ROUND2 (A, B, C, D, 1, 3); + ROUND2 (D, A, B, C, 5, 5); + ROUND2 (C, D, A, B, 9, 9); + ROUND2 (B, C, D, A, 13, 13); + ROUND2 (A, B, C, D, 2, 3); + ROUND2 (D, A, B, C, 6, 5); + ROUND2 (C, D, A, B, 10, 9); + ROUND2 (B, C, D, A, 14, 13); + ROUND2 (A, B, C, D, 3, 3); + ROUND2 (D, A, B, C, 7, 5); + ROUND2 (C, D, A, B, 11, 9); + ROUND2 (B, C, D, A, 15, 13); + + ROUND3 (A, B, C, D, 0, 3); + ROUND3 (D, A, B, C, 8, 9); + ROUND3 (C, D, A, B, 4, 11); + ROUND3 (B, C, D, A, 12, 15); + ROUND3 (A, B, C, D, 2, 3); + ROUND3 (D, A, B, C, 10, 9); + ROUND3 (C, D, A, B, 6, 11); + ROUND3 (B, C, D, A, 14, 15); + ROUND3 (A, B, C, D, 1, 3); + ROUND3 (D, A, B, C, 9, 9); + ROUND3 (C, D, A, B, 5, 11); + ROUND3 (B, C, D, A, 13, 15); + ROUND3 (A, B, C, D, 3, 3); + ROUND3 (D, A, B, C, 11, 9); + ROUND3 (C, D, A, B, 7, 11); + ROUND3 (B, C, D, A, 15, 15); + + A += AA; + B += BB; + C += CC; + D += DD; + + A &= 0xFFFFFFFF; + B &= 0xFFFFFFFF; + C &= 0xFFFFFFFF; + D &= 0xFFFFFFFF; + + for (j = 0; j < 16; j++) + X[j] = 0; +} + +static void +copy64 (uint32x * M, unsigned char *in) +{ + int i; + + for (i = 0; i < 16; i++) + M[i] = (in[i * 4 + 3] << 24) | (in[i * 4 + 2] << 16) | + (in[i * 4 + 1] << 8) | (in[i * 4 + 0] << 0); +} + +static void +copy4 (unsigned char *out, uint32x x) +{ + out[0] = x & 0xFF; + out[1] = (x >> 8) & 0xFF; + out[2] = (x >> 16) & 0xFF; + out[3] = (x >> 24) & 0xFF; +} + +/* produce a md4 message digest from data of length n bytes */ +void +mdfour (unsigned char *out, unsigned char *in, int n) +{ + unsigned char buf[128]; + uint32x M[16]; + uint32x b = n * 8; + int i; + + A = 0x67452301; + B = 0xefcdab89; + C = 0x98badcfe; + D = 0x10325476; + + while (n > 64) + { + copy64 (M, in); + spa_mdfour64 (M); + in += 64; + n -= 64; + } + + for (i = 0; i < 128; i++) + buf[i] = 0; + memcpy (buf, in, n); + buf[n] = 0x80; + + if (n <= 55) + { + copy4 (buf + 56, b); + copy64 (M, buf); + spa_mdfour64 (M); + } + else + { + copy4 (buf + 120, b); + copy64 (M, buf); + spa_mdfour64 (M); + copy64 (M, buf + 64); + spa_mdfour64 (M); + } + + for (i = 0; i < 128; i++) + buf[i] = 0; + copy64 (M, buf); + + copy4 (out, A); + copy4 (out + 4, B); + copy4 (out + 8, C); + copy4 (out + 12, D); + + A = B = C = D = 0; +} + +char versionString[] = "libntlm version 0.21"; + +/* Utility routines that handle NTLM auth structures. */ + +/* The [IS]VAL macros are to take care of byte order for non-Intel + * Machines -- I think this file is OK, but it hasn't been tested. + * The other files (the ones stolen from Samba) should be OK. + */ + + +/* I am not crazy about these macros -- they seem to have gotten + * a bit complex. A new scheme for handling string/buffer fields + * in the structures probably needs to be designed + */ + +#define spa_bytes_add(ptr, header, buf, count) \ +{ \ +if (buf && count) \ + { \ + SSVAL(&ptr->header.len,0,count); \ + SSVAL(&ptr->header.maxlen,0,count); \ + SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \ + memcpy(ptr->buffer+ptr->bufIndex, buf, count); \ + ptr->bufIndex += count; \ + } \ +else \ + { \ + ptr->header.len = \ + ptr->header.maxlen = 0; \ + SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \ + } \ +} + +#define spa_string_add(ptr, header, string) \ +{ \ +char *p = string; \ +int len = 0; \ +if (p) len = strlen(p); \ +spa_bytes_add(ptr, header, ((unsigned char*)p), len); \ +} + +#define spa_unicode_add_string(ptr, header, string) \ +{ \ +char *p = string; \ +unsigned char *b = NULL; \ +int len = 0; \ +if (p) \ + { \ + len = strlen(p); \ + b = strToUnicode(p); \ + } \ +spa_bytes_add(ptr, header, b, len*2); \ +} + + +#define GetUnicodeString(structPtr, header) \ +unicodeToString(((char*)structPtr) + IVAL(&structPtr->header.offset,0) , SVAL(&structPtr->header.len,0)/2) +#define GetString(structPtr, header) \ +toString((((char *)structPtr) + IVAL(&structPtr->header.offset,0)), SVAL(&structPtr->header.len,0)) +#define DumpBuffer(fp, structPtr, header) \ +dumpRaw(fp,((unsigned char*)structPtr)+IVAL(&structPtr->header.offset,0),SVAL(&structPtr->header.len,0)) + + +static void +dumpRaw (FILE * fp, unsigned char *buf, size_t len) +{ + int i; + + for (i = 0; i < len; ++i) + fprintf (fp, "%02x ", buf[i]); + + fprintf (fp, "\n"); +} + +char * +unicodeToString (char *p, size_t len) +{ + int i; + static char buf[1024]; + + assert (len + 1 < sizeof buf); + + for (i = 0; i < len; ++i) + { + buf[i] = *p & 0x7f; + p += 2; + } + + buf[i] = '\0'; + return buf; +} + +static unsigned char * +strToUnicode (char *p) +{ + static unsigned char buf[1024]; + size_t l = strlen (p); + int i = 0; + + assert (l * 2 < sizeof buf); + + while (l--) + { + buf[i++] = *p++; + buf[i++] = 0; + } + + return buf; +} + +static unsigned char * +toString (char *p, size_t len) +{ + static unsigned char buf[1024]; + + assert (len + 1 < sizeof buf); + + memcpy (buf, p, len); + buf[len] = 0; + return buf; +} + +void +dumpSmbNtlmAuthRequest (FILE * fp, SPAAuthRequest * request) +{ + fprintf (fp, "NTLM Request:\n"); + fprintf (fp, " Ident = %s\n", request->ident); + fprintf (fp, " mType = %d\n", IVAL (&request->msgType, 0)); + fprintf (fp, " Flags = %08x\n", IVAL (&request->flags, 0)); + fprintf (fp, " User = %s\n", GetString (request, user)); + fprintf (fp, " Domain = %s\n", GetString (request, domain)); +} + +void +dumpSmbNtlmAuthChallenge (FILE * fp, SPAAuthChallenge * challenge) +{ + fprintf (fp, "NTLM Challenge:\n"); + fprintf (fp, " Ident = %s\n", challenge->ident); + fprintf (fp, " mType = %d\n", IVAL (&challenge->msgType, 0)); + fprintf (fp, " Domain = %s\n", GetUnicodeString (challenge, uDomain)); + fprintf (fp, " Flags = %08x\n", IVAL (&challenge->flags, 0)); + fprintf (fp, " Challenge = "); + dumpRaw (fp, challenge->challengeData, 8); +} + +void +dumpSmbNtlmAuthResponse (FILE * fp, SPAAuthResponse * response) +{ + fprintf (fp, "NTLM Response:\n"); + fprintf (fp, " Ident = %s\n", response->ident); + fprintf (fp, " mType = %d\n", IVAL (&response->msgType, 0)); + fprintf (fp, " LmResp = "); + DumpBuffer (fp, response, lmResponse); + fprintf (fp, " NTResp = "); + DumpBuffer (fp, response, ntResponse); + fprintf (fp, " Domain = %s\n", GetUnicodeString (response, uDomain)); + fprintf (fp, " User = %s\n", GetUnicodeString (response, uUser)); + fprintf (fp, " Wks = %s\n", GetUnicodeString (response, uWks)); + fprintf (fp, " sKey = "); + DumpBuffer (fp, response, sessionKey); + fprintf (fp, " Flags = %08x\n", IVAL (&response->flags, 0)); +} + +void +spa_build_auth_request (SPAAuthRequest * request, char *user, char *domain) +{ + char *u = strdup (user); + char *p = strchr (u, '@'); + + if (p) + { + if (!domain) + domain = p + 1; + *p = '\0'; + } + + request->bufIndex = 0; + memcpy (request->ident, "NTLMSSP\0\0\0", 8); + SIVAL (&request->msgType, 0, 1); + SIVAL (&request->flags, 0, 0x0000b207); /* have to figure out what these mean */ + spa_string_add (request, user, u); + spa_string_add (request, domain, domain); + free (u); +} + + + +void +spa_build_auth_challenge (SPAAuthRequest * request, SPAAuthChallenge * challenge) +{ + char chalstr[8]; + int i; + int p = (int)getpid(); + int random_seed = (int)time(NULL) ^ ((p << 16) | p); + + request = request; /* Added by PH to stop compilers whinging */ + + /* Ensure challenge data is cleared, in case it isn't all used. This + patch added by PH on suggestion of Russell King */ + + memset(challenge, 0, sizeof(SPAAuthChallenge)); + + challenge->bufIndex = 0; + memcpy (challenge->ident, "NTLMSSP\0", 8); + SIVAL (&challenge->msgType, 0, 2); + SIVAL (&challenge->flags, 0, 0x00008201); + SIVAL (&challenge->uDomain.len, 0, 0x0000); + SIVAL (&challenge->uDomain.maxlen, 0, 0x0000); + SIVAL (&challenge->uDomain.offset, 0, 0x00002800); + + /* generate eight pseudo random bytes (method ripped from host.c) */ + + for(i=0;i<8;i++) { + chalstr[i] = (unsigned char)(random_seed >> 16) % 256; + random_seed = (1103515245 - (chalstr[i])) * random_seed + 12345; + }; + + memcpy(challenge->challengeData,chalstr,8); +} + + + + +/* This is the original source of this function, preserved here for reference. +The new version below was re-organized by PH following a patch and some further +suggestions from Mark Lyda to fix the problem that is described at the head of +this module. At the same time, I removed the untidiness in the code below that +involves the "d" and "domain" variables. */ + +#ifdef NEVER +void +spa_build_auth_response (SPAAuthChallenge * challenge, + SPAAuthResponse * response, char *user, + char *password) +{ + uint8x lmRespData[24]; + uint8x ntRespData[24]; + char *d = strdup (GetUnicodeString (challenge, uDomain)); + char *domain = d; + char *u = strdup (user); + char *p = strchr (u, '@'); + + if (p) + { + domain = p + 1; + *p = '\0'; + } + + spa_smb_encrypt ((uchar *)password, challenge->challengeData, lmRespData); + spa_smb_nt_encrypt ((uchar *)password, challenge->challengeData, ntRespData); + + response->bufIndex = 0; + memcpy (response->ident, "NTLMSSP\0\0\0", 8); + SIVAL (&response->msgType, 0, 3); + + spa_bytes_add (response, lmResponse, lmRespData, 24); + spa_bytes_add (response, ntResponse, ntRespData, 24); + spa_unicode_add_string (response, uDomain, domain); + spa_unicode_add_string (response, uUser, u); + spa_unicode_add_string (response, uWks, u); + spa_string_add (response, sessionKey, NULL); + + response->flags = challenge->flags; + + free (d); + free (u); +} +#endif + + +/* This is the re-organized version (see comments above) */ + +void +spa_build_auth_response (SPAAuthChallenge * challenge, + SPAAuthResponse * response, char *user, + char *password) +{ + uint8x lmRespData[24]; + uint8x ntRespData[24]; + uint32x cf = IVAL(&challenge->flags, 0); + char *u = strdup (user); + char *p = strchr (u, '@'); + char *d = NULL; + char *domain; + + if (p) + { + domain = p + 1; + *p = '\0'; + } + + else domain = d = strdup((cf & 0x1)? + (const char *)GetUnicodeString(challenge, uDomain) : + (const char *)GetString(challenge, uDomain)); + + spa_smb_encrypt ((uchar *)password, challenge->challengeData, lmRespData); + spa_smb_nt_encrypt ((uchar *)password, challenge->challengeData, ntRespData); + + response->bufIndex = 0; + memcpy (response->ident, "NTLMSSP\0\0\0", 8); + SIVAL (&response->msgType, 0, 3); + + spa_bytes_add (response, lmResponse, lmRespData, (cf & 0x200) ? 24 : 0); + spa_bytes_add (response, ntResponse, ntRespData, (cf & 0x8000) ? 24 : 0); + + if (cf & 0x1) { /* Unicode Text */ + spa_unicode_add_string (response, uDomain, domain); + spa_unicode_add_string (response, uUser, u); + spa_unicode_add_string (response, uWks, u); + } else { /* OEM Text */ + spa_string_add (response, uDomain, domain); + spa_string_add (response, uUser, u); + spa_string_add (response, uWks, u); + } + + spa_string_add (response, sessionKey, NULL); + response->flags = challenge->flags; + + if (d != NULL) free (d); + free (u); +} diff --git a/src/src/auths/auth-spa.h b/src/src/auths/auth-spa.h new file mode 100644 index 000000000..52394e570 --- /dev/null +++ b/src/src/auths/auth-spa.h @@ -0,0 +1,91 @@ +/* $Cambridge: exim/src/src/auths/auth-spa.h,v 1.1 2004/10/07 13:10:00 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * This file provides the necessary methods for authenticating with + * Microsoft's Secure Password Authentication. + + * All the code used here was torn by Marc Prud'hommeaux out of the + * Samba project (by Andrew Tridgell, Jeremy Allison, and others). + */ + +/* It seems that some systems have existing but different definitions of some +of the following types. I received a complaint about "int16" causing +compilation problems. So I (PH) have renamed them all, to be on the safe side. + +typedef signed short int16; +typedef unsigned short uint16; +typedef unsigned uint32; +typedef unsigned char uint8; +*/ + +typedef signed short int16x; +typedef unsigned short uint16x; +typedef unsigned uint32x; +typedef unsigned char uint8x; + +typedef struct +{ + uint16x len; + uint16x maxlen; + uint32x offset; +} SPAStrHeader; + +typedef struct +{ + char ident[8]; + uint32x msgType; + SPAStrHeader uDomain; + uint32x flags; + uint8x challengeData[8]; + uint8x reserved[8]; + SPAStrHeader emptyString; + uint8x buffer[1024]; + uint32x bufIndex; +} SPAAuthChallenge; + + +typedef struct +{ + char ident[8]; + uint32x msgType; + uint32x flags; + SPAStrHeader user; + SPAStrHeader domain; + uint8x buffer[1024]; + uint32x bufIndex; +} SPAAuthRequest; + +typedef struct +{ + char ident[8]; + uint32x msgType; + SPAStrHeader lmResponse; + SPAStrHeader ntResponse; + SPAStrHeader uDomain; + SPAStrHeader uUser; + SPAStrHeader uWks; + SPAStrHeader sessionKey; + uint32x flags; + uint8x buffer[1024]; + uint32x bufIndex; +} SPAAuthResponse; + +#define spa_request_length(ptr) (((ptr)->buffer - (uint8x*)(ptr)) + (ptr)->bufIndex) + +void spa_bits_to_base64 (unsigned char *, const unsigned char *, int); +int spa_base64_to_bits(char *, const char *); +void spa_build_auth_response (SPAAuthChallenge *challenge, + SPAAuthResponse *response, char *user, char *password); +void spa_build_auth_request (SPAAuthRequest *request, char *user, + char *domain); +extern void spa_smb_encrypt (unsigned char * passwd, unsigned char * c8, + unsigned char * p24); +extern void spa_smb_nt_encrypt (unsigned char * passwd, unsigned char * c8, + unsigned char * p24); +extern char *unicodeToString(char *p, size_t len); +extern void spa_build_auth_challenge(SPAAuthRequest *, SPAAuthChallenge *); + diff --git a/src/src/auths/b64decode.c b/src/src/auths/b64decode.c new file mode 100644 index 000000000..3524ac151 --- /dev/null +++ b/src/src/auths/b64decode.c @@ -0,0 +1,86 @@ +/* $Cambridge: exim/src/src/auths/b64decode.c,v 1.1 2004/10/07 13:10:00 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Decode byte-string in base 64 * +*************************************************/ + +/* This function decodes a string in base 64 format as defined in RFC 2045 +(MIME) and required by the SMTP AUTH extension (RFC 2554). The decoding +algorithm is written out in a straightforward way. Turning it into some kind of +compact loop is messy and would probably run more slowly. + +Arguments: + code points to the coded string, zero-terminated + ptr where to put the pointer to the result, which is in + dynamic store + +Returns: the number of bytes in the result, + or -1 if the input was malformed + +A zero is added on to the end to make it easy in cases where the result is to +be interpreted as text. This is not included in the count. */ + +static uschar dec64table[] = { + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, /* 0-15 */ + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, /* 16-31 */ + 255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63, /* 32-47 */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255,255,255,255, /* 48-63 */ + 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 64-79 */ + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255, /* 80-95 */ + 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 96-111 */ + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255 /* 112-127*/ +}; + +int +auth_b64decode(uschar *code, uschar **ptr) +{ +register int x, y; +uschar *result = store_get(3*(Ustrlen(code)/4) + 1); + +*ptr = result; + +/* Each cycle of the loop handles a quantum of 4 input bytes. For the last +quantum this may decode to 1, 2, or 3 output bytes. */ + +while ((x = (*code++)) != 0) + { + if (x > 127 || (x = dec64table[x]) == 255) return -1; + if ((y = (*code++)) == 0 || (y = dec64table[y]) == 255) + return -1; + *result++ = (x << 2) | (y >> 4); + + if ((x = (*code++)) == '=') + { + if (*code++ != '=' || *code != 0) return -1; + } + else + { + if (x > 127 || (x = dec64table[x]) == 255) return -1; + *result++ = (y << 4) | (x >> 2); + if ((y = (*code++)) == '=') + { + if (*code != 0) return -1; + } + else + { + if (y > 127 || (y = dec64table[y]) == 255) return -1; + *result++ = (x << 6) | y; + } + } + } + +*result = 0; +return result - *ptr; +} + +/* End of b64decode.c */ diff --git a/src/src/auths/b64encode.c b/src/src/auths/b64encode.c new file mode 100644 index 000000000..6fc8d7b5d --- /dev/null +++ b/src/src/auths/b64encode.c @@ -0,0 +1,76 @@ +/* $Cambridge: exim/src/src/auths/b64encode.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Encode byte-string in base 64 * +*************************************************/ + +/* This function encodes a string of bytes, containing any values whatsoever, +in base 64 as defined in RFC 2045 (MIME) and required by the SMTP AUTH +extension (RFC 2554). The encoding algorithm is written out in a +straightforward way. Turning it into some kind of compact loop is messy and +would probably run more slowly. + +Arguments: + clear points to the clear text bytes + len the number of bytes to encode + +Returns: a pointer to the zero-terminated base 64 string, which + is in working store +*/ + +static uschar *enc64table = + US"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +uschar * +auth_b64encode(uschar *clear, int len) +{ +uschar *code = store_get(4*((len+2)/3) + 1); +uschar *p = code; + +while (len-- >0) + { + register int x, y; + + x = *clear++; + *p++ = enc64table[(x >> 2) & 63]; + + if (len-- <= 0) + { + *p++ = enc64table[(x << 4) & 63]; + *p++ = '='; + *p++ = '='; + break; + } + + y = *clear++; + *p++ = enc64table[((x << 4) | ((y >> 4) & 15)) & 63]; + + if (len-- <= 0) + { + *p++ = enc64table[(y << 2) & 63]; + *p++ = '='; + break; + } + + x = *clear++; + *p++ = enc64table[((y << 2) | ((x >> 6) & 3)) & 63]; + + *p++ = enc64table[x & 63]; + } + +*p = 0; + +return code; +} + +/* End of b64encode.c */ diff --git a/src/src/auths/call_pam.c b/src/src/auths/call_pam.c new file mode 100644 index 000000000..b5f7992a5 --- /dev/null +++ b/src/src/auths/call_pam.c @@ -0,0 +1,204 @@ +/* $Cambridge: exim/src/src/auths/call_pam.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + +/* This module contains functions that call the PAM authentication mechanism +defined by Sun for Solaris and also available for Linux and other OS. + +We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the PAM headers +available for compiling. Therefore, compile these functions only if SUPPORT_PAM +is defined. However, some compilers don't like compiling empty modules, so keep +them happy with a dummy when skipping the rest. Make it reference itself to +stop picky compilers complaining that it is unused, and put in a dummy argument +to stop even pickier compilers complaining about infinite loops. */ + +#ifndef SUPPORT_PAM +static void dummy(int x) { dummy(x-1); } +#else /* SUPPORT_PAM */ + +#ifdef PAM_H_IN_PAM +#include +#else +#include +#endif + +/* According to the specification, it should be possible to have an application +data pointer passed to the conversation function. However, I was unable to get +this to work on Solaris 2.6, so static variables are used instead. */ + +static int pam_conv_had_error; +static uschar *pam_args; +static BOOL pam_arg_ended; + + + +/************************************************* +* PAM conversation function * +*************************************************/ + +/* This function is passed to the PAM authentication function, and it calls it +back when it wants data from the client. The string list is in pam_args. When +we reach the end, we pass back an empty string once. If this function is called +again, it will give an error response. This is protection against something +crazy happening. + +Arguments: + num_msg number of messages associated with the call + msg points to an array of length num_msg of pam_message structures + resp set to point to the response block, which has to be got by + this function + appdata_ptr the application data pointer - not used because in Solaris + 2.6 it always arrived in pam_converse() as NULL + +Returns: a PAM return code +*/ + +static int +pam_converse (int num_msg, PAM_CONVERSE_ARG2_TYPE **msg, + struct pam_response **resp, void *appdata_ptr) +{ +int i; +int sep = 0; +struct pam_response *reply; + +if (pam_arg_ended) return PAM_CONV_ERR; + +reply = malloc(sizeof(struct pam_response) * num_msg); + +if (reply == NULL) return PAM_CONV_ERR; + +for (i = 0; i < num_msg; i++) + { + uschar *arg; + switch (msg[i]->msg_style) + { + case PAM_PROMPT_ECHO_ON: + case PAM_PROMPT_ECHO_OFF: + arg = string_nextinlist(&pam_args, &sep, big_buffer, big_buffer_size); + if (arg == NULL) + { + arg = US""; + pam_arg_ended = TRUE; + } + reply[i].resp = CS string_copy_malloc(arg); /* PAM frees resp */ + reply[i].resp_retcode = PAM_SUCCESS; + break; + + case PAM_TEXT_INFO: /* Just acknowledge messages */ + case PAM_ERROR_MSG: + reply[i].resp_retcode = PAM_SUCCESS; + reply[i].resp = NULL; + break; + + default: /* Must be an error of some sort... */ + free (reply); + pam_conv_had_error = TRUE; + return PAM_CONV_ERR; + } + } + +*resp = reply; +return PAM_SUCCESS; +} + + + +/************************************************* +* Perform PAM authentication * +*************************************************/ + +/* This function calls the PAM authentication mechanism, passing over one or +more data strings. + +Arguments: + s a colon-separated list of strings + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_pam(uschar *s, uschar **errptr) +{ +pam_handle_t *pamh = NULL; +struct pam_conv pamc; +int pam_error; +int sep = 0; +uschar *user; + +/* Set up the input data structure: the address of the conversation function, +and a pointer to application data, which we don't use because I couldn't get it +to work under Solaris 2.6 - it always arrived in pam_converse() as NULL. */ + +pamc.conv = pam_converse; +pamc.appdata_ptr = NULL; + +/* Initialize the static data - the current input data, the error flag, and the +flag for data end. */ + +pam_args = s; +pam_conv_had_error = FALSE; +pam_arg_ended = FALSE; + +/* The first string in the list is the user. If this is an empty string, we +fail. PAM doesn't support authentication with an empty user (it prompts for it, +causing a potential mis-interpretation). */ + +user = string_nextinlist(&pam_args, &sep, big_buffer, big_buffer_size); +if (user == NULL || user[0] == 0) return FAIL; + +/* Start off PAM interaction */ + +DEBUG(D_auth) + debug_printf("Running PAM authentication for user \"%s\"\n", user); + +pam_error = pam_start ("exim", CS user, &pamc, &pamh); + +/* Do the authentication - the pam_authenticate() will call pam_converse() to +get the data it wants. After successful authentication we call pam_acct_mgmt() +to apply any other restrictions (e.g. only some times of day). */ + +if (pam_error == PAM_SUCCESS) + { + pam_error = pam_authenticate (pamh, PAM_SILENT); + if (pam_error == PAM_SUCCESS && !pam_conv_had_error) + pam_error = pam_acct_mgmt (pamh, PAM_SILENT); + } + +/* Finish the PAM interaction - this causes it to clean up store etc. Unclear +what should be passed as the second argument. */ + +pam_end(pamh, PAM_SUCCESS); + +/* Sort out the return code. If not success, set the error message. */ + +if (pam_error == PAM_SUCCESS) + { + DEBUG(D_auth) debug_printf("PAM success\n"); + return OK; + } + +*errptr = (uschar *)pam_strerror(pamh, pam_error); +DEBUG(D_auth) debug_printf("PAM error: %s\n", *errptr); + +if (pam_error == PAM_USER_UNKNOWN || + pam_error == PAM_AUTH_ERR || + pam_error == PAM_ACCT_EXPIRED) + return FAIL; + +return ERROR; +} + +#endif /* SUPPORT_PAM */ + +/* End of call_pam.c */ diff --git a/src/src/auths/call_pwcheck.c b/src/src/auths/call_pwcheck.c new file mode 100644 index 000000000..c029cba68 --- /dev/null +++ b/src/src/auths/call_pwcheck.c @@ -0,0 +1,122 @@ +/* $Cambridge: exim/src/src/auths/call_pwcheck.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This module contains interface functions to the two Cyrus authentication +daemons. The original one was "pwcheck", which gives its name to the source +file. This is now deprecated in favour of "saslauthd". */ + + +#include "../exim.h" +#include "pwcheck.h" + + +/************************************************* +* External entry point for pwcheck * +*************************************************/ + +/* This function calls the now-deprecated "pwcheck" Cyrus-SASL authentication +daemon, passing over a colon-separated user name and password. As this is +called from the string expander, the string will always be in dynamic store and +can be overwritten. + +Arguments: + s a colon-separated username:password string + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_pwcheck(uschar *s, uschar **errptr) +{ +uschar *reply = NULL; +uschar *pw = Ustrrchr(s, ':'); + +if (pw == NULL) + { + *errptr = US"pwcheck: malformed input - missing colon"; + return ERROR; + } + +*pw++ = 0; /* Separate user and password */ + +DEBUG(D_auth) + debug_printf("Running pwcheck authentication for user \"%s\"\n", s); + +switch (pwcheck_verify_password(CS s, CS pw, (const char **)(&reply))) + { + case PWCHECK_OK: + DEBUG(D_auth) debug_printf("pwcheck: success (%s)\n", reply); + return OK; + + case PWCHECK_NO: + DEBUG(D_auth) debug_printf("pwcheck: access denied (%s)\n", reply); + return FAIL; + + default: + DEBUG(D_auth) debug_printf("pwcheck: query failed (%s)\n", reply); + *errptr = reply; + return ERROR; + } +} + + +/************************************************* +* External entry point for pwauthd * +*************************************************/ + +/* This function calls the "saslauthd" Cyrus-SASL authentication daemon, +saslauthd, As this is called from the string expander, all the strings will +always be in dynamic store and can be overwritten. + +Arguments: + username username + password password + service optional service + realm optional realm + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_saslauthd(uschar *username, uschar *password, uschar *service, + uschar *realm, uschar **errptr) +{ +uschar *reply = NULL; + +if (service == NULL) service = US""; +if (realm == NULL) realm = US""; + +DEBUG(D_auth) + debug_printf("Running saslauthd authentication for user \"%s\" \n", username); + +switch (saslauthd_verify_password(username, password, service, + realm, (const uschar **)(&reply))) + { + case PWCHECK_OK: + DEBUG(D_auth) debug_printf("saslauthd: success (%s)\n", reply); + return OK; + + case PWCHECK_NO: + DEBUG(D_auth) debug_printf("saslauthd: access denied (%s)\n", reply); + return FAIL; + + default: + DEBUG(D_auth) debug_printf("saslauthd: query failed (%s)\n", reply); + *errptr = reply; + return ERROR; + } +} + +/* End of call_pwcheck.c */ diff --git a/src/src/auths/call_radius.c b/src/src/auths/call_radius.c new file mode 100644 index 000000000..5cfcd4eb6 --- /dev/null +++ b/src/src/auths/call_radius.c @@ -0,0 +1,190 @@ +/* $Cambridge: exim/src/src/auths/call_radius.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file was originally supplied by Ian Kirk. The libradius support came +from Alex Kiernan. */ + +#include "../exim.h" + +/* This module contains functions that call the Radius authentication +mechanism. + +We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the Radius headers +available for compiling. Therefore, compile these functions only if +RADIUS_CONFIG_FILE is defined. However, some compilers don't like compiling +empty modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef RADIUS_CONFIG_FILE +static void dummy(int x) { dummy(x-1); } +#else /* RADIUS_CONFIG_FILE */ + + +/* Two different Radius libraries are supported. The default is radiusclient. */ + +#ifdef RADIUS_LIB_RADLIB + #include +#else + #ifndef RADIUS_LIB_RADIUSCLIENT + #define RADIUS_LIB_RADIUSCLIENT + #endif + #include +#endif + + + +/************************************************* +* Perform RADIUS authentication * +*************************************************/ + +/* This function calls the Radius authentication mechanism, passing over one or +more data strings. + +Arguments: + s a colon-separated list of strings + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_radius(uschar *s, uschar **errptr) +{ +uschar *user; +uschar *radius_args = s; +int result; +int sep = 0; + +#ifdef RADIUS_LIB_RADLIB +struct rad_handle *h; +#else +VALUE_PAIR *send = NULL; +VALUE_PAIR *received; +unsigned int service = PW_AUTHENTICATE_ONLY; +char msg[4096]; +#endif + + +user = string_nextinlist(&radius_args, &sep, big_buffer, big_buffer_size); +if (user == NULL) user = US""; + +DEBUG(D_auth) debug_printf("Running RADIUS authentication for user \"%s\" " + "and \"%s\"\n", user, radius_args); + +*errptr = NULL; + + +/* Authenticate using the radiusclient library */ + +#ifdef RADIUS_LIB_RADIUSCLIENT + +rc_openlog("exim"); + +if (rc_read_config(RADIUS_CONFIG_FILE) != 0) + *errptr = string_sprintf("RADIUS: can't open %s", RADIUS_CONFIG_FILE); + +else if (rc_read_dictionary(rc_conf_str("dictionary")) != 0) + *errptr = string_sprintf("RADIUS: can't read dictionary"); + +else if (rc_avpair_add(&send, PW_USER_NAME, user, 0) == NULL) + *errptr = string_sprintf("RADIUS: add user name failed\n"); + +else if (rc_avpair_add(&send, PW_USER_PASSWORD, CS radius_args, 0) == NULL) + *errptr = string_sprintf("RADIUS: add password failed\n"); + +else if (rc_avpair_add(&send, PW_SERVICE_TYPE, &service, 0) == NULL) + *errptr = string_sprintf("RADIUS: add service type failed\n"); + +if (*errptr != NULL) + { + DEBUG(D_auth) debug_printf("%s\n", *errptr); + return ERROR; + } + +result = rc_auth(0, send, &received, msg); +DEBUG(D_auth) debug_printf("RADIUS code returned %d\n", result); + +switch (result) + { + case OK_RC: + return OK; + + case ERROR_RC: + return FAIL; + + case TIMEOUT_RC: + *errptr = US"RADIUS: timed out"; + return ERROR; + + default: + case BADRESP_RC: + *errptr = string_sprintf("RADIUS: unexpected response (%d)", result); + return ERROR; + } + +#else /* RADIUS_LIB_RADIUSCLIENT not set => RADIUS_LIB_RADLIB is set */ + +/* Authenticate using the libradius library */ + +h = rad_auth_open(); +if (h == NULL) + { + *errptr = string_sprintf("RADIUS: can't initialise libradius"); + return ERROR; + } +if (rad_config(h, RADIUS_CONFIG_FILE) != 0 || + rad_create_request(h, RAD_ACCESS_REQUEST) != 0 || + rad_put_string(h, RAD_USER_NAME, CS user) != 0 || + rad_put_string(h, RAD_USER_PASSWORD, CS radius_args) != 0 || + rad_put_int(h, RAD_SERVICE_TYPE, RAD_AUTHENTICATE_ONLY) != 0) + { + *errptr = string_sprintf("RADIUS: %s", rad_strerror(h)); + result = ERROR; + } +else + { + result = rad_send_request(h); + + switch(result) + { + case RAD_ACCESS_ACCEPT: + result = OK; + break; + + case RAD_ACCESS_REJECT: + result = FAIL; + break; + + case -1: + *errptr = string_sprintf("RADIUS: %s", rad_strerror(h)); + result = ERROR; + break; + + default: + *errptr = string_sprintf("RADIUS: unexpected response (%d)", result); + result= ERROR; + break; + } + } + +if (*errptr != NULL) DEBUG(D_auth) debug_printf("%s\n", *errptr); +rad_close(h); +return result; + +#endif /* RADIUS_LIB_RADLIB */ +} + +#endif /* RADIUS_CONFIG_FILE */ + +/* End of call_radius.c */ diff --git a/src/src/auths/cram_md5.c b/src/src/auths/cram_md5.c new file mode 100644 index 000000000..bd9547a42 --- /dev/null +++ b/src/src/auths/cram_md5.c @@ -0,0 +1,347 @@ +/* $Cambridge: exim/src/src/auths/cram_md5.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +/* The stand-alone version just tests the algorithm. We have to drag +in the MD5 computation functions, without their own stand-alone main +program. */ + +#ifdef STAND_ALONE +#define CRAM_STAND_ALONE +#include "md5.c" + + +/* This is the normal, non-stand-alone case */ + +#else +#include "../exim.h" +#include "cram_md5.h" + +/* Options specific to the cram_md5 authentication mechanism. */ + +optionlist auth_cram_md5_options[] = { + { "client_name", opt_stringptr, + (void *)(offsetof(auth_cram_md5_options_block, client_name)) }, + { "client_secret", opt_stringptr, + (void *)(offsetof(auth_cram_md5_options_block, client_secret)) }, + { "server_secret", opt_stringptr, + (void *)(offsetof(auth_cram_md5_options_block, server_secret)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_cram_md5_options_count = + sizeof(auth_cram_md5_options)/sizeof(optionlist); + +/* Default private options block for the contidion authentication method. */ + +auth_cram_md5_options_block auth_cram_md5_option_defaults = { + NULL, /* server_secret */ + NULL, /* client_secret */ + NULL /* client_name */ +}; + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_cram_md5_init(auth_instance *ablock) +{ +auth_cram_md5_options_block *ob = + (auth_cram_md5_options_block *)(ablock->options_block); +if (ob->server_secret != NULL) ablock->server = TRUE; +if (ob->client_secret != NULL) + { + ablock->client = TRUE; + if (ob->client_name == NULL) ob->client_name = primary_hostname; + } +} + +#endif /* STAND_ALONE */ + + + +/************************************************* +* Peform the CRAM-MD5 algorithm * +*************************************************/ + +/* The CRAM-MD5 algorithm is described in RFC 2195. It computes + + MD5((secret XOR opad), MD5((secret XOR ipad), challenge)) + +where secret is padded out to 64 characters (after being reduced to an MD5 +digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and +0x5c respectively, and comma means concatenation. + +Arguments: + secret the shared secret + challenge the challenge text + digest 16-byte slot to put the answer in + +Returns: nothing +*/ + +static void +compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr) +{ +md5 base; +int i; +int len = Ustrlen(secret); +uschar isecret[64]; +uschar osecret[64]; +uschar md5secret[16]; + +/* If the secret is longer than 64 characters, we compute its MD5 digest +and use that. */ + +if (len > 64) + { + md5_start(&base); + md5_end(&base, (uschar *)secret, len, md5secret); + secret = (uschar *)md5secret; + len = 16; + } + +/* The key length is now known to be <= 64. Set up the padded and xor'ed +versions. */ + +memcpy(isecret, secret, len); +memset(isecret+len, 0, 64-len); +memcpy(osecret, isecret, 64); + +for (i = 0; i < 64; i++) + { + isecret[i] ^= 0x36; + osecret[i] ^= 0x5c; + } + +/* Compute the inner MD5 digest */ + +md5_start(&base); +md5_mid(&base, isecret); +md5_end(&base, (uschar *)challenge, Ustrlen(challenge), md5secret); + +/* Compute the outer MD5 digest */ + +md5_start(&base); +md5_mid(&base, osecret); +md5_end(&base, md5secret, 16, digestptr); +} + + +#ifndef STAND_ALONE + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_cram_md5_server(auth_instance *ablock, uschar *data) +{ +auth_cram_md5_options_block *ob = + (auth_cram_md5_options_block *)(ablock->options_block); +uschar *challenge = string_sprintf("<%d.%d@%s>", getpid(), time(NULL), + primary_hostname); +uschar *clear, *secret; +uschar digest[16]; +int i, rc, len; + +/* If we are running in the test harness, always send the same challenge, +an example string taken from the RFC. */ + +if (running_in_test_harness) + challenge = US"<1896.697170952@postoffice.reston.mci.net>"; + +/* No data should have been sent with the AUTH command */ + +if (*data != 0) return UNEXPECTED; + +/* Send the challenge, read the return */ + +if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc; +if ((len = auth_b64decode(data, &clear)) < 0) return BAD64; + +/* The return consists of a user name, space-separated from the CRAM-MD5 +digest, expressed in hex. Extract the user name and put it in $1. Then check +that the remaining length is 32. */ + +expand_nstring[1] = clear; +while (*clear != 0 && !isspace(*clear)) clear++; +if (!isspace(*clear)) return FAIL; +*clear++ = 0; + +expand_nlength[1] = clear - expand_nstring[1] - 1; +if (len - expand_nlength[1] - 1 != 32) return FAIL; +expand_nmax = 1; + +/* Expand the server_secret string so that it can compute a value dependent on +the user name if necessary. */ + +debug_print_string(ablock->server_debug_string); /* customized debugging */ +secret = expand_string(ob->server_secret); + +/* A forced fail implies failure of authentication - i.e. we have no secret for +the given name. */ + +if (secret == NULL) + { + if (expand_string_forcedfail) return FAIL; + auth_defer_msg = expand_string_message; + return DEFER; + } + +/* Compute the CRAM-MD5 digest that we should have received from the client. */ + +compute_cram_md5(secret, challenge, digest); + +HDEBUG(D_auth) + { + uschar buff[64]; + debug_printf("CRAM-MD5: user name = %s\n", expand_nstring[1]); + debug_printf(" challenge = %s\n", challenge); + debug_printf(" received = %s\n", clear); + Ustrcpy(buff," digest = "); + for (i = 0; i < 16; i++) sprintf(CS buff+22+2*i, "%02x", digest[i]); + debug_printf("%.54s\n", buff); + } + +/* We now have to compare the digest, which is 16 bytes in binary, with the +data received, which is expressed in lower case hex. We checked above that +there were 32 characters of data left. */ + +for (i = 0; i < 16; i++) + { + int a = *clear++; + int b = *clear++; + if (((((a >= 'a')? a - 'a' + 10 : a - '0') << 4) + + ((b >= 'a')? b - 'a' + 10 : b - '0')) != digest[i]) return FAIL; + } + +return OK; +} + + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_cram_md5_client( + auth_instance *ablock, /* authenticator block */ + smtp_inblock *inblock, /* input connection */ + smtp_outblock *outblock, /* output connection */ + int timeout, /* command timeout */ + uschar *buffer, /* for reading response */ + int buffsize) /* size of buffer */ +{ +auth_cram_md5_options_block *ob = + (auth_cram_md5_options_block *)(ablock->options_block); +uschar *secret = expand_string(ob->client_secret); +uschar *name = expand_string(ob->client_name); +uschar *challenge, *p; +int i; +uschar digest[16]; + +/* If expansion of either the secret or the user name failed, return CANCELLED +or ERROR, as approriate. */ + +if (secret == NULL || name == NULL) + { + if (expand_string_forcedfail) return CANCELLED; + string_format(buffer, buffsize, "expansion of \"%s\" failed in " + "%s authenticator: %s", + (secret == NULL)? ob->client_secret : ob->client_name, + ablock->name, expand_string_message); + return ERROR; + } + +/* Initiate the authentication exchange and read the challenge, which arrives +in base 64. */ + +if (smtp_write_command(outblock, FALSE, "AUTH %s\r\n", ablock->public_name) < 0) + return FAIL_SEND; +if (smtp_read_response(inblock, (uschar *)buffer, buffsize, '3', timeout) < 0) + return FAIL; + +if (auth_b64decode(buffer + 4, &challenge) < 0) + { + string_format(buffer, buffsize, "bad base 64 string in challenge: %s", + big_buffer + 4); + return ERROR; + } + +/* Run the CRAM-MD5 algorithm on the secret and the challenge */ + +compute_cram_md5(secret, challenge, digest); + +/* Create the response from the user name plus the CRAM-MD5 digest */ + +string_format(big_buffer, big_buffer_size - 36, "%s", name); +p = big_buffer; +while (*p != 0) p++; +*p++ = ' '; + +for (i = 0; i < 16; i++) + { + sprintf(CS p, "%02x", digest[i]); + p += 2; + } + +/* Send the response, in base 64, and check the result. The response is +in big_buffer, but auth_b64encode() returns its result in working store, +so calling smtp_write_command(), which uses big_buffer, is OK. */ + +buffer[0] = 0; +if (smtp_write_command(outblock, FALSE, "%s\r\n", auth_b64encode(big_buffer, + p - big_buffer)) < 0) return FAIL_SEND; + +return smtp_read_response(inblock, (uschar *)buffer, buffsize, '2', timeout)? + OK : FAIL; +} +#endif /* STAND_ALONE */ + + +/************************************************* +************************************************** +* Stand-alone test program * +************************************************** +*************************************************/ + +#ifdef STAND_ALONE + +int main(int argc, char **argv) +{ +int i; +uschar *secret = US argv[1]; +uschar *challenge = US argv[2]; +uschar digest[16]; + +compute_cram_md5(secret, challenge, digest); + +for (i = 0; i < 16; i++) printf("%02x", digest[i]); +printf("\n"); + +return 0; +} + +#endif + +/* End of cram_md5.c */ diff --git a/src/src/auths/cram_md5.h b/src/src/auths/cram_md5.h new file mode 100644 index 000000000..7f57750e4 --- /dev/null +++ b/src/src/auths/cram_md5.h @@ -0,0 +1,34 @@ +/* $Cambridge: exim/src/src/auths/cram_md5.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *server_secret; + uschar *client_secret; + uschar *client_name; +} auth_cram_md5_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_cram_md5_options[]; +extern int auth_cram_md5_options_count; + +/* Block containing default values. */ + +extern auth_cram_md5_options_block auth_cram_md5_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_cram_md5_init(auth_instance *); +extern int auth_cram_md5_server(auth_instance *, uschar *); +extern int auth_cram_md5_client(auth_instance *, smtp_inblock *, + smtp_outblock *, int, uschar *, int); + +/* End of cram_md5.h */ diff --git a/src/src/auths/cyrus_sasl.c b/src/src/auths/cyrus_sasl.c new file mode 100644 index 000000000..4d75aa434 --- /dev/null +++ b/src/src/auths/cyrus_sasl.c @@ -0,0 +1,336 @@ +/* $Cambridge: exim/src/src/auths/cyrus_sasl.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2003 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This code was contributed by Matthew Byng-Maddick */ + +/* Copyright (c) A L Digital 2004 */ + +/* A generic (mechanism independent) Cyrus SASL authenticator. */ + + +#include "../exim.h" + + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the Cyrus SASL header +available for compiling. Therefore, compile these functions only if +AUTH_CYRUS_SASL is defined. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef AUTH_CYRUS_SASL +static void dummy(int x) { dummy(x-1); } +#else + + +#include +#include "cyrus_sasl.h" + +/* Options specific to the cyrus_sasl authentication mechanism. */ + +optionlist auth_cyrus_sasl_options[] = { + { "server_hostname", opt_stringptr, + (void *)(offsetof(auth_cyrus_sasl_options_block, server_hostname)) }, + { "server_mech", opt_stringptr, + (void *)(offsetof(auth_cyrus_sasl_options_block, server_mech)) }, + { "server_realm", opt_stringptr, + (void *)(offsetof(auth_cyrus_sasl_options_block, server_realm)) }, + { "server_service", opt_stringptr, + (void *)(offsetof(auth_cyrus_sasl_options_block, server_service)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_cyrus_sasl_options_count = + sizeof(auth_cyrus_sasl_options)/sizeof(optionlist); + +/* Default private options block for the contidion authentication method. */ + +auth_cyrus_sasl_options_block auth_cyrus_sasl_option_defaults = { + US"smtp", /* server_service */ + US"$primary_hostname", /* server_hostname */ + NULL, /* server_realm */ + NULL /* server_mech */ +}; + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_cyrus_sasl_init(auth_instance *ablock) +{ +auth_cyrus_sasl_options_block *ob = + (auth_cyrus_sasl_options_block *)(ablock->options_block); +sasl_callback_t cbs[]={{SASL_CB_LIST_END, NULL, NULL}}; +sasl_conn_t *conn; +uschar *list, *listptr, *buffer; +int rc, i; +unsigned int len; +uschar *rs_point; + +/* default the mechanism to our "public name" */ +if(ob->server_mech == NULL) + ob->server_mech=string_copy(ablock->public_name); + +/* we're going to initialise the library to check that there is an + * authenticator of type whatever mechanism we're using + */ +rc=sasl_server_init(cbs, "exim"); +if( rc != SASL_OK ) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't initialise Cyrus SASL library.", ablock->name); + +rc=sasl_server_new(CS ob->server_service, CS primary_hostname, + CS ob->server_realm, NULL, NULL, NULL, 0, &conn); +if( rc != SASL_OK ) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't initialise Cyrus SASL server connection.", ablock->name); + +rc=sasl_listmech(conn, NULL, "", ":", "", (const char **)(&list), &len, &i); +if( rc != SASL_OK ) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't get Cyrus SASL mechanism list.", ablock->name); + +i=':'; +listptr=list; + +HDEBUG(D_auth) debug_printf("Cyrus SASL knows about: %s\n", list); + +/* the store_get / store_reset mechanism is hierarchical + * the hierarchy is stored for us behind our back. This point + * creates a hierarchy point for this function. + */ +rs_point=store_get(0); + +/* loop until either we get to the end of the list, or we match the + * public name of this authenticator + */ +while( ( buffer = string_nextinlist(&listptr, &i, NULL, 0) ) && + strcmpic(buffer,ob->server_mech) ); + +if(!buffer) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "Cyrus SASL doesn't know about mechanism %s.", ablock->name, ob->server_mech); + +store_reset(rs_point); + +HDEBUG(D_auth) debug_printf("Cyrus SASL driver %s: %s initialised\n", ablock->name, ablock->public_name); + +/* make sure that if we get here then we're allowed to advertise. */ +ablock->server = TRUE; + +sasl_dispose(&conn); +sasl_done(); +} + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +/* note, we don't care too much about memory allocation in this, because this is entirely + * within a shortlived child + */ + +int +auth_cyrus_sasl_server(auth_instance *ablock, uschar *data) +{ +auth_cyrus_sasl_options_block *ob = + (auth_cyrus_sasl_options_block *)(ablock->options_block); +uschar *output, *out2, *input, *clear, *hname; +uschar *debug = NULL; /* Stops compiler complaining */ +sasl_callback_t cbs[]={{SASL_CB_LIST_END, NULL, NULL}}; +sasl_conn_t *conn; +int rc, firsttime=1, clen; +unsigned int inlen, outlen; + +input=data; +inlen=Ustrlen(data); + +HDEBUG(D_auth) debug=string_copy(data); + +hname=expand_string(ob->server_hostname); +if(hname == NULL) + { + auth_defer_msg = expand_string_message; + return DEFER; + } + +if(inlen) + { + clen=auth_b64decode(input, &clear); + if(clen < 0) + { + return BAD64; + } + input=clear; + inlen=clen; + } + +rc=sasl_server_init(cbs, "exim"); +if (rc != SASL_OK) + { + auth_defer_msg = US"couldn't initialise Cyrus SASL library"; + return DEFER; + } + +rc=sasl_server_new(CS ob->server_service, CS ob->server_hostname, + CS ob->server_realm, NULL, NULL, NULL, 0, &conn); +if( rc != SASL_OK ) + { + auth_defer_msg = US"couldn't initialise Cyrus SASL connection"; + sasl_done(); + return DEFER; + } + +rc=SASL_CONTINUE; + +while(rc==SASL_CONTINUE) + { + if(firsttime) + { + firsttime=0; + HDEBUG(D_auth) debug_printf("Calling sasl_server_start(%s,\"%s\")\n", ob->server_mech, debug); + rc=sasl_server_start(conn, CS ob->server_mech, inlen?CS input:NULL, inlen, + (const char **)(&output), &outlen); + } + else + { + /* make sure that we have a null-terminated string */ + out2=store_get(outlen+1); + memcpy(out2,output,outlen); + out2[outlen]='\0'; + if((rc=auth_get_data(&input, out2, outlen))!=OK) + { + /* we couldn't get the data, so free up the library before + * returning whatever error we get */ + sasl_dispose(&conn); + sasl_done(); + return rc; + } + inlen=Ustrlen(input); + + HDEBUG(D_auth) debug=string_copy(input); + if(inlen) + { + clen=auth_b64decode(input, &clear); + if(clen < 0) + { + sasl_dispose(&conn); + sasl_done(); + return BAD64; + } + input=clear; + inlen=clen; + } + + HDEBUG(D_auth) debug_printf("Calling sasl_server_step(\"%s\")\n", debug); + rc=sasl_server_step(conn, CS input, inlen, (const char **)(&output), &outlen); + } + if(rc==SASL_BADPROT) + { + sasl_dispose(&conn); + sasl_done(); + return UNEXPECTED; + } + else if( rc==SASL_FAIL || rc==SASL_BUFOVER + || rc==SASL_BADMAC || rc==SASL_BADAUTH + || rc==SASL_NOAUTHZ || rc==SASL_ENCRYPT + || rc==SASL_EXPIRED || rc==SASL_DISABLED + || rc==SASL_NOUSER ) + { + /* these are considered permanent failure codes */ + HDEBUG(D_auth) + debug_printf("Cyrus SASL permanent failure %d (%s)\n", rc, sasl_errstring(rc, NULL, NULL)); + log_write(0, LOG_REJECT, "%s authenticator (%s):\n " + "Cyrus SASL permanent failure: %s", ablock->name, ob->server_mech, + sasl_errstring(rc, NULL, NULL)); + sasl_dispose(&conn); + sasl_done(); + return FAIL; + } + else if(rc==SASL_NOMECH) + { + /* this is a temporary failure, because the mechanism is not + * available for this user. If it wasn't available at all, we + * shouldn't have got here in the first place... + */ + HDEBUG(D_auth) + debug_printf("Cyrus SASL temporary failure %d (%s)\n", rc, sasl_errstring(rc, NULL, NULL)); + auth_defer_msg = + string_sprintf("Cyrus SASL: mechanism %s not available", ob->server_mech); + sasl_dispose(&conn); + sasl_done(); + return DEFER; + } + else if(!(rc==SASL_OK || rc==SASL_CONTINUE)) + { + /* Anything else is a temporary failure, and we'll let SASL print out + * the error string for us + */ + HDEBUG(D_auth) + debug_printf("Cyrus SASL temporary failure %d (%s)\n", rc, sasl_errstring(rc, NULL, NULL)); + auth_defer_msg = + string_sprintf("Cyrus SASL: %s", sasl_errstring(rc, NULL, NULL)); + sasl_dispose(&conn); + sasl_done(); + return DEFER; + } + else if(rc==SASL_OK) + { + /* get the username and copy it into $1 */ + rc=sasl_getprop(conn, SASL_USERNAME, (const void **)(&out2)); + expand_nstring[1]=string_copy(out2); + expand_nlength[1]=Ustrlen(expand_nstring[1]); + expand_nmax=1; + + HDEBUG(D_auth) + debug_printf("Cyrus SASL %s authentiction succeeded for %s\n", ob->server_mech, out2); + /* close down the connection, freeing up library's memory */ + sasl_dispose(&conn); + sasl_done(); + return OK; + } + } +/* NOTREACHED */ +return 0; /* Stop compiler complaints */ +} + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_cyrus_sasl_client( + auth_instance *ablock, /* authenticator block */ + smtp_inblock *inblock, /* input connection */ + smtp_outblock *outblock, /* output connection */ + int timeout, /* command timeout */ + uschar *buffer, /* for reading response */ + int buffsize) /* size of buffer */ +{ +/* We don't support clients (yet) in this implementation of cyrus_sasl */ +return FAIL; +} + +#endif /* AUTH_CYRUS_SASL */ + +/* End of cyrus_sasl.c */ diff --git a/src/src/auths/cyrus_sasl.h b/src/src/auths/cyrus_sasl.h new file mode 100644 index 000000000..40792cc49 --- /dev/null +++ b/src/src/auths/cyrus_sasl.h @@ -0,0 +1,37 @@ +/* $Cambridge: exim/src/src/auths/cyrus_sasl.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2003 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Copyright (c) A L Digital Ltd 2004 */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *server_service; + uschar *server_hostname; + uschar *server_realm; + uschar *server_mech; +} auth_cyrus_sasl_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_cyrus_sasl_options[]; +extern int auth_cyrus_sasl_options_count; + +/* Block containing default values. */ + +extern auth_cyrus_sasl_options_block auth_cyrus_sasl_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_cyrus_sasl_init(auth_instance *); +extern int auth_cyrus_sasl_server(auth_instance *, uschar *); +extern int auth_cyrus_sasl_client(auth_instance *, smtp_inblock *, + smtp_outblock *, int, uschar *, int); + +/* End of cyrus_sasl.h */ diff --git a/src/src/auths/get_data.c b/src/src/auths/get_data.c new file mode 100644 index 000000000..ed65bd0c3 --- /dev/null +++ b/src/src/auths/get_data.c @@ -0,0 +1,48 @@ +/* $Cambridge: exim/src/src/auths/get_data.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Issue a challenge and get a response * +*************************************************/ + +/* This function is used by authentication drivers to output a challenge +to the SMTP client and read the response line. + +Arguments: + aptr set to point to the response (which is in big_buffer) + challenge the challenge text (unencoded, may be binary) + challen the length of the challenge text + +Returns: OK on success + BAD64 if response too large for buffer + CANCELLED if response is "*" +*/ + +int +auth_get_data(uschar **aptr, uschar *challenge, int challen) +{ +int c; +int p = 0; +smtp_printf("334 %s\r\n", auth_b64encode(challenge, challen)); +while ((c = receive_getc()) != '\n' && c != EOF) + { + if (p >= big_buffer_size - 1) return BAD64; + big_buffer[p++] = c; + } +if (p > 0 && big_buffer[p-1] == '\r') p--; +big_buffer[p] = 0; +if (Ustrcmp(big_buffer, "*") == 0) return CANCELLED; +*aptr = big_buffer; +return OK; +} + +/* End of get_data.c */ diff --git a/src/src/auths/get_no64_data.c b/src/src/auths/get_no64_data.c new file mode 100644 index 000000000..9cb2ec0ef --- /dev/null +++ b/src/src/auths/get_no64_data.c @@ -0,0 +1,48 @@ +/* $Cambridge: exim/src/src/auths/get_no64_data.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Issue a non-b64 challenge and get a response * +*************************************************/ + +/* This function is used by authentication drivers to output a challenge +to the SMTP client and read the response line. This version does not use base +64 encoding for the text on the 334 line. It is used by the SPA authenticator. + +Arguments: + aptr set to point to the response (which is in big_buffer) + challenge the challenge text (unencoded) + +Returns: OK on success + BAD64 if response too large for buffer + CANCELLED if response is "*" +*/ + +int +auth_get_no64_data(uschar **aptr, uschar *challenge) +{ +int c; +int p = 0; +smtp_printf("334 %s\r\n", challenge); +while ((c = receive_getc()) != '\n' && c != EOF) + { + if (p >= big_buffer_size - 1) return BAD64; + big_buffer[p++] = c; + } +if (p > 0 && big_buffer[p-1] == '\r') p--; +big_buffer[p] = 0; +if (Ustrcmp(big_buffer, "*") == 0) return CANCELLED; +*aptr = big_buffer; +return OK; +} + +/* End of get_no64_data.c */ diff --git a/src/src/auths/md5.c b/src/src/auths/md5.c new file mode 100644 index 000000000..0f3dad16f --- /dev/null +++ b/src/src/auths/md5.c @@ -0,0 +1,359 @@ +/* $Cambridge: exim/src/src/auths/md5.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#ifndef STAND_ALONE +#include "../exim.h" + +/* For stand-alone testing, we need to have the structure defined, and +to be able to do I/O */ + +#else +#include +#include "../mytypes.h" +typedef struct md5 { + unsigned int length; + unsigned int abcd[4]; + } +md5; +#endif + + + +/************************************************* +* Start off a new MD5 computation. * +*************************************************/ + +/* +Argument: pointer to md5 storage structure +Returns: nothing +*/ + +void +md5_start(md5 *base) +{ +base->abcd[0] = 0x67452301; +base->abcd[1] = 0xefcdab89; +base->abcd[2] = 0x98badcfe; +base->abcd[3] = 0x10325476; +base->length = 0; +} + + + +/************************************************* +* Process another 64-byte block * +*************************************************/ + +/* This function implements central part of the algorithm which is described +in RFC 1321. + +Arguments: + base pointer to md5 storage structure + text pointer to next 64 bytes of subject text + +Returns: nothing +*/ + +void +md5_mid(md5 *base, const uschar *text) +{ +register unsigned int a = base->abcd[0]; +register unsigned int b = base->abcd[1]; +register unsigned int c = base->abcd[2]; +register unsigned int d = base->abcd[3]; +int i; +unsigned int X[16]; +base->length += 64; + +/* Load the 64 bytes into a set of working integers, treating them as 32-bit +numbers in little-endian order. */ + +for (i = 0; i < 16; i++) + { + X[i] = (unsigned int)(text[0]) | + ((unsigned int)(text[1]) << 8) | + ((unsigned int)(text[2]) << 16) | + ((unsigned int)(text[3]) << 24); + text += 4; + } + +/* For each round of processing there is a function to be applied. We define it +as a macro each time round. */ + +/*********************************************** +* Round 1 * +* F(X,Y,Z) = XY v not(X) Z * +* a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s) * +***********************************************/ + +#define OP(a, b, c, d, k, s, ti) \ + a += ((b & c) | (~b & d)) + X[k] + (unsigned int)ti; \ + a = b + ((a << s) | (a >> (32 - s))) + +OP(a, b, c, d, 0, 7, 0xd76aa478); +OP(d, a, b, c, 1, 12, 0xe8c7b756); +OP(c, d, a, b, 2, 17, 0x242070db); +OP(b, c, d, a, 3, 22, 0xc1bdceee); +OP(a, b, c, d, 4, 7, 0xf57c0faf); +OP(d, a, b, c, 5, 12, 0x4787c62a); +OP(c, d, a, b, 6, 17, 0xa8304613); +OP(b, c, d, a, 7, 22, 0xfd469501); +OP(a, b, c, d, 8, 7, 0x698098d8); +OP(d, a, b, c, 9, 12, 0x8b44f7af); +OP(c, d, a, b, 10, 17, 0xffff5bb1); +OP(b, c, d, a, 11, 22, 0x895cd7be); +OP(a, b, c, d, 12, 7, 0x6b901122); +OP(d, a, b, c, 13, 12, 0xfd987193); +OP(c, d, a, b, 14, 17, 0xa679438e); +OP(b, c, d, a, 15, 22, 0x49b40821); + +#undef OP + +/*********************************************** +* Round 2 * +* F(X,Y,Z) = XZ v Y not(Z) * +* a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s) * +***********************************************/ + +#define OP(a, b, c, d, k, s, ti) \ + a += ((b & d) | (c & ~d)) + X[k] + (unsigned int)ti; \ + a = b + ((a << s) | (a >> (32 - s))) + +OP(a, b, c, d, 1, 5, 0xf61e2562); +OP(d, a, b, c, 6, 9, 0xc040b340); +OP(c, d, a, b, 11, 14, 0x265e5a51); +OP(b, c, d, a, 0, 20, 0xe9b6c7aa); +OP(a, b, c, d, 5, 5, 0xd62f105d); +OP(d, a, b, c, 10, 9, 0x02441453); +OP(c, d, a, b, 15, 14, 0xd8a1e681); +OP(b, c, d, a, 4, 20, 0xe7d3fbc8); +OP(a, b, c, d, 9, 5, 0x21e1cde6); +OP(d, a, b, c, 14, 9, 0xc33707d6); +OP(c, d, a, b, 3, 14, 0xf4d50d87); +OP(b, c, d, a, 8, 20, 0x455a14ed); +OP(a, b, c, d, 13, 5, 0xa9e3e905); +OP(d, a, b, c, 2, 9, 0xfcefa3f8); +OP(c, d, a, b, 7, 14, 0x676f02d9); +OP(b, c, d, a, 12, 20, 0x8d2a4c8a); + +#undef OP + +/*********************************************** +* Round 3 * +* F(X,Y,Z) = X xor Y xor Z * +* a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s) * +***********************************************/ + +#define OP(a, b, c, d, k, s, ti) \ + a += (b ^ c ^ d) + X[k] + (unsigned int)ti; \ + a = b + ((a << s) | (a >> (32 - s))) + +OP(a, b, c, d, 5, 4, 0xfffa3942); +OP(d, a, b, c, 8, 11, 0x8771f681); +OP(c, d, a, b, 11, 16, 0x6d9d6122); +OP(b, c, d, a, 14, 23, 0xfde5380c); +OP(a, b, c, d, 1, 4, 0xa4beea44); +OP(d, a, b, c, 4, 11, 0x4bdecfa9); +OP(c, d, a, b, 7, 16, 0xf6bb4b60); +OP(b, c, d, a, 10, 23, 0xbebfbc70); +OP(a, b, c, d, 13, 4, 0x289b7ec6); +OP(d, a, b, c, 0, 11, 0xeaa127fa); +OP(c, d, a, b, 3, 16, 0xd4ef3085); +OP(b, c, d, a, 6, 23, 0x04881d05); +OP(a, b, c, d, 9, 4, 0xd9d4d039); +OP(d, a, b, c, 12, 11, 0xe6db99e5); +OP(c, d, a, b, 15, 16, 0x1fa27cf8); +OP(b, c, d, a, 2, 23, 0xc4ac5665); + +#undef OP + +/*********************************************** +* Round 4 * +* F(X,Y,Z) = Y xor (X v not(Z)) * +* a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s) * +***********************************************/ + +#define OP(a, b, c, d, k, s, ti) \ + a += (c ^ (b | ~d)) + X[k] + (unsigned int)ti; \ + a = b + ((a << s) | (a >> (32 - s))) + +OP(a, b, c, d, 0, 6, 0xf4292244); +OP(d, a, b, c, 7, 10, 0x432aff97); +OP(c, d, a, b, 14, 15, 0xab9423a7); +OP(b, c, d, a, 5, 21, 0xfc93a039); +OP(a, b, c, d, 12, 6, 0x655b59c3); +OP(d, a, b, c, 3, 10, 0x8f0ccc92); +OP(c, d, a, b, 10, 15, 0xffeff47d); +OP(b, c, d, a, 1, 21, 0x85845dd1); +OP(a, b, c, d, 8, 6, 0x6fa87e4f); +OP(d, a, b, c, 15, 10, 0xfe2ce6e0); +OP(c, d, a, b, 6, 15, 0xa3014314); +OP(b, c, d, a, 13, 21, 0x4e0811a1); +OP(a, b, c, d, 4, 6, 0xf7537e82); +OP(d, a, b, c, 11, 10, 0xbd3af235); +OP(c, d, a, b, 2, 15, 0x2ad7d2bb); +OP(b, c, d, a, 9, 21, 0xeb86d391); + +#undef OP + +/* Add the new values back into the accumulators. */ + +base->abcd[0] += a; +base->abcd[1] += b; +base->abcd[2] += c; +base->abcd[3] += d; +} + + + + +/************************************************* +* Process the final text string * +*************************************************/ + +/* The string may be of any length. It is padded out according to the rules +for computing MD5 digests. The final result is then converted to text form +and returned. + +Arguments: + base pointer to the md5 storage structure + text pointer to the final text vector + length length of the final text vector + digest points to 16 bytes in which to place the result + +Returns: nothing +*/ + +void +md5_end(md5 *base, const uschar *text, int length, uschar *digest) +{ +int i; +uschar work[64]; + +/* Process in chunks of 64 until we have less than 64 bytes left. */ + +while (length >= 64) + { + md5_mid(base, text); + text += 64; + length -= 64; + } + +/* If the remaining string contains more than 55 bytes, we must pad it +out to 64, process it, and then set up the final chunk as 56 bytes of +padding. If it has less than 56 bytes, we pad it out to 56 bytes as the +final chunk. */ + +memcpy(work, text, length); +work[length] = 0x80; + +if (length > 55) + { + memset(work+length+1, 0, 63-length); + md5_mid(base, work); + base->length -= 64; + memset(work, 0, 56); + } +else + { + memset(work+length+1, 0, 55-length); + } + +/* The final 8 bytes of the final chunk are a 64-bit representation of the +length of the input string *bits*, before padding, low order word first, and +low order bytes first in each word. This implementation is designed for short +strings, and so operates with a single int counter only. */ + +length += base->length; /* Total length in bytes */ +length <<= 3; /* Total length in bits */ + +work[56] = length & 0xff; +work[57] = (length >> 8) & 0xff; +work[58] = (length >> 16) & 0xff; +work[59] = (length >> 24) & 0xff; + +memset(work+60, 0, 4); + +/* Process the final 64-byte chunk */ + +md5_mid(base, work); + +/* Pass back the result, low-order byte first in each word. */ + +for (i = 0; i < 4; i++) + { + register int x = base->abcd[i]; + *digest++ = x & 0xff; + *digest++ = (x >> 8) & 0xff; + *digest++ = (x >> 16) & 0xff; + *digest++ = (x >> 24) & 0xff; + } +} + + + +/************************************************* +************************************************** +* Stand-alone test program * +************************************************** +*************************************************/ + +#if defined STAND_ALONE & !defined CRAM_STAND_ALONE + +/* Test values */ + +static uschar *tests[] = { + "", "d41d8cd98f00b204e9800998ecf8427e", + + "a", "0cc175b9c0f1b6a831c399e269772661", + + "abc", "900150983cd24fb0d6963f7d28e17f72", + + "message digest", "f96b697d7cb7938d525a2f31aaf161d0", + + "abcdefghijklmnopqrstuvwxyz", "c3fcd3d76192e4007dfb496cca67e13b", + + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "d174ab98d277d9f5a5611c2c9f419d9f", + + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890", + "57edf4a22be3c955ac49da2e2107b67a", + + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "a0842fcc02167127b0bb9a7c38e71ba8" +}; + +int main(void) +{ +md5 base; +int i = 0x01020304; +uschar *ctest = (uschar *)(&i); +uschar buffer[256]; +uschar digest[16]; +printf("Checking md5: %s-endian\n", (ctest[0] == 0x04)? "little" : "big"); + +for (i = 0; i < sizeof(tests)/sizeof(uschar *); i += 2) + { + int j; + uschar s[33]; + printf("%s\nShould be: %s\n", tests[i], tests[i+1]); + md5_start(&base); + md5_end(&base, tests[i], strlen(tests[i]), digest); + for (j = 0; j < 16; j++) sprintf(s+2*j, "%02x", digest[j]); + printf("Computed: %s\n", s); + if (strcmp(s, tests[i+1]) != 0) printf("*** No match ***\n"); + printf("\n"); + } +} +#endif + +/* End of md5.c */ diff --git a/src/src/auths/plaintext.c b/src/src/auths/plaintext.c new file mode 100644 index 000000000..b15a08d51 --- /dev/null +++ b/src/src/auths/plaintext.c @@ -0,0 +1,305 @@ +/* $Cambridge: exim/src/src/auths/plaintext.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "plaintext.h" + + +/* Options specific to the plaintext authentication mechanism. */ + +optionlist auth_plaintext_options[] = { + { "client_send", opt_stringptr, + (void *)(offsetof(auth_plaintext_options_block, client_send)) }, + { "server_condition", opt_stringptr, + (void *)(offsetof(auth_plaintext_options_block, server_condition)) }, + { "server_prompts", opt_stringptr, + (void *)(offsetof(auth_plaintext_options_block, server_prompts)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_plaintext_options_count = + sizeof(auth_plaintext_options)/sizeof(optionlist); + +/* Default private options block for the plaintext authentication method. */ + +auth_plaintext_options_block auth_plaintext_option_defaults = { + NULL, /* server_condition */ + NULL, /* server_prompts */ + NULL /* client_send */ +}; + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_plaintext_init(auth_instance *ablock) +{ +auth_plaintext_options_block *ob = + (auth_plaintext_options_block *)(ablock->options_block); +if (ablock->public_name == NULL) ablock->public_name = ablock->name; +if (ob->server_condition != NULL) ablock->server = TRUE; +if (ob->client_send != NULL) ablock->client = TRUE; +} + + + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_plaintext_server(auth_instance *ablock, uschar *data) +{ +auth_plaintext_options_block *ob = + (auth_plaintext_options_block *)(ablock->options_block); +uschar *prompts = ob->server_prompts; +uschar *clear, *cond, *end, *s; +int number = 1; +int len, rc; +int sep = 0; + +/* Expand a non-empty list of prompt strings */ + +if (prompts != NULL) + { + prompts = expand_string(prompts); + if (prompts == NULL) + { + auth_defer_msg = expand_string_message; + return DEFER; + } + } + +/* If data was supplied on the AUTH command, decode it, and split it up into +multiple items at binary zeros. If the data consists of the string "=" it +indicates a single, empty string. */ + +if (*data != 0) + { + if (Ustrcmp(data, "=") == 0) + { + expand_nstring[++expand_nmax] = US""; + expand_nlength[expand_nmax] = 0; + } + else + { + if ((len = auth_b64decode(data, &clear)) < 0) return BAD64; + end = clear + len; + while (clear < end && expand_nmax < EXPAND_MAXN) + { + expand_nstring[++expand_nmax] = clear; + while (*clear != 0) clear++; + expand_nlength[expand_nmax] = clear++ - expand_nstring[expand_nmax]; + } + } + } + +/* Now go through the list of prompt strings. Skip over any whose data has +already been provided as part of the AUTH command. For the rest, send them +out as prompts, and get a data item back. If the data item is "*", abandon the +authentication attempt. Otherwise, split it into items as above. */ + +while ((s = string_nextinlist(&prompts, &sep, big_buffer, big_buffer_size)) + != NULL && expand_nmax < EXPAND_MAXN) + { + if (number++ <= expand_nmax) continue; + if ((rc = auth_get_data(&data, s, Ustrlen(s))) != OK) return rc; + if ((len = auth_b64decode(data, &clear)) < 0) return BAD64; + end = clear + len; + + /* This loop must run at least once, in case the length is zero */ + do + { + expand_nstring[++expand_nmax] = clear; + while (*clear != 0) clear++; + expand_nlength[expand_nmax] = clear++ - expand_nstring[expand_nmax]; + } + while (clear < end && expand_nmax < EXPAND_MAXN); + } + +/* We now have a number of items of data in $1, $2, etc. Match against the +decoded data by expanding the condition. Also expand the id to set if +authentication succeeds. */ + +cond = expand_string(ob->server_condition); + +HDEBUG(D_auth) + { + int i; + debug_printf("%s authenticator:\n", ablock->name); + for (i = 1; i <= expand_nmax; i++) + debug_printf(" $%d = %.*s\n", i, expand_nlength[i], expand_nstring[i]); + debug_print_string(ablock->server_debug_string); /* customized debug */ + if (cond == NULL) + debug_printf("expansion failed: %s\n", expand_string_message); + else + debug_printf("expanded string: %s\n", cond); + } + +/* A forced expansion failure causes authentication to fail. Other expansion +failures yield DEFER, which will cause a temporary error code to be returned to +the AUTH command. The problem is at the server end, so the client should try +again later. */ + +if (cond == NULL) + { + if (expand_string_forcedfail) return FAIL; + auth_defer_msg = expand_string_message; + return DEFER; + } + +/* Return FAIL for empty string, "0", "no", and "false"; return OK for +"1", "yes", and "true"; return DEFER for anything else, with the string +available as an error text for the user. */ + +if (*cond == 0 || + Ustrcmp(cond, "0") == 0 || + strcmpic(cond, US"no") == 0 || + strcmpic(cond, US"false") == 0) + return FAIL; + +if (Ustrcmp(cond, "1") == 0 || + strcmpic(cond, US"yes") == 0 || + strcmpic(cond, US"true") == 0) + return OK; + +auth_defer_msg = cond; +auth_defer_user_msg = string_sprintf(": %s", cond); +return DEFER; +} + + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_plaintext_client( + auth_instance *ablock, /* authenticator block */ + smtp_inblock *inblock, /* connection inblock */ + smtp_outblock *outblock, /* connection outblock */ + int timeout, /* command timeout */ + uschar *buffer, /* buffer for reading response */ + int buffsize) /* size of buffer */ +{ +auth_plaintext_options_block *ob = + (auth_plaintext_options_block *)(ablock->options_block); +uschar *text = ob->client_send; +uschar *s; +BOOL first = TRUE; +int sep = 0; + +/* The text is broken up into a number of different data items, which are +sent one by one. The first one is sent with the AUTH command; the remainder are +sent in response to subsequent prompts. Each is expanded before being sent. */ + +while ((s = string_nextinlist(&text, &sep, big_buffer, big_buffer_size)) != NULL) + { + int i, len; + uschar *ss = expand_string(s); + + /* Forced expansion failure is not an error; authentication is abandoned. On + all but the first string, we have to abandon the authentication attempt by + sending a line containing "*". Save the failed expansion string, because it + is in big_buffer, and that gets used by the sending function. */ + + if (ss == NULL) + { + uschar *ssave = string_copy(s); + if (!first) + { + if (smtp_write_command(outblock, FALSE, "*\r\n") >= 0) + (void) smtp_read_response(inblock, US buffer, buffsize, '2', timeout); + } + if (expand_string_forcedfail) return CANCELLED; + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", ssave, ablock->name, expand_string_message); + return ERROR; + } + + len = Ustrlen(ss); + + /* The character ^ is used as an escape for a binary zero character, which is + needed for the PLAIN mechanism. It must be doubled if really needed. */ + + for (i = 0; i < len; i++) + { + if (ss[i] == '^') + { + if (ss[i+1] != '^') ss[i] = 0; else + { + i++; + len--; + memmove(ss + i, ss + i + 1, len - i); + } + } + } + + /* The first string is attached to the AUTH command; others are sent + unembelished. */ + + if (first) + { + first = FALSE; + if (smtp_write_command(outblock, FALSE, "AUTH %s%s%s\r\n", + ablock->public_name, (len == 0)? "" : " ", + auth_b64encode(ss, len)) < 0) + return FAIL_SEND; + } + else + { + if (smtp_write_command(outblock, FALSE, "%s\r\n", + auth_b64encode(ss, len)) < 0) + return FAIL_SEND; + } + + /* If we receive a success response from the server, authentication + has succeeded. There may be more data to send, but is there any point + in provoking an error here? */ + + if (smtp_read_response(inblock, US buffer, buffsize, '2', timeout)) return OK; + + /* Not a success response. If errno != 0 there is some kind of transmission + error. Otherwise, check the response code in the buffer. If it starts with + '3', more data is expected. */ + + if (errno != 0 || buffer[0] != '3') return FAIL; + + /* If there is no more data to send, we have to cancel the authentication + exchange and return ERROR. */ + + if (text == NULL) + { + if (smtp_write_command(outblock, FALSE, "*\r\n") >= 0) + (void)smtp_read_response(inblock, US buffer, buffsize, '2', timeout); + string_format(buffer, buffsize, "Too few items in client_send in %s " + "authenticator", ablock->name); + return ERROR; + } + } + +/* Control should never actually get here. */ + +return FAIL; +} + +/* End of plaintext.c */ diff --git a/src/src/auths/plaintext.h b/src/src/auths/plaintext.h new file mode 100644 index 000000000..ca3b09b3a --- /dev/null +++ b/src/src/auths/plaintext.h @@ -0,0 +1,34 @@ +/* $Cambridge: exim/src/src/auths/plaintext.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *server_condition; + uschar *server_prompts; + uschar *client_send; +} auth_plaintext_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_plaintext_options[]; +extern int auth_plaintext_options_count; + +/* Block containing default values. */ + +extern auth_plaintext_options_block auth_plaintext_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_plaintext_init(auth_instance *); +extern int auth_plaintext_server(auth_instance *, uschar *); +extern int auth_plaintext_client(auth_instance *, smtp_inblock *, + smtp_outblock *, int, uschar *, int); + +/* End of plaintext.h */ diff --git a/src/src/auths/pwcheck.c b/src/src/auths/pwcheck.c new file mode 100644 index 000000000..f477b6f06 --- /dev/null +++ b/src/src/auths/pwcheck.c @@ -0,0 +1,455 @@ +/* $Cambridge: exim/src/src/auths/pwcheck.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/* SASL server API implementation + * Rob Siemborski + * Tim Martin + * $Id: checkpw.c,v 1.49 2002/03/07 19:14:04 ken3 Exp $ + */ +/* + * Copyright (c) 2001 Carnegie Mellon University. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The name "Carnegie Mellon University" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For permission or any other legal + * details, please contact + * Office of Technology Transfer + * Carnegie Mellon University + * 5000 Forbes Avenue + * Pittsburgh, PA 15213-3890 + * (412) 268-4387, fax: (412) 268-7395 + * tech-transfer@andrew.cmu.edu + * + * 4. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by Computing Services + * at Carnegie Mellon University (http://www.cmu.edu/computing/)." + * + * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO + * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE + * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Taken from Cyrus-SASL library and adapted by Alexander S. Sabourenkov + * Oct 2001 - Apr 2002: Slightly modified by Philip Hazel. + * Aug 2003: new code for saslauthd from Alexander S. Sabourenkov incorporated + * by Philip Hazel (minor mods to avoid compiler warnings) + * + * screwdriver@lxnt.info + * + */ + +/* Originally this module supported only the pwcheck daemon, which is where its +name comes from. Nowadays it supports saslauthd as well; pwcheck is in fact +deprecated. The definitions of CYRUS_PWCHECK_SOCKET and CYRUS_SASLAUTHD_SOCKET +determine whether the facilities are actually supported or not. */ + + +#include "../exim.h" +#include "pwcheck.h" + + +#if defined(CYRUS_PWCHECK_SOCKET) || defined(CYRUS_SASLAUTHD_SOCKET) + +#include + +static int retry_read(int, void *, unsigned ); +static int retry_writev(int, struct iovec *, int ); +static int read_string(int, uschar **); +static int write_string(int, const uschar *, int); + +#endif + + +/* A dummy function that always fails if pwcheck support is not +wanted. */ + +#ifndef CYRUS_PWCHECK_SOCKET +int pwcheck_verify_password(const char *userid, + const char *passwd, + const char **reply) +{ +userid = userid; /* Keep picky compilers happy */ +passwd = passwd; +*reply = "pwcheck support is not included in this Exim binary"; +return PWCHECK_FAIL; +} + + +/* This is the real function */ + +#else + + /* taken from cyrus-sasl file checkpw.c */ + /* pwcheck daemon-authenticated login */ + int pwcheck_verify_password(const char *userid, + const char *passwd, + const char **reply) + { + int s, start, r, n; + struct sockaddr_un srvaddr; + struct iovec iov[2]; + static char response[1024]; + + if (reply) { *reply = NULL; } + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s == -1) { return PWCHECK_FAIL; } + + memset((char *)&srvaddr, 0, sizeof(srvaddr)); + srvaddr.sun_family = AF_UNIX; + strncpy(srvaddr.sun_path, CYRUS_PWCHECK_SOCKET, sizeof(srvaddr.sun_path)); + r = connect(s, (struct sockaddr *)&srvaddr, sizeof(srvaddr)); + if (r == -1) { + DEBUG(D_auth) + debug_printf("Cannot connect to pwcheck daemon (at '%s')\n",CYRUS_PWCHECK_SOCKET); + if (reply) { *reply = "cannot connect to pwcheck daemon"; } + return PWCHECK_FAIL; + } + + iov[0].iov_base = (char *)userid; + iov[0].iov_len = strlen(userid)+1; + iov[1].iov_base = (char *)passwd; + iov[1].iov_len = strlen(passwd)+1; + + retry_writev(s, iov, 2); + + start = 0; + while (start < sizeof(response) - 1) { + n = read(s, response+start, sizeof(response) - 1 - start); + if (n < 1) break; + start += n; + } + + close(s); + + if (start > 1 && !strncmp(response, "OK", 2)) { + return PWCHECK_OK; + } + + response[start] = '\0'; + if (reply) { *reply = response; } + return PWCHECK_NO; + } + +#endif + + + + /* A dummy function that always fails if saslauthd support is not +wanted. */ + +#ifndef CYRUS_SASLAUTHD_SOCKET +int saslauthd_verify_password(const uschar *userid, + const uschar *passwd, + const uschar *service, + const uschar *realm, + const uschar **reply) +{ +userid = userid; /* Keep picky compilers happy */ +passwd = passwd; +service = service; +realm = realm; +*reply = US"saslauthd support is not included in this Exim binary"; +return PWCHECK_FAIL; +} + + +/* This is the real function */ + +#else + /* written from scratch */ + /* saslauthd daemon-authenticated login */ + +int saslauthd_verify_password(const uschar *userid, + const uschar *password, + const uschar *service, + const uschar *realm, + const uschar **reply) +{ + uschar *daemon_reply; + int s, r; + struct sockaddr_un srvaddr; + + DEBUG(D_auth) + debug_printf("saslauthd userid='%s' servicename='%s'" + " realm='%s'\n", userid, service, realm ); + + if (reply) + *reply = NULL; + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s == -1) { + if (reply) + *reply = CUstrerror(errno); + return PWCHECK_FAIL; + } + + memset((char *)&srvaddr, 0, sizeof(srvaddr)); + srvaddr.sun_family = AF_UNIX; + strncpy(srvaddr.sun_path, CYRUS_SASLAUTHD_SOCKET, + sizeof(srvaddr.sun_path)); + r = connect(s, (struct sockaddr *)&srvaddr, sizeof(srvaddr)); + if (r == -1) { + DEBUG(D_auth) + debug_printf("Cannot connect to saslauthd daemon (at '%s'): %s\n", + CYRUS_SASLAUTHD_SOCKET, strerror(errno)); + if (reply) + *reply = string_sprintf("cannot connect to saslauthd daemon at " + "%s: %s", CYRUS_SASLAUTHD_SOCKET, + strerror(errno)); + return PWCHECK_FAIL; + } + + if ( write_string(s, userid, Ustrlen(userid)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send userid to saslauthd daemon \n"); + close(s); + return PWCHECK_FAIL; + } + + if ( write_string(s, password, Ustrlen(password)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send password to saslauthd daemon \n"); + close(s); + return PWCHECK_FAIL; + } + + memset((void *)password, 0, Ustrlen(password)); + + if ( write_string(s, service, Ustrlen(service)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send service name to saslauthd daemon \n"); + close(s); + return PWCHECK_FAIL; + } + + if ( write_string(s, realm, Ustrlen(realm)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send realm to saslauthd daemon \n"); + close(s); + return PWCHECK_FAIL; + } + + if ( read_string(s, &daemon_reply ) < 2) { + DEBUG(D_auth) + debug_printf("Corrupted answer '%s' received. \n", daemon_reply); + close(s); + return PWCHECK_FAIL; + } + + close(s); + + DEBUG(D_auth) + debug_printf("Answer '%s' received. \n", daemon_reply); + + *reply = daemon_reply; + + if ( (daemon_reply[0] == 'O') && (daemon_reply[1] == 'K') ) + return PWCHECK_OK; + + if ( (daemon_reply[0] == 'N') && (daemon_reply[1] == 'O') ) + return PWCHECK_NO; + + return PWCHECK_FAIL; +} + +#endif + + +/* helper functions */ +#if defined(CYRUS_PWCHECK_SOCKET) || defined(CYRUS_SASLAUTHD_SOCKET) + +#define MAX_REQ_LEN 1024 + +/* written from scratch */ + +/* FUNCTION: read_string */ + +/* SYNOPSIS + * read a sasld-style counted string into + * store-allocated buffer, set pointer to the buffer, + * return number of bytes read or -1 on failure. + * END SYNOPSIS */ + +static int read_string(int fd, uschar **retval) { + unsigned short count; + int rc; + + rc = (retry_read(fd, &count, sizeof(count)) < (int) sizeof(count)); + if (!rc) { + count = ntohs(count); + if (count > MAX_REQ_LEN) { + return -1; + } else { + *retval = store_get(count + 1); + rc = (retry_read(fd, *retval, count) < (int) count); + (*retval)[count] = '\0'; + return count; + } + } + return -1; +} + + +/* FUNCTION: write_string */ + +/* SYNOPSIS + * write a sasld-style counted string into given fd + * written bytes on success, -1 on failure. + * END SYNOPSIS */ + +static int write_string(int fd, const uschar *string, int len) { + unsigned short count; + int rc; + struct iovec iov[2]; + + count = htons(len); + + iov[0].iov_base = (void *) &count; + iov[0].iov_len = sizeof(count); + iov[1].iov_base = (void *) string; + iov[1].iov_len = len; + + rc = retry_writev(fd, iov, 2); + + return rc; +} + + +/* taken from cyrus-sasl file saslauthd/saslauthd-unix.c */ + +/* FUNCTION: retry_read */ + +/* SYNOPSIS + * Keep calling the read() system call with 'fd', 'buf', and 'nbyte' + * until all the data is read in or an error occurs. + * END SYNOPSIS */ +static int retry_read(int fd, void *inbuf, unsigned nbyte) +{ + int n; + int nread = 0; + char *buf = (char *)inbuf; + + if (nbyte == 0) return 0; + + for (;;) { + n = read(fd, buf, nbyte); + if (n == 0) { + /* end of file */ + return -1; + } + if (n == -1) { + if (errno == EINTR) continue; + return -1; + } + + nread += n; + + if (n >= (int) nbyte) return nread; + + buf += n; + nbyte -= n; + } +} + +/* END FUNCTION: retry_read */ + +/* FUNCTION: retry_writev */ + +/* SYNOPSIS + * Keep calling the writev() system call with 'fd', 'iov', and 'iovcnt' + * until all the data is written out or an error occurs. + * END SYNOPSIS */ + +static int /* R: bytes written, or -1 on error */ +retry_writev ( + /* PARAMETERS */ + int fd, /* I: fd to write on */ + struct iovec *iov, /* U: iovec array base + * modified as data written */ + int iovcnt /* I: number of iovec entries */ + /* END PARAMETERS */ + ) +{ + /* VARIABLES */ + int n; /* return value from writev() */ + int i; /* loop counter */ + int written; /* bytes written so far */ + static int iov_max; /* max number of iovec entries */ + /* END VARIABLES */ + + /* initialization */ +#ifdef MAXIOV + iov_max = MAXIOV; +#else /* ! MAXIOV */ +# ifdef IOV_MAX + iov_max = IOV_MAX; +# else /* ! IOV_MAX */ + iov_max = 8192; +# endif /* ! IOV_MAX */ +#endif /* ! MAXIOV */ + written = 0; + + for (;;) { + + while (iovcnt && iov[0].iov_len == 0) { + iov++; + iovcnt--; + } + + if (!iovcnt) { + return written; + } + + n = writev(fd, iov, iovcnt > iov_max ? iov_max : iovcnt); + if (n == -1) { + if (errno == EINVAL && iov_max > 10) { + iov_max /= 2; + continue; + } + if (errno == EINTR) { + continue; + } + return -1; + } else { + written += n; + } + + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > (unsigned) n) { + iov[i].iov_base = (char *)iov[i].iov_base + n; + iov[i].iov_len -= n; + break; + } + n -= iov[i].iov_len; + iov[i].iov_len = 0; + } + + if (i == iovcnt) { + return written; + } + } + /* NOTREACHED */ +} + +/* END FUNCTION: retry_writev */ +#endif + +/* End of auths/pwcheck.c */ diff --git a/src/src/auths/pwcheck.h b/src/src/auths/pwcheck.h new file mode 100644 index 000000000..7e2c01782 --- /dev/null +++ b/src/src/auths/pwcheck.h @@ -0,0 +1,29 @@ +/* $Cambridge: exim/src/src/auths/pwcheck.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file provides support for authentication via the Cyrus SASL pwcheck +daemon (whence its name) and the newer saslauthd daemon. */ + +/* Error codes used internally within the authentication functions */ + +/* PWCHECK_OK - auth successful + PWCHECK_NO - access denied + PWCHECK_FAIL - [temporary] failure */ + +#define PWCHECK_OK 0 +#define PWCHECK_NO 1 +#define PWCHECK_FAIL 2 + +/* Cyrus functions for doing the business. */ + +extern int pwcheck_verify_password(const char *, const char *, const char **); +extern int saslauthd_verify_password(const uschar *, const uschar *, + const uschar *, const uschar *, const uschar **); + +/* End of pwcheck.h */ diff --git a/src/src/auths/sha1.c b/src/src/auths/sha1.c new file mode 100644 index 000000000..86b1da9c6 --- /dev/null +++ b/src/src/auths/sha1.c @@ -0,0 +1,554 @@ +/* $Cambridge: exim/src/src/auths/sha1.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#ifndef STAND_ALONE +#include "../exim.h" + +/* For stand-alone testing, we need to have the structure defined, and +to be able to do I/O */ + +#else +#include +#include +typedef unsigned char uschar; +typedef struct sha1 { + unsigned int H[5]; + unsigned int length; + } +sha1; +#endif + + + +/************************************************* +* Start off a new SHA-1 computation. * +*************************************************/ + +/* +Argument: pointer to sha1 storage structure +Returns: nothing +*/ + +void +sha1_start(sha1 *base) +{ +base->H[0] = 0x67452301; +base->H[1] = 0xefcdab89; +base->H[2] = 0x98badcfe; +base->H[3] = 0x10325476; +base->H[4] = 0xc3d2e1f0; +base->length = 0; +} + + + +/************************************************* +* Process another 64-byte block * +*************************************************/ + +/* This function implements central part of the algorithm + +Arguments: + base pointer to sha1 storage structure + text pointer to next 64 bytes of subject text + +Returns: nothing +*/ + +void +sha1_mid(sha1 *base, const uschar *text) +{ +register int i; +unsigned int A, B, C, D, E; +unsigned int W[80]; + +base->length += 64; + +for (i = 0; i < 16; i++) + { + W[i] = (text[0] << 24) | (text[1] << 16) | (text[2] << 8) | text[3]; + text += 4; + } + +for (i = 16; i < 80; i++) + { + register unsigned int x = W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16]; + W[i] = (x << 1) | (x >> 31); + } + +A = base->H[0]; +B = base->H[1]; +C = base->H[2]; +D = base->H[3]; +E = base->H[4]; + +for (i = 0; i < 20; i++) + { + unsigned int T; + T = ((A << 5) | (A >> 27)) + ((B & C) | ((~B) & D)) + E + W[i] + 0x5a827999; + E = D; + D = C; + C = (B << 30) | (B >> 2); + B = A; + A = T; + } + +for (i = 20; i < 40; i++) + { + unsigned int T; + T = ((A << 5) | (A >> 27)) + (B ^ C ^ D) + E + W[i] + 0x6ed9eba1; + E = D; + D = C; + C = (B << 30) | (B >> 2); + B = A; + A = T; + } + +for (i = 40; i < 60; i++) + { + unsigned int T; + T = ((A << 5) | (A >> 27)) + ((B & C) | (B & D) | (C & D)) + E + W[i] + + 0x8f1bbcdc; + E = D; + D = C; + C = (B << 30) | (B >> 2); + B = A; + A = T; + } + +for (i = 60; i < 80; i++) + { + unsigned int T; + T = ((A << 5) | (A >> 27)) + (B ^ C ^ D) + E + W[i] + 0xca62c1d6; + E = D; + D = C; + C = (B << 30) | (B >> 2); + B = A; + A = T; + } + +base->H[0] += A; +base->H[1] += B; +base->H[2] += C; +base->H[3] += D; +base->H[4] += E; +} + + + + +/************************************************* +* Process the final text string * +*************************************************/ + +/* The string may be of any length. It is padded out according to the rules +for computing SHA-1 digests. The final result is then converted to text form +and returned. + +Arguments: + base pointer to the sha1 storage structure + text pointer to the final text vector + length length of the final text vector + digest points to 16 bytes in which to place the result + +Returns: nothing +*/ + +void +sha1_end(sha1 *base, const uschar *text, int length, uschar *digest) +{ +int i; +uschar work[64]; + +/* Process in chunks of 64 until we have less than 64 bytes left. */ + +while (length >= 64) + { + sha1_mid(base, text); + text += 64; + length -= 64; + } + +/* If the remaining string contains more than 55 bytes, we must pad it +out to 64, process it, and then set up the final chunk as 56 bytes of +padding. If it has less than 56 bytes, we pad it out to 56 bytes as the +final chunk. */ + +memcpy(work, text, length); +work[length] = 0x80; + +if (length > 55) + { + memset(work+length+1, 0, 63-length); + sha1_mid(base, work); + base->length -= 64; + memset(work, 0, 56); + } +else + { + memset(work+length+1, 0, 55-length); + } + +/* The final 8 bytes of the final chunk are a 64-bit representation of the +length of the input string *bits*, before padding, high order word first, and +high order bytes first in each word. This implementation is designed for short +strings, and so operates with a single int counter only. */ + +length += base->length; /* Total length in bytes */ +length <<= 3; /* Total length in bits */ + +work[63] = length & 0xff; +work[62] = (length >> 8) & 0xff; +work[61] = (length >> 16) & 0xff; +work[60] = (length >> 24) & 0xff; + +memset(work+56, 0, 4); + +/* Process the final 64-byte chunk */ + +sha1_mid(base, work); + +/* Pass back the result, high-order byte first in each word. */ + +for (i = 0; i < 5; i++) + { + register int x = base->H[i]; + *digest++ = (x >> 24) & 0xff; + *digest++ = (x >> 16) & 0xff; + *digest++ = (x >> 8) & 0xff; + *digest++ = x & 0xff; + } +} + + + +/************************************************* +************************************************** +* Stand-alone test program * +************************************************** +*************************************************/ + +#ifdef STAND_ALONE + +/* Test values. The first 128 may contain binary zeros and have increasing +length. */ + +static uschar *tests[] = { + "", + "\x24", + "\x70\xf0", + "\x0e\x1e\xf0", + "\x08\x38\x78\x8f", + "\x10\x3e\x08\xfc\x0f", + "\xe7\xc7\x1e\x07\xef\x03", + "\xe0\xfb\x71\xf8\xf9\xc1\xfc", + "\xff\x7c\x60\x3c\x1f\x80\xe2\x07", + "\xf0\x3f\xc8\x60\x81\xfe\x01\xf8\x7f", + "\x9f\xc7\xf8\x1f\xc1\xe3\xc7\xc7\x3f\x00", + "\x00\x7f\xbf\xdf\xc0\xfe\x02\x7e\x00\xf8\x7f", + "\x01\x01\xc0\x1e\x03\xf8\x30\x08\x0f\xf3\xf9\xff", + "\xc4\x03\xfc\x0f\xf8\x01\xc0\x0f\xf0\x06\x10\x41\xff", + "\xff\x07\x80\x47\xfc\x1f\xc0\x60\x30\x1f\xe0\x3c\x03\xff", + "\x80\x3f\x84\x3e\xff\xc0\x3f\x0f\x00\x7f\xc0\x1f\xe7\xfc\x00", + "\xff\xe0\x7f\x01\x81\x81\xff\x81\xff\x00\x3e\x00\x20\x7f\x80\x0f", + "\x00\x3e\x00\x70\x1f\xe0\x0f\xfa\xff\xc8\x3f\xf3\xfe\x00\xff\x80\xff", + "\x7f\xef\xc0\x1e\x7c\xff\xe0\x1f\xfe\x00\x1f\xf0\x08\xff\xc0\x7f\xf0\x00", + "\xe0\x0f\x80\x07\x0c\x01\xff\xe0\x03\xf0\x2f\xf8\x3f\xef\x00\x78\x01\xfe\x00", + "\xe7\x00\x10\x00\xf8\x18\x0f\xf0\xff\x00\xff\x80\x3f\xc3\xfe\xf0\x0f\xfc\x01\xff", + "\x00\x1f\xf8\x0f\xfc\x00\xfc\x00\xff\x87\xc0\x0f\x80\x7b\xff\x00\x0f\x02\x01\xff\xc0", + "\x00\x0f\xf0\x03\xc7\xf8\x3e\x03\xff\x80\x03\xff\x80\x07\xff\x0f\xff\x1f\x83\xff\x80\x1f", + "\xff\xc0\x1f\x80\x3f\x9f\xf8\x78\x3e\x7f\xf8\x00\x3e\x20\x04\x3f\x80\x7f\xfc\x00\x1f\xfe\x00", + "\x3f\x07\x80\xe0\x07\xe0\x00\xfc\x7f\xc0\xc0\x0f\x8f\xf0\x80\x0e\x0e\x03\xff\xbf\xfc\x01\xff\xe0", + "\xff\xfc\x11\xfc\xe0\x0e\x1f\xff\x87\x80\x1f\xe0\xff\xfd\xff\xc0\x03\xff\xc0\x0f\x00\x07\xf0\x01\xff", + "\xf0\x07\xc1\xfe\x00\xf8\x01\xe7\x80\xff\x80\x3f\x1f\x7f\x8c\x00\x1c\x00\x0f\xf8\x07\xfc\x00\xff\xfc\x00", + "\x00\x0f\xf8\x3f\xc0\x60\x00\x7f\xf8\xff\x00\x03\xf0\x3c\x07\xc0\x7f\xe0\x3f\xf8\x01\x00\x7e\x03\xff\xc0\x00", + "\x00\x0f\xf8\x03\x00\x1f\xff\x00\x0f\xfe\x00\x3f\x00\x03\xff\xe0\x07\xc0\xff\x00\x3c\x7f\xf0\x01\xff\xf8\x3f\xff", + "\x00\x01\xe0\xe0\x1f\xfe\x00\x03\xfc\x00\x0f\xff\xe0\x0f\xff\x00\x0e\x00\x7f\xfc\x0f\xfe\x00\x78\x00\x3f\xff\x00\xff", + "\x80\x41\xff\xc3\xfe\x00\x1e\x00\x0f\xff\xe0\xff\x80\x0f\xe0\x00\x7f\xf7\xff\x01\xfe\x01\xff\xdf\xff\x00\x01\xff\xe0\x00", + "\xf8\x07\x00\xff\xc0\x7f\xbe\x00\x0f\xff\x00\x03\xe3\xf0\xff\xf0\x00\x1f\x81\xff\x80\x0f\xff\x80\x20\x03\xf0\x03\x80\xff\xfc", + "\x00\x38\x20\x00\x7f\xf0\x01\xff\xfe\xcf\xfe\x07\xff\xc0\x00\x7f\xf8\x1f\x00\x00\xc0\x00\xc0\x0f\xff\x3e\x0f\xc0\x0f\xff\x80\x00", + "\x1f\xf8\x07\xff\xf8\x03\xe0\x01\xff\xfc\x3f\xf8\x00\x38\x1f\x00\x3f\xdc\x01\xc0\x04\xff\xff\x00\x0f\xfc\x08\x02\x00\x01\xf0\x3f\xff", + "\x80\x07\x86\x00\x03\xff\xe0\x00\x3f\xf8\x00\x0f\x80\x0f\xf8\x0f\xff\xe0\x00\x1f\x80\x00\x7f\xf8\xc0\x0f\xff\xf0\x7c\x04\x07\xff\x00\x00", + "\x01\xff\x00\x18\x3e\x0f\x00\x07\xff\xc0\x00\xf0\x1f\xfe\x07\x80\x60\x0f\xf8\x00\x3f\xfe\x38\x1f\xc0\x00\x3f\x81\xff\xfc\x1f\xe0\x00\x3f\xff", + "\xf0\x3f\xff\xc0\x00\x7f\xf0\x00\x3f\xff\x0f\xe0\x07\x0f\xfc\x7e\x03\xff\xf0\xfc\x0f\x9f\xc0\x3f\xff\xcf\xff\x00\x00\xff\xc0\x00\xe7\x01\xff\xf8", + "\x00\x01\xff\x80\x20\x00\x7f\xe0\x00\x7e\x07\xff\xf8\xc7\xf8\xff\xf0\x0f\xfe\x00\x00\xe0\x0f\xe0\x00\x1f\xff\x87\xff\x00\x01\xf0\x00\x7f\xc1\xff\xff", + "\x00\x00\x7f\xff\xc0\x01\xfe\x7e\x01\xff\xfe\xff\xf0\x7f\xff\xcf\xf8\x07\xfe\x00\x0f\xff\xc0\x07\xff\xfc\x00\x3e\x00\x07\xfc\x00\x7f\xc0\x07\x80\x0f\xff", + "\xff\xff\x03\xff\x07\xf8\xff\xff\x80\x00\x7f\xfe\xff\xfe\x00\x03\xff\xf8\x1f\xff\x3f\xf8\x1f\xff\x00\x1f\xff\x0f\xc0\x7f\xf0\x01\xff\xe0\x00\x1f\xff\x00\x00", + "\xff\xff\x00\x00\xff\xfc\x00\x03\x0f\xff\xf0\x01\xf8\x00\x0f\xe1\xff\xff\x03\xe0\x3f\x1f\xff\x80\x00\x7c\x00\x01\xff\xc0\x01\x7f\xfe\x00\x0e\x07\xff\xe0\xff\xff", + "\xc0\x00\x3f\xfe\x03\xfc\x0c\x00\x04\x01\xff\xe1\xe0\x03\xff\xe0\x30\x01\xff\x00\x00\x3c\x1e\x01\x80\x01\xff\x00\x40\x3f\xfe\x00\x3f\xff\x80\x7c\x01\xff\x80\x00\x7f", + "\x3f\xa0\x00\x0f\xff\x81\xff\xc0\x0f\xf0\x7f\xf8\x00\x0f\xc0\x00\x7f\xe0\x01\xe0\x00\x04\xff\x00\x1f\xfe\x00\x01\xff\x80\x07\xff\xfe\x00\x3f\xff\xc0\x03\xff\x80\x00\x3f", + "\xf0\x1f\xff\x01\xff\x80\xff\xc0\x80\x07\xf0\x00\x03\xff\x80\x00\x18\x01\xff\xfc\x00\xff\xfc\x03\xff\xff\x00\x7f\xc0\x03\xff\xc7\xff\xc0\x03\xf0\xff\x80\x00\x3f\xfe\x00\x00", + "\x07\xf1\xbf\xff\xe0\x00\x78\x00\x07\xe0\x00\x80\x03\xf0\x3f\xf7\x00\x00\x38\x00\xfe\x00\xf8\x0f\xfe\x00\x00\x80\x3f\xff\xc1\xff\xfc\x00\xff\xff\x8f\xf0\x00\x1f\xff\xf0\x0f\xff", + "\x00\x1c\x00\x07\xff\xfc\x00\x5e\x3f\xff\x00\x00\x3c\xff\xff\xc0\x3f\xff\x81\xe0\x70\x00\x1f\xfc\x00\x03\xff\x00\x00\x7f\xff\xc0\x1f\x8c\x0f\xff\xf0\xff\x80\x07\xe0\x10\x01\xff\xff", + "\xc0\x00\x07\xff\x80\x7f\xff\x80\x01\x80\x3f\xff\xcf\xc0\xfe\x00\xff\xc0\x1f\xfc\x01\xff\xf8\x00\xff\xfe\x0f\xff\xf0\x06\x00\x00\xc0\x3f\xff\x80\x78\xff\xfc\x00\x0f\xff\xf0\x00\x0f\xff", + "\xff\xe0\x07\xff\xf8\x00\x7f\xf0\x1f\xff\x80\x01\xff\xf8\x1f\xf8\x01\x03\xff\xe0\x00\x03\xe0\x78\x0f\xff\x00\x0f\xfc\x1f\xf8\x00\x0f\xff\xe0\x1f\x00\x07\xff\xfc\x00\x1f\x03\xff\xf7\xff\xff", + "\xc0\xf8\x00\x03\xfe\x00\x3f\xff\xf0\x00\x03\xfc\x0f\xff\x80\x00\xe3\xff\xf8\x3f\xfe\x00\x00\x73\xe0\xff\xfc\x07\xff\xc3\xff\xfe\x03\x00\x00\x70\x00\x03\xff\xf8\x0f\xff\xe0\x00\x1f\xff\xf8\x00", + "\xff\xf0\x0f\xc7\xff\xfc\x00\x3f\xfe\x00\x00\x3f\xff\x80\x3f\x80\x00\x3f\xff\xc0\x00\x70\x01\xff\xc1\x80\x03\xff\xff\x80\x00\x61\xff\xfe\x03\xfd\x80\x3f\xff\xe0\x01\xc1\xff\xff\x80\x00\x0f\xfe\x00", + "\xff\xfc\x00\x03\xff\xf0\x0f\xf8\x00\x07\xdf\x8f\xff\xf8\x00\x01\xff\xfe\x00\x80\x00\xff\x80\x1f\xf0\x00\x01\x1c\x00\x00\x3f\xf8\x00\x3f\xff\xef\xff\xfe\x01\xc3\x80\x80\x01\xff\xff\xc0\x00\x07\xff\xff", + "\xff\xff\xc0\x01\xff\xc1\xff\xff\x87\xff\xff\x00\x3f\x00\x00\x1f\xfc\x00\x01\xff\x80\x1f\xc0\x1f\xff\x00\x00\xff\x80\x1f\xff\xf8\x7f\xf8\x3f\xff\xc1\xff\xff\xe0\x01\xc0\x3f\xf7\xff\xfe\xfc\x00\x00\x3f\xff", + "\x00\xff\x81\xff\xe0\x03\xf8\x0e\x00\x00\xff\xf8\x1f\xff\xfe\x00\x00\xff\x80\x00\x07\xff\xf8\x01\xff\xe0\x00\x0f\xf0\x01\xfe\x00\x3f\xf0\x7f\xe0\x00\x7f\xff\xe0\x1f\xff\xfc\x01\xff\xe0\x01\x80\x00\x07\xff\xff", + "\x00\x0f\xff\xf0\x00\x00\xe0\x0f\xf8\x00\x00\xff\xff\x80\x03\xff\xe1\xff\xff\x3f\xf8\x0f\xff\xc7\xe0\x00\x1f\xff\x00\x3f\xfe\x0f\xff\xf0\x03\x00\xc0\x00\x1f\xff\xfc\x3f\xff\xe0\x3f\xff\xf8\x1f\xf0\x00\x1f\xff\xc0", + "\x01\x80\x00\x1f\x01\xff\xff\x83\x00\x01\xfc\x00\x7f\xe0\x0e\x7f\xfe\x00\x00\x38\x00\xff\x00\x00\x3f\xff\x83\x83\xff\xc0\x00\x7f\xff\x80\x1f\xff\xf0\x1f\xff\xfc\x00\x03\x7f\xff\x81\xc0\x00\x07\xff\x83\xff\xff\x00\x00", + "\xff\x80\x0d\xff\xe0\x03\xff\xf0\x00\xff\xfc\x00\xf0\x01\xf8\x07\xff\xf8\x0f\x80\x0f\xff\xff\x00\xff\xff\x87\xff\xe1\xff\xfc\x67\x8c\x7f\xfe\x00\x03\xff\x3f\xfc\x07\x01\xff\xff\xe0\x00\x01\xff\xff\xc0\x0c\x40\x0f\xff\xff", + "\x00\x00\x1f\xff\xfe\x00\x1f\x00\x00\x1f\xff\xff\x07\xff\xff\xc0\x07\xff\xe0\x00\x02\x00\x00\xff\x00\x78\x00\x00\xe0\x00\x08\x00\x1f\xff\xff\x00\x03\xf8\x1f\x00\x00\x0f\xff\xc0\x00\x01\xff\xff\xe1\xf8\x00\x00\x3f\x80\x0f\xff", + "\x00\x0f\xf8\x00\xfc\x00\x03\xff\xff\x00\x00\x3f\xf0\x01\xff\xff\xe0\x7f\xf8\x00\xf8\x0f\xff\xff\x80\x00\x0f\xff\xfc\x0f\xff\xe0\x00\x00\xff\xc3\xff\xf0\x07\xff\xff\x00\x38\xf8\x00\x20\x1f\xfe\x3f\xfe\x00\xfe\x00\x7f\xff\xc0\x00", + "\x00\x3f\x00\xe0\x00\x0f\xff\xfc\x7f\xff\xfc\x00\x00\x7e\x00\x00\xff\xfe\x1f\xf0\x00\x1f\xf0\x00\x1f\xff\x87\xf0\x00\x3f\xc0\x0f\xff\x87\xff\x00\x3f\x81\xff\xff\xf7\xff\xe0\xff\xe0\x3f\x9f\xff\x00\x07\x00\x7f\xfc\x03\xff\xf0\x00\x00", + "\xe0\x3f\xff\xf0\xff\x80\x3e\x00\x03\xff\xe0\x00\x0f\xfc\x00\x07\xff\xf8\x00\x00\x7f\x80\x00\x0f\xf8\x01\xff\x7f\xff\xf0\x00\x3f\xff\xfe\x7f\xff\xe0\x00\xff\xc3\xff\xff\x00\x00\xf0\x00\x00\x7f\xff\x00\x3f\xff\xf0\x00\x01\xc0\x03\xff\xff", + "\x00\x03\xc0\x01\xff\xdf\xfd\xff\x9f\xfe\x1f\xff\xff\x00\x3f\xff\xfe\x00\x00\x7f\xcf\xff\xf0\x1f\xff\xfe\x07\xf0\x00\xff\xff\xe0\x00\x01\x00\x07\xff\x80\x1f\xe0\x00\x00\xff\xfe\x03\xff\xff\x80\x03\xf0\x0f\xff\xfe\x00\x00\x1f\xff\xf8\x00\x00", + "\x00\x1f\xff\xfb\xff\xfe\x00\x07\xff\xf0\x00\x00\xff\xff\x00\x00\x0f\xf3\xff\xfe\x00\x78\x00\x00\x3e\x00\x00\x3f\xff\xf8\x00\x1f\xff\xff\x80\x00\x03\xff\xff\x00\x07\xff\xee\x00\x1f\xfc\x00\x78\x00\x00\x1f\xff\x07\xff\xfe\x03\xff\xff\xe0\x00\x00", + "\x00\x7f\xff\xfe\x00\x00\x3f\xfc\x03\xff\xfc\x1f\xff\xf0\x7f\xd8\x03\xf0\x00\xfd\xfc\x38\x00\x08\x00\x10\x00\xe0\x06\x00\x7f\xfe\x00\x00\x0f\xff\x80\x00\x3f\x03\xff\xfe\xff\xff\xf9\xff\xf8\x00\x07\xff\xfc\x01\xff\xc0\x00\x03\xff\xff\xe0\x03\xff\xff", + "\xff\xf0\x0f\xff\xff\x00\x06\x00\xff\xff\xf0\x07\xff\xe0\x04\x00\x03\x00\x00\x03\xf0\xff\xff\x00\x03\xff\xfb\xff\xc3\xff\xf0\x07\xff\xff\xc7\x00\x7f\x80\x00\x03\xff\xf8\x00\x1f\xe1\xff\xf8\x63\xfc\x00\x3f\xc0\x9f\xff\xf8\x00\x00\x7f\xff\x1f\xff\xfc\x00", + "\x00\x3f\xff\xfc\x00\x0f\xc7\x80\x00\x02\x00\x1e\x00\x00\x60\x7f\x03\xfe\x00\x00\x1f\xff\x80\x1f\xf8\x00\x00\xff\xff\x80\x00\x03\xff\xc0\x00\x7f\xff\xc0\x7f\xe0\x03\xfc\x00\xff\xf7\xff\xff\x00\x00\x1f\xf0\x00\x03\xff\xff\xe1\xff\xff\x80\x0f\xf8\x00\x00\x1f", + "\x00\x01\xfe\x00\x03\x83\xf3\xff\xff\x80\x07\xff\xfc\x3f\xff\xfc\x03\xff\x80\x00\x06\x00\x00\x78\x00\x07\xff\xff\x80\x07\xfc\x01\xf8\x00\x07\xff\xff\xc0\x00\x38\x00\x07\xff\xfe\x3f\xff\xf8\x3f\xff\xcf\x3f\xfc\x00\x7f\xff\x00\x1f\xff\x80\x00\x30\x03\xff\xff\x00", + "\xf8\x00\x38\x00\x00\x3e\x3f\x00\x00\x3f\xff\xf0\x02\x00\x00\x0f\xff\xff\x80\x80\x03\xff\xc0\x00\x04\x00\x0f\xc0\x3f\xff\xfe\x00\x00\x3f\xff\xfe\x00\x3f\xff\xf8\x00\x30\x00\x7b\xff\x00\x00\x03\xff\xfc\x3f\xe1\xff\x80\x00\x70\x1f\xff\xc0\x07\xfc\x00\x1f\xff\xf0\x00", + "\x00\x03\xf8\x18\x00\x00\x70\x3f\xff\xf8\x00\x00\xff\xcf\xff\xff\xc0\x03\xff\xfe\x00\x10\x00\x00\xfe\x03\xff\xf8\x00\x00\x7e\x00\x00\x7f\x8f\xff\xc0\x00\x00\x7f\xff\xe0\x00\x3c\x07\xc0\x00\x00\x7f\xff\x01\xff\xf8\x01\xff\x80\x00\x0f\xff\xf9\xe0\x00\x3f\xff\xe0\x00\x00", + "\xff\xfe\x00\x3f\xc0\x1f\xff\xf0\x7f\xf8\x00\x01\xff\xf8\x1f\xff\xfe\x00\x00\xff\xff\xf8\x00\x7f\xff\x80\x3f\xff\xff\x00\x7f\xff\xf8\x00\x0c\x00\x00\x0f\xfe\x7e\x00\x3f\xe0\x18\x7f\xfe\x00\x00\x38\x00\x00\x3f\xff\xfe\x00\x00\x03\xfc\xff\xe1\xfe\x1f\xff\xfe\x00\x00\x07\xff", + "\x00\x00\x07\xff\xfe\x00\x00\x07\xfe\x00\x00\x3f\xe0\x00\x7f\xff\xc0\x00\x00\x7f\xff\xfc\x00\xfe\x00\x03\xff\xe0\x00\x1f\x0f\xfc\x00\x1f\xff\x80\x00\x07\xff\xff\xf0\x00\xff\xff\xf0\x00\x00\x1f\xff\xf8\x01\xff\xe0\x1f\xff\xff\x00\x1f\x80\x07\xf0\x00\x01\xff\xf8\x00\x01\xff\xff", + "\x00\x00\x3f\xff\xff\x03\xfe\x00\x00\x07\xc0\x00\x00\x7f\xfc\x0f\xf0\x00\x00\x1f\xff\xfe\x00\x00\x07\xc0\x00\x00\xff\xfe\x00\x00\x3f\xff\xfc\x01\xff\x7f\xfc\x00\x1f\xf8\x00\x1f\xff\x07\xff\xff\xe0\x00\x7f\xff\xfc\x01\xff\xff\xf0\x00\x01\xff\xf8\x00\x1e\x00\x00\x7f\xfc\x00\x3f\xff", + "\xfe\x3f\xff\x83\xff\xfe\x00\x07\xff\xff\xf0\x00\x3e\x00\x00\xff\xff\xfc\x00\x40\x3f\xfe\x00\x00\x03\xf0\x00\x00\x70\x3f\xf8\x0f\xff\xff\xe0\x1f\x80\x00\x03\xc3\xff\xff\xf0\x00\x01\xff\xf0\x0f\x80\x00\x0f\xe0\xff\xff\xfe\xf0\x00\x01\xff\xc0\x00\x00\x7f\xf0\x00\x00\x7f\xfe\xe0\x00\x00", + "\x00\x00\x03\xff\xf0\x01\xfc\x00\x00\xff\xff\x00\x00\x7f\xff\xff\x80\x07\xff\x8f\xff\x80\x00\x0f\xff\xf0\x00\x00\x3c\x00\x03\xc0\xff\xff\xfe\x01\xff\xff\x80\x0c\x7f\xff\xf8\x00\x00\x1f\xf0\x00\x00\x7f\x80\x00\x00\x80\x00\x00\xff\xff\xf0\x1f\xff\xe0\x00\xff\xff\xfe\x1f\xff\x1f\xc0\x00\x00", + "\xff\xff\xfe\x07\xff\xc0\x00\x06\x3f\x9f\xf0\x07\xff\xf0\x3f\xfe\x1f\xff\xff\x81\xff\xff\xc0\x00\x02\x00\xfe\x00\x04\x00\x07\x00\x00\x01\xff\xff\xfe\x00\x00\x07\xff\xfe\x00\x1f\xfe\x00\x00\xff\xff\xe0\x07\xf8\x00\xff\xff\xfc\x00\x3f\xf3\xff\xff\xc0\x00\x7f\xff\xe0\x00\x0f\xff\xfc\x07\xff\xff", + "\xff\xf0\x00\x00\x7e\x00\x1e\x03\xff\xff\x00\x00\x73\xff\xf0\x00\x00\x0f\xff\xdf\xff\xff\xdf\xfc\x00\x07\xfe\x07\xff\xfe\x00\x00\x1f\xdf\xef\xff\xf0\x3f\xff\xfc\x00\x00\x07\xff\xff\xf0\x00\x00\x7f\xe0\x07\xff\x80\x00\x00\x7f\xe0\x03\xff\xff\xf9\xff\xe0\x00\x00\x3f\xe3\xff\xff\xfc\x00\x00\x03\xff", + "\x00\x03\xff\x00\x00\x3f\xff\x80\x01\xf0\x00\x0f\xfe\x00\x00\x06\x00\x03\xff\xff\xfc\x03\xff\xff\xf7\x80\x00\x00\x7f\xc0\x0f\xff\xe3\xfe\x0f\x00\x00\x7f\xff\x00\x7f\xf8\x00\x00\xff\xff\xee\x00\x7e\x01\xc0\x00\x1f\xe0\x00\x07\xff\xff\xf8\x00\x00\xe1\xff\xfc\x3f\xe7\xff\xff\xf8\x3f\xff\xfc\x00\x1f\xff", + "\x00\x00\x0f\xff\xf8\x00\x00\xff\xff\xfc\x00\x1f\xe0\x07\xff\xff\x00\x01\xff\xdf\xff\x80\x00\x3f\xff\xfc\x00\x00\x0f\xfc\x07\xff\x00\x00\xff\x80\x00\x03\xff\xff\xf0\x00\x07\xff\xff\xf0\x00\xff\xfe\x1f\xff\xff\xe0\x3f\xff\xfe\x00\x00\x60\x00\x00\xff\xff\x7f\xff\xf0\x00\x03\xff\xff\xc0\x07\x00\x01\xff\xff", + "\x00\x00\x20\x7f\xfe\x0f\x83\xff\xff\x80\x03\xff\x00\x00\x00\xff\xff\xe0\x00\x1f\xff\xff\xe0\x00\x3f\xfe\x7f\xff\xf0\x00\x1f\xff\xff\xe0\x00\x00\xff\xff\x87\xff\xc0\x00\x17\xfd\xff\x9f\xff\xfb\xff\xff\xe0\x00\x03\xe0\x00\x07\xff\x9f\xff\xff\x80\x00\x7f\xff\xff\x00\x01\xff\xff\xc0\xff\xff\xc0\x10\x00\x00\x1f", + "\x00\x00\x07\xff\xc0\x00\xff\xe0\x00\x07\xff\x80\x03\x80\x00\x0f\xf8\x00\x00\x7f\xff\xfe\x00\x00\x18\x00\xff\xf0\x20\x01\xff\xfe\x00\x00\x60\x0f\xf0\xe0\x03\xff\xfe\x00\x3e\x1f\xff\xfc\x00\x03\xff\x80\x00\x00\xff\xf8\x00\x01\x00\x00\x0f\xf3\xff\xfc\x00\x03\xff\xff\xe1\xff\xff\xc1\xf0\x00\x00\xff\xff\xff\x00\x00", + "\xff\xff\xf0\x00\x00\x07\xff\xfc\x00\x7f\x87\xff\xff\x00\x00\x00\x7f\xff\xc0\x7f\xff\x80\x00\x03\xf0\xff\x3f\xff\x80\x30\x07\xff\xff\x1f\x8e\x00\x7f\xff\xff\xc0\x01\xff\xfc\x07\xf8\x00\x00\x7f\xff\xfc\x00\x3f\xf0\x00\xf8\x00\x00\x07\xff\x00\x00\x0e\x00\x0f\xff\x80\x00\x7f\xc0\x01\xff\x8f\xf8\x00\x07\x01\xff\xff\xff", + "\xff\x80\x3f\xff\x3f\xfe\x00\x00\xff\xff\xff\x9f\xff\xf8\x3f\xff\xf8\x00\x00\x0f\xf8\x00\x00\x03\xfe\x00\x7f\xff\xff\x00\x0f\xff\x01\xff\xf0\x0f\xff\xe0\x20\x7f\xff\xfc\xff\x01\xf8\x00\x07\xff\xe0\x00\x7f\xf8\x00\x0f\xff\x80\x00\x00\x7f\xe0\x00\x3f\xf8\x01\xfe\x00\x07\xff\xf0\x00\x00\x7f\xff\xff\xc0\x00\x01\xff\xff\xff", + "\x00\x7f\xff\xe0\x00\x01\xff\xff\xf8\x00\x00\x3f\xff\xfc\x00\x7f\xfe\x00\x00\x03\xff\xff\xf0\x03\xff\xe0\x00\x7f\x80\x00\x0f\xff\x3f\xf8\x00\x00\x7f\xff\xff\x00\x07\x80\x1f\x83\xf8\x00\x00\x0f\xfe\x3f\xff\xc0\x3f\xff\xfe\x1f\xe0\x00\x07\xc0\x03\xff\xf0\x0f\xc0\x00\x03\xff\xff\x80\x00\x00\x7f\x80\x00\x00\xff\xff\x80\x00\x00", + "\xfe\x00\x00\x20\x00\x04\x00\x0f\xff\xff\xc0\x01\xff\xf8\x3f\xc0\x00\x00\xff\xff\xc0\x00\xff\xff\xff\x80\x00\x3f\xf8\x00\x7f\xff\xfe\x7f\xf8\x00\x7f\xff\x80\x07\xff\xc0\x00\x0f\xff\xf8\x00\x7f\xff\xc0\x00\xff\xff\xc0\x3f\xff\xff\xe0\x0f\xff\xff\xe0\xe0\x1f\xff\x80\x00\x00\x7f\xff\xc0\x71\xff\xff\xfc\x00\x01\xff\xff\xf8\x00\x00", + "\xff\xff\xe0\x00\x0f\xff\xf0\x00\x00\x3f\xff\xff\xc0\x00\xff\xff\x00\x00\x0f\xff\xff\xe0\x00\x01\xff\x00\x00\x1f\xff\xe0\x3f\xfc\x00\x03\xe0\x1f\xf0\x1f\xf8\x00\x00\x3f\xff\xff\xc0\x0f\xfe\x00\x00\x20\x00\x00\xff\xfc\x00\x0f\xff\xfe\x3f\xff\xff\x00\xff\xf0\x00\x00\x80\x00\x1f\x03\xe0\x01\xff\xfa\x00\x3f\xe0\x00\x00\x70\x00\x00\x0f", + "\xfd\xff\xc0\x00\x20\x01\xfe\x00\x3f\xf8\x00\x03\xff\x00\x00\x03\xf8\xff\xcf\xc3\xff\xff\xfc\x00\x03\xff\xff\xfc\x00\x00\x78\x3f\xff\xf0\x01\xff\xe0\x0f\xff\xff\x00\x00\x07\xff\xff\xfc\xff\xff\xf8\x00\x01\xff\x80\x00\x07\xff\xff\xfc\x00\x00\x1c\x00\x01\xff\xff\x07\xf8\x00\x00\x1f\xff\xff\xf0\x00\x01\xfe\x00\x7f\xff\xf0\x1f\xfc\x00\x00", + "\x00\x00\x00\x3f\xff\x00\x00\x07\xff\xff\xfc\x00\x00\x01\xe0\x0f\xff\x83\xfc\x01\xff\xff\xf0\x0f\xff\x00\x00\x00\x3f\xff\xff\xc0\x1f\xff\xff\xe0\x00\x20\x00\x00\x3f\xff\xff\x00\x00\xff\xfc\x03\xff\xff\xf0\x00\x1f\xff\xfc\x00\x00\x03\xff\xff\xc0\x7f\x80\x00\xff\xff\xff\x00\x0f\x0f\xff\xff\xe0\x00\x00\xff\xf8\x00\x00\xff\xfe\x00\x00\x0f\xff", + "\xff\xfe\x03\x80\x00\x03\xff\xff\xc0\x3f\xff\xff\x00\x03\xff\xff\xf8\x00\x00\x01\xff\xff\xcf\xfc\x00\x00\xe0\xef\xf8\x00\x0f\xff\xe3\xf8\x00\x3f\xff\xff\x80\x3f\xbf\xfe\x00\x00\xff\xfc\x00\x00\x01\xff\x00\x00\xcf\xc0\x01\xfc\x00\x00\x7f\xff\xff\xc0\x00\x10\x7f\xff\xfc\xfe\x00\x00\x07\xc0\xff\xff\xff\x8f\xff\x00\x00\x1f\x9e\x00\x00\x01\xff\xff", + "\x00\x7f\xff\xe0\x00\x01\xff\xff\xc3\xff\x80\x01\x03\xfc\x00\x00\x00\xfc\x01\xff\xff\xf8\x7f\xe7\xf0\x00\x00\x7f\xc0\x3f\xff\xff\xc0\x00\x00\x1f\xff\x80\x00\x01\xff\xe0\x00\x00\x70\x00\x00\x1c\x7f\xff\xf8\x1f\xfc\x00\x00\x07\xef\xe0\xff\xff\xc1\xff\xfc\x00\x00\x01\xff\xff\xff\xa0\x07\xff\x00\x1e\x00\x1f\xfc\x00\x00\x38\x00\x18\xc0\x00\x00\x7f\xff", + "\x00\x0f\xff\xf8\x00\x00\x07\xff\x00\xfc\x00\x00\x03\xff\xfc\x00\x07\xff\xff\xe0\x00\x00\xff\xfc\x0f\xff\x00\x00\x0f\xff\xfe\x0f\x80\x07\xff\x03\xff\xff\xf9\xff\xfe\x00\x00\x03\xf8\x00\x00\x07\xe0\x00\x00\xc0\x00\x1f\xff\xf0\x7f\xff\xff\xc0\x07\xff\xff\xfc\x00\x00\x3f\xff\xff\xe0\x00\x00\x1f\xff\xf8\x1f\xfe\x00\x00\x3f\xff\xff\xe0\x3f\x06\x00\x00\x00", + "\xff\xf0\x00\x08\x00\x0f\xef\xff\xff\xfc\x00\x00\x7f\xff\xf0\x00\x7f\xff\xf8\x00\xff\xff\x81\xff\xff\xe0\xff\xff\xff\x00\x00\x00\x80\x00\x03\xff\x80\x3f\xff\xfc\x00\x00\x1f\xff\xc0\x0f\xff\xfe\x00\x00\x00\x73\xf0\x1f\xfe\x00\xff\xc0\x3f\xff\x00\x3f\xff\x83\xff\xfe\x01\xff\xff\xf7\xff\xff\x80\x00\x00\x3f\x00\x00\x1f\xe3\xff\xff\xf0\x00\x0f\xff\xf0\x00\x00", + "\x00\x00\x7f\xfc\x00\x7f\xe0\x00\x0f\xff\xe0\x01\xf8\x00\x3f\xff\x00\x00\x78\x00\x7f\xe0\x00\x00\x1f\x00\x07\xff\xff\xf8\xf9\xf0\x01\xff\xf8\x07\xc0\x0f\xff\xf8\x00\x07\xf8\x7f\xfe\x00\x00\x0f\xff\xe3\xf0\x00\x07\xff\xff\xfc\x03\x1c\x00\x00\x7f\xe0\x00\xff\xff\xfc\x00\x00\x0f\xf3\xff\xe0\x00\x00\x0f\xff\xf9\x00\x00\x10\x00\x3f\xff\xfc\xf8\x7f\xff\x00\x00\x00", + "\x00\x03\xff\xff\xc0\x7f\xff\xff\xc0\x00\x03\xff\xff\xff\x00\x00\x0f\xff\xf0\x1f\xff\xf0\x00\x07\xff\xff\xef\xff\x81\xf7\xff\xfe\x00\x07\xff\xf0\x00\x00\x1f\xff\xc0\x0f\x80\x00\x0f\xff\xfc\x00\x00\xff\xff\xff\xc0\x03\xff\xe3\xff\xff\xfe\x00\x1f\xff\xff\x00\x00\xff\xff\xff\x0f\xff\xf1\xf8\x00\x00\x01\xff\xff\xff\x80\x1f\xff\xfe\x00\x08\x00\x00\x7f\xff\xff\x80\x00", + "\x1f\xe0\x00\x7c\x1f\xc0\x07\xff\xc0\x07\xff\xff\xfe\x00\x3c\x00\x00\x00\xff\xff\x80\x00\x07\xff\xff\x00\x1f\xf8\xff\xc0\x00\xff\x81\xff\x01\xff\xfe\x00\x78\x7f\xff\xf0\x00\x01\x80\x00\x00\x1f\xff\x00\x00\x1f\xff\xf0\x00\x1f\xff\xff\xe0\x00\x3c\x00\x00\x1f\xff\xff\x80\x03\xff\xe0\x01\xff\xff\xf9\xff\xf8\x00\x00\x7c\x00\x00\xfe\x00\x00\xff\xff\xff\x00\x00\x0f\xff\xff", + "\xfc\x00\x01\xff\x00\x00\x0c\x00\xff\xff\xe3\xff\xff\xf0\x80\x0e\x0e\x00\x00\x0f\xfe\x00\x03\xff\xc0\x00\x00\x7f\xff\xff\xe0\xc0\x00\x00\x07\xe0\xff\xff\x03\x9f\xff\xff\xc1\xc0\x00\x03\xff\xff\xc3\xff\xff\xfc\xff\xff\xc0\x00\x01\xfc\x00\x0f\xfc\x00\x00\x00\x7f\xff\xff\x03\xff\xff\xfc\x0f\xff\xfe\x00\x00\x03\x80\x3f\xff\xff\x00\x00\xff\xff\xf8\x00\x03\xff\xff\x80\x00\x00", + "\xff\xff\x80\xff\xff\xf8\x00\x00\xfc\x00\x03\xff\xf8\x00\x0f\xff\xff\x00\x03\x00\x00\x00\x7f\xff\xf0\x00\x3f\xff\xf0\x00\x01\xfc\x01\x00\x03\xff\x80\x1f\xff\xe3\xff\xff\xf8\x00\x1f\xff\xff\xf8\x01\xff\xdf\xff\xfb\xff\xc0\x00\x00\x3f\xff\xf8\x00\x00\x80\xc7\xff\xff\xf8\x0f\xff\x00\x60\x1f\xff\xe0\x00\x01\xff\xff\xfe\x0f\xff\xff\xfc\x00\x00\x00\xf0\x06\x03\xff\xff\xfe\x00\x00", + "\xff\x00\x0f\xff\xfc\x00\x0f\xff\xff\xfc\x07\xff\xfc\x00\x00\xf0\x00\x00\x80\x00\x7f\xfe\x00\x00\x0f\xff\xff\xfc\x3f\x00\xff\xff\xff\x00\x1f\xff\xff\xf0\x00\x1f\xff\xff\xe0\x00\x1f\xff\xff\xc3\xff\x00\x00\x01\xff\xff\xf0\x00\x00\x0f\xff\xe0\x07\xfc\x00\x00\x00\xfe\x00\x07\xff\xff\xf8\x00\x00\x3f\x00\x00\x0f\x80\x00\x3f\xff\xc0\x00\x11\xff\xef\x00\x07\x00\x7f\xff\xfc\x00\x00\x00", + "\xfe\x00\x00\x7f\xf7\xff\xff\x00\x00\x0f\xff\xff\xe0\x01\xff\xe0\x00\x00\x03\xff\xe0\x00\xff\xfe\x00\x01\xff\xf7\xff\xf8\x00\x0f\xff\x00\x00\x00\x38\x00\x07\xff\xf8\x07\xff\xfc\x00\x1f\xff\xff\x0f\xc1\xff\xff\xc0\x00\xff\xff\x0f\xff\xf0\x01\xff\xf8\x00\x01\xff\xff\x80\x00\x00\x0f\xf8\x00\x3f\xff\xfe\x3f\xff\xff\xf0\x00\x00\x38\x0f\xc3\xff\xff\xff\x1f\xff\xc0\x3f\xff\xff\xe0\x00\x00", + "\xff\xff\xf0\x00\x7f\xc0\x07\xff\xff\x81\xff\xc0\x00\x01\xff\xff\xff\xc0\x00\x00\x1f\xff\xfe\x03\xc0\x00\x0f\xff\xff\xfc\x00\x03\xff\xff\xff\x83\xff\xc0\x00\x07\xf0\x00\x00\x1f\x80\x00\x00\x3f\xff\xff\xe7\xff\x00\x07\xff\xff\xfc\x00\x00\x1f\xff\xf8\x00\x03\xf0\x00\xff\xfc\x00\x1f\xff\xff\xe0\x03\xff\xf0\x00\x01\xff\xff\xff\x80\x00\x00\x0f\xff\xff\xf8\x10\x00\x1e\x03\xff\xff\xff\x80\x00", + "\x00\x01\xff\xfe\x00\x00\x00\x7f\xff\xff\xf0\x00\x00\x1f\xff\x80\x07\xff\xff\xff\x80\x00\x01\xff\xfc\x03\xff\xff\x9f\xff\xfe\x00\x08\x00\x00\x06\x00\x00\x00\x81\xff\xff\xc0\x00\x07\xf8\x00\xff\xff\xff\x00\x00\x00\xff\xff\xf8\x00\x3f\xff\xff\xf8\x3f\xf8\x00\x00\x00\x7f\xff\xc0\x7f\xff\xff\xc0\xff\xff\xfe\x00\x00\x01\xff\xf8\x00\x00\x03\xff\xc1\xff\xf0\x00\x00\x03\x07\xf8\x01\xff\xfe\x00\x00", + "\xf8\x00\x00\x01\xff\xff\x80\x00\x0f\xff\xff\x8f\xff\x00\x1f\xff\x0f\xff\xc0\x00\x00\xff\xff\xfc\x00\x0f\xff\xf0\x00\x70\x00\x00\x3f\xff\xc0\x00\x00\x7c\x00\x00\x7e\x00\x0f\xfc\x00\x00\x00\xff\xff\x80\x00\x00\x0f\xff\xff\xfc\x38\x00\x00\x03\xf0\x00\x31\xf0\x1f\xff\xff\xc0\x07\xff\xff\xe0\x1f\xff\xff\xf3\xfe\x00\x00\x00\xff\xff\xc0\x00\x1f\xe7\xff\xe1\xff\xff\xdf\x00\x00\x00\x1f\xff\x00\x00\x00", + "\x3f\xff\xff\xf8\x00\x00\x00\x60\x00\x0f\xff\xff\xe0\x07\xff\xff\xff\x00\x00\x3f\xff\xff\xf0\x1f\xff\xff\x80\x00\x70\x00\x00\x01\x00\x00\x00\x3f\xff\xfe\x00\x00\x00\x1f\xf8\xfc\xc0\x0f\xff\xf8\x00\x3f\xff\xc0\xff\xff\x80\x00\x03\xff\xff\xf8\x00\x3f\xff\xfc\x00\x00\x0f\x81\xff\xc0\x03\xff\xc0\x3f\xff\xff\x80\x03\xff\xfe\x00\xff\xff\xfe\x00\x00\x1c\x00\x00\x00\x3f\xff\xff\xf8\x00\x7f\xff\xc0\x00\x00", + "\x00\x00\x00\x0f\xff\xfe\x0f\xff\xff\x87\xff\xff\xff\x00\x80\x00\x0f\xff\xc0\x00\x03\xf0\x1f\xf7\xe0\x00\x00\x70\x00\x01\xff\xff\xff\x80\x01\xfe\x07\xf0\x00\x01\xff\xfc\x00\x00\x04\x00\x01\xff\xfe\x07\xff\xff\xfe\x00\x07\xc0\x00\x00\xff\xff\xff\x87\xf0\x03\xff\xfc\x00\x00\x1f\xf8\x00\x01\xff\xff\xc0\x00\x00\x3f\xff\xc0\x00\x00\x7f\x8f\xff\xf8\x00\x00\x00\x7f\xff\xe0\x06\x0e\x00\x00\x0f\xff\xff\x80\x00", + "\x03\xff\xff\xfd\xe0\x00\x00\x1f\xff\xf8\x01\xff\xff\xfb\xff\xff\xe0\x01\xf0\xf0\x00\x00\xff\xff\xff\x80\x00\x00\x7f\xff\xff\xe0\x1f\xff\xff\xf0\x01\x80\xff\xff\xff\xe0\x00\x00\x7f\xff\xf0\xff\xff\xc0\x00\x00\x07\xfe\x00\x00\x00\x1e\x00\x1f\xff\xff\xe0\x1f\xff\xe0\x01\xff\xc0\x00\x3f\xff\xe0\x00\x00\x0f\xf0\x00\xff\xff\x7f\xc0\x1f\xf8\x3f\xff\xff\xc0\x00\x00\x7f\xff\xe0\xff\xfc\x00\x00\xff\x0f\xff\xff\xff", + "\x07\xff\xfc\x03\xff\xff\xff\xdf\xff\xff\x87\xff\x18\x00\x03\x80\x01\xff\x00\x00\x00\x1f\xff\x00\x00\x3f\xff\xff\xc0\x1f\xe0\x3f\xff\xff\xfc\x00\x00\x01\xff\xf8\x00\x00\x3f\xff\xff\xf8\x00\x00\x07\xe0\x00\x07\xff\xf9\xff\xe0\x00\x3f\xe0\x00\x7f\xef\xf0\x00\x07\x81\xff\xfc\x00\x00\x00\xff\xe0\x00\x30\x00\x00\x00\xff\xff\xf0\x00\x00\x03\xff\xfc\x7f\x07\xf8\x03\xff\xff\xff\x00\x3f\xfc\x00\x00\x01\xff\xc0\x00\x00", + "\x00\x7f\xfc\x00\x00\x03\xff\xf8\x00\x00\x61\xfe\x7f\xff\xfe\x00\x00\x1f\xff\xfc\x3f\xff\x80\x01\xff\xff\xff\xe0\x00\xff\xff\xff\x80\x1f\xf8\x00\x7f\xff\xff\xf8\x00\x00\x07\xff\xff\xe0\x00\x00\x07\xff\xff\xff\x80\x00\xff\x80\x0f\xff\xff\xfc\x00\x00\x7f\xff\xfe\x00\x00\x00\x30\x00\x00\x7f\x80\x00\x07\xff\xff\xf0\x00\x00\x03\xff\xc0\x0f\xff\xff\x80\x3f\xff\x80\x03\xff\xff\xfe\x03\xff\xff\xff\x7f\xfc\x1f\xf0\x00\x00", + "\x1f\xf0\x00\x00\x7f\xff\xfe\x02\x00\x00\x03\xff\xff\xff\xd8\x07\xff\xff\xe0\x01\xff\xff\x80\x00\x00\x07\xc0\x00\x0f\xff\xc0\x7f\xf0\x00\x07\xff\xff\x80\x00\x07\xf0\x00\x00\x7f\xfc\x03\xff\xff\xff\xc0\x00\x01\xff\xff\xf9\xff\xfe\x00\x00\x1f\xff\xc0\x00\x00\x03\xfe\x3f\xff\xff\x00\x07\xfe\x00\x00\x03\xc0\x00\x3f\xf8\x00\x10\x03\xfc\x00\x0f\xff\xc0\x00\x7f\xff\xe0\x00\xff\xf0\x00\x00\x7f\xff\xe0\x00\x00\x0f\xff\xff\xff", + "\xff\xff\xff\xf8\x00\x00\x60\x00\x00\x00\xff\xff\xfc\x03\xff\xfc\x00\x00\x3c\x00\x3f\xe0\x7f\xf8\x00\x07\xff\xf8\x0f\xf8\x00\x00\x7f\xff\xff\xfc\x00\x7f\xc2\x00\x03\xff\xff\xfe\x00\x01\xff\xff\xff\xf0\x03\xff\xff\xf0\x18\x07\xc0\x00\x0f\xff\xc0\x00\x00\x7f\xff\xff\x87\xe0\x00\x00\x07\x00\x1f\x80\x04\x07\xff\xe0\x00\x00\x1f\xff\x81\xff\x80\x00\x03\xff\xfc\x00\x00\x07\xff\xff\xff\xc0\x00\x00\x1f\x80\x01\xff\xff\x00\x00\x00", + "\x00\x0f\xfc\x1f\xf8\x00\x0f\xff\xff\xf8\x07\xff\xf1\xfc\x00\x1f\xff\xff\xf0\x00\x0f\xff\xdf\xff\xff\xff\x00\x00\x03\xff\xff\xff\x00\x01\xff\xff\xff\xc0\x10\x0f\xf0\x00\x00\x00\xfc\x00\x1f\x00\x07\x00\x01\xf0\x00\x00\x1f\xe0\x00\x00\x30\x7c\x3f\xff\xe0\x00\xff\xfc\x07\xff\xfc\x00\x3f\xff\xff\xf8\xff\xff\xc1\xfc\x1f\xff\xff\xf8\x00\x01\xff\xfc\x00\x00\x0f\xff\xff\xff\x00\x00\xff\xf8\x0c\x00\x00\x07\xff\xff\x00\x00\x00\x7f\xff", + "\xff\xff\x80\xff\xff\x00\x00\x00\x7f\xff\xff\x00\x1f\xfc\x00\x06\x00\x0f\xf8\x00\x00\x01\x80\x00\x00\x7f\xff\xff\xe0\x3f\xff\xff\xfc\x00\x60\x00\x00\x00\xfe\x00\x00\x07\xff\xff\xf0\x7f\xff\xff\xf8\x00\x00\x80\x00\x00\x0f\xff\xff\xff\xbf\xff\xff\xc0\x07\xff\xfe\x00\x00\x1c\x00\x1f\xfc\x07\x00\x01\xff\xff\x00\x00\x00\x80\x00\x1f\xff\x03\x80\x00\x00\x3f\xff\xff\xf8\x00\x07\xff\xff\xff\x80\x00\x1f\xff\xff\xe0\x1f\xff\xff\xc0\x00\x00", + "\xff\xff\xff\xf8\x00\x00\x00\x7e\x00\x00\x00\x1f\xff\x80\x07\xff\xff\xff\x80\x00\x00\x0f\xff\xff\xc3\xff\xf0\x00\x00\x04\x7f\xc0\x7f\xf0\x00\x3f\xff\x80\x00\x7f\xe0\x00\x03\xff\xc0\x00\x07\xff\x00\x00\x0f\xff\x80\x00\x00\x07\x80\x00\x00\x0f\xff\xff\xff\x01\xff\xff\xff\xc0\x03\xc0\x00\x00\x03\xff\xff\xe0\x00\x0f\xff\xff\xc0\x00\x03\xff\xfe\x00\x03\xff\xf8\x00\x00\x0f\xff\xff\xc0\x01\xff\xe0\x00\x00\xff\xff\xfc\x00\x00\x1f\xff\xff\xff", + "\xff\xf0\x00\x3f\xfc\x00\x00\xff\xff\xff\xe0\x1f\xc3\xfe\x00\x07\xff\xf8\x00\x0f\xf0\x01\xff\xff\xf0\x00\x00\xff\xc0\x0f\xff\xff\x80\x00\x00\xff\xff\xf3\xff\x80\x00\x00\x80\x08\x38\x00\x00\x0f\xff\xf0\x00\x1f\xff\xff\xfc\x00\x0f\x80\x00\x70\x00\x00\x31\xff\xff\xfe\x3f\xff\xf8\x00\x00\x00\x3c\x3f\xf0\x0f\xff\xff\x00\x03\xff\xfb\xff\xff\xff\x00\x0f\xff\xff\xfe\x00\x00\x00\xf0\x00\x00\x00\xff\xff\xfc\x00\x7f\xff\xf0\x00\x01\xff\xff\xfe\x00", + "\xff\xff\xf0\x0f\xf8\x3f\xff\xff\xe0\x00\x03\xff\xfe\x00\x00\x3f\xff\x80\xff\xff\xff\x00\x00\x00\xff\xff\xf8\xff\xff\xf0\x00\x0f\xf1\xff\xff\x00\x00\x00\x0f\xff\xfc\x00\x00\x00\x1f\xf0\x00\x00\x1f\xf0\x03\xff\xff\xff\xe1\xff\xe0\x00\x00\x1f\xff\xc1\xfe\x00\x00\x07\xff\xfc\x00\x00\x00\x1f\xfe\x3f\xc0\x00\x00\x01\xff\xfc\x00\x00\x1f\xff\xff\x0f\xe0\x00\x01\xfc\x00\xfe\x00\x00\x00\x7f\xe0\x00\x1f\xff\xff\xe0\x7f\x00\x0f\xf0\x00\xff\xfe\x00\x00", + "\x00\x7f\xff\xc0\x07\xff\xff\xff\x80\x07\xff\xff\xfe\x00\x00\x00\x7e\x00\x00\x00\x0f\xff\x80\x1f\xff\xfe\x07\xff\xff\xf0\x03\xc7\xff\xff\xfe\x00\x00\x00\x7f\xfe\x00\x00\x1f\xff\xfe\x00\x00\x00\xff\xfc\x00\x1f\xff\xc0\x00\x00\x3f\xff\x00\x1e\x00\x00\x03\xff\xff\xff\x80\x00\x00\x7f\xff\xf8\x03\xff\xfc\x00\x01\xff\xff\xfe\x00\x0f\xff\x02\x00\x07\xff\xff\xfe\x00\x00\x00\xfe\x01\xff\xff\xf7\xff\xff\x19\xff\xff\x00\x00\x07\xff\xc1\xff\x00\x00\x07\xff", + "\x00\x00\x00\x40\x00\x00\x07\xff\xff\xf8\x1f\xff\xff\xc0\x00\x3e\xff\xff\xf0\x00\x00\x00\x7f\xff\xfe\x00\x00\x00\xff\x80\x00\x00\x3f\xf1\xff\xff\xff\xe0\x00\x00\x01\xff\xff\xff\x00\x00\x1f\xff\xf8\x00\x07\xff\xff\xf8\x00\x1f\xff\xc1\xff\xff\xff\xe0\x01\xff\xff\xff\xe0\x00\x00\x07\xff\xff\xff\xcf\xff\xe0\x00\x3f\xe0\xff\xff\xc0\x00\x07\xff\xff\xe0\x01\xff\xfc\x3f\x00\x01\xff\xff\xfe\x00\x01\xff\x0f\xff\xff\xfc\x00\x00\x01\xff\xff\x80\x00\x00\x0f\xff", + "\xff\xff\xf0\xff\xff\xff\xc0\x03\x80\x00\x01\x00\x00\x03\xff\xff\xff\xf1\xff\xff\xff\xe0\x07\xfc\x00\x00\x03\xff\xf0\x00\x00\x00\x7e\x00\x00\x00\x07\x00\x3f\xff\xfc\x00\x0f\xc7\xff\xff\x00\x00\x07\xff\xff\xc0\x00\x00\x03\xff\xfc\x00\x00\xff\xe0\x00\x00\x00\x7f\xf0\x00\x00\xff\xff\xff\xfd\xff\x00\xff\xe0\xff\xff\xe0\x07\xff\xff\xf8\x7f\xff\xfe\x18\x00\x00\x01\xf0\x00\x1f\xff\xfe\x01\xc0\x01\xff\xff\xff\xf8\x00\x01\xfe\xff\xff\xff\x80\x00\x00\x07\xff\xff", + "\xff\xff\xf0\x00\x1f\xff\xff\x00\x00\x3f\xff\xf0\x00\xff\xff\x00\x07\xff\xff\xf8\x03\xff\xff\xe7\xff\xff\xff\x81\x00\x00\x01\xff\x00\x00\x3f\xff\xff\xf8\x00\x00\x00\xf0\x07\xff\xc0\x0f\xf0\x00\x3f\xff\xc0\x00\x7f\xf8\x00\xff\xfc\x00\x00\x1f\xff\x80\x1f\xfc\x00\x00\x01\xff\xff\x00\x00\x7f\xff\xff\xfe\x00\x00\x0f\xff\x80\x00\x00\x0f\xff\xff\xfc\x00\x00\x00\xff\xff\xfc\x00\x0f\xff\x80\x00\x3f\xff\xfe\x00\x00\x3f\xe0\x0f\xff\xff\x00\x00\x01\xff\xf0\x00\x00\x00", + "\xff\xc0\x00\x00\x03\xff\xff\xfc\x00\xff\xff\xfc\x00\x00\x00\x7f\xff\xff\x00\x00\x00\x1f\xe0\x00\x0f\xff\xc0\xf0\x00\x00\x7f\xff\xff\xe0\x00\x20\x1f\xff\xff\xff\x00\x00\x00\x1f\x80\x00\x00\x07\xff\xf1\xff\xff\xff\xc0\x00\x00\x1f\xff\xff\xf0\x00\x3f\xff\xf8\x00\x3f\xff\xff\xfe\x01\xff\xff\xfe\x7f\x9e\x00\x1f\xff\xfc\x00\x7f\xe0\x7f\xff\xff\xe0\x00\x7f\xff\xfe\x00\x00\x01\xff\xff\xff\xf8\x01\xff\xc0\x03\x00\x0f\xff\xf8\x00\x00\x0f\xf0\x0f\xff\x00\x00\x00\x0f\xff", + "\x00\x03\xff\xff\xff\xcf\xff\xf8\x7f\x8f\xff\xff\xfc\x01\xff\xff\xfc\x00\x00\x1f\xff\xff\xff\x80\x00\x00\x01\xff\xff\xe1\xff\xf0\x00\x00\x00\xff\xff\xff\xf8\x03\x80\x00\x3f\x80\x00\x0f\xff\xff\xff\xc0\x00\x00\x02\x7f\xff\xf8\x03\xff\xc0\x00\x00\x3f\xff\x80\x00\x00\x01\xff\xff\xe0\x00\x00\x03\x80\x00\x00\xff\xe0\x7f\xff\xff\xfc\x00\x00\x01\xff\xff\xfc\x00\x00\x00\xff\xff\x80\x00\x07\xfe\x00\x00\x07\xff\xf0\x00\x00\x1f\x80\x00\x00\x3e\x1f\xff\xff\xff\x9f\xff\xff\xff", + "\xff\xff\xfe\x00\x03\xff\xff\xff\x80\x01\xff\xff\xff\xa0\x3f\xff\xf8\x00\x7f\x03\xff\xff\xc0\x00\x0f\xff\xc3\xff\xf8\x00\x03\xff\xff\xff\xc0\x7f\xf0\x1f\xe0\x0f\xff\xff\xc0\x00\x1f\xfe\x0f\xff\xff\xe0\x00\x07\xff\xff\xfc\x00\x00\xfc\x00\x3f\xff\xff\xf0\x00\x00\x00\x3f\xfc\x00\x00\x00\x0f\xff\xc4\x00\x00\xff\xc0\x00\x03\xff\xff\xff\x80\x00\x03\xfc\x0f\xff\xff\xf0\x00\x00\x03\xff\xff\xc0\x07\xff\xff\xf8\x0c\x3f\xff\xf0\x00\x1f\xff\x80\x00\x00\x01\xff\xfe\x00\x00\x3f\xff", + "\x00\x00\x00\x1f\xff\xc3\xff\xff\x80\x00\x00\x3f\xff\xff\xfe\x00\x00\x00\x0f\xe0\x00\x7f\xff\xe0\x0f\xfe\x00\xff\xff\xff\xe0\x03\xff\xf8\x00\x00\x00\x3f\xe0\x00\x00\x01\xff\xff\xe7\xff\xff\xff\xe0\x00\x00\x07\xff\xff\xc0\x00\x1f\xe0\x00\x00\x01\xff\xff\x00\x00\x03\xff\xff\xff\xe0\x00\x00\x1f\xe0\x03\xff\xff\x00\x00\x00\x07\xff\xf0\x3f\x80\x00\x00\x0f\xff\xff\xfe\x00\x00\xff\xff\xff\x00\x00\x3f\xff\xff\x80\x0f\xff\xfe\x01\xff\xff\xff\xf8\x3f\xff\xff\xc0\x00\x00\xff\xff\xff", + "\xff\xff\xff\x80\x00\xff\xff\xff\xf0\x00\x00\x1f\x00\x3f\xff\xfe\x0f\xf0\x1c\x00\xff\xff\xe0\x00\x0f\xc0\x1f\xff\xc0\x00\x00\x01\xff\x80\xf7\xff\xf8\x00\x00\x3f\xff\xc0\x00\x00\x01\xff\xf0\x00\x03\xe3\xfc\x00\x07\xf8\x00\x00\x3f\xff\xff\xfe\x00\x00\x1f\x00\x00\x00\x18\x00\x3f\xff\xff\xf8\x0f\xe0\x00\x00\x00\x60\x00\x07\xff\xff\xfe\x00\x60\xff\xff\xff\xf8\x00\x00\x3f\xff\x80\x00\x00\x3f\xff\xff\xf0\x00\x00\x0f\xf0\x00\xff\xff\xf1\xff\x00\x3f\xff\xff\xff\x01\xff\xe0\x00\x00\x00", + "\xff\xff\xff\xfc\x00\x00\x00\xff\xff\xf0\x01\xff\xff\xf8\x00\x00\xff\xff\xff\xe7\xff\xf8\x01\xf8\x7f\xff\xff\x80\x00\x3f\xff\xfc\x00\x00\x01\xff\xff\xe0\x00\x3f\xc0\x00\x00\x7f\xff\xff\x00\x00\x0f\xff\xff\xff\xc0\x00\x0f\xff\xf1\xff\xff\xff\xf8\x00\x0f\xff\xff\xfc\x00\x00\x07\xff\xff\xff\x80\xff\xff\xf8\x07\xf0\x00\x00\x00\x1f\xff\xff\xff\xc0\x00\xff\xff\xff\xc0\x00\x0e\xff\xff\xff\xc0\x00\x00\x03\xff\xf8\x00\x00\x03\xff\x80\x00\x00\x00\xe0\x00\x00\x0f\xf8\x00\x00\x3f\xff\xff\xff", + "\x3f\x00\x03\xc0\x1f\x00\x00\x00\x0f\xf0\x00\x07\xff\xff\xef\x00\xfe\x00\x7f\xfe\x00\x00\x3f\xf0\x00\x3f\xc0\x00\x00\x07\xfc\x00\x00\x03\xff\xff\xff\x0f\xff\xff\xff\xc0\x07\xf8\x00\x00\xf8\x00\x00\x3f\x9f\xff\xff\xfc\x00\x00\x3f\xff\xc0\x00\x03\xff\xff\xc0\x00\x00\xff\xfc\x00\x1f\xff\xe0\x00\x00\x07\xff\xe0\x07\x00\x00\x00\x7f\xff\xfe\x00\x00\x00\xff\xff\xfe\x00\x00\x0f\xfc\x00\x07\xff\xf0\x00\x00\x00\x7f\x80\x03\xcf\xff\x80\x00\x01\xff\xff\xe0\x3c\x00\x00\x3f\xff\xff\xff\x80\x00\x00", + "\x30\x00\x00\x00\xff\xf8\x00\x00\xff\xfc\x00\x3f\xff\xff\x80\x00\x00\x0f\xcf\xff\xcf\xff\xff\xc0\xff\xff\xff\x80\x00\x00\x01\xff\xff\xff\xf0\x00\x00\x00\x1f\xff\x00\x03\xff\xfc\x00\x00\x00\x07\xff\x80\x00\x7f\xff\xff\xfe\x0f\xff\xc0\x00\x00\x00\x7f\xf0\xbf\xff\xff\xe0\x00\x00\xff\xf8\x00\x00\x01\xff\xff\xf8\x00\x00\x3e\x00\x00\x07\xff\xe0\x00\x00\x00\x80\x00\x03\x80\x01\xff\xff\xff\xf8\x60\x00\x00\x00\xff\xf0\x00\x1f\xff\xff\xc7\xff\xf0\x40\xff\xff\xfe\x00\x00\x07\xff\xdf\xff\x80\x00\x00", + "\xff\xff\xff\x83\xff\xf8\x1f\xff\x1f\xff\xff\x80\x0f\xff\xff\xe0\x00\x00\x00\x3f\xff\xff\xc0\xf8\x00\x00\x78\x00\x1c\x00\x00\x00\x1f\xe0\x00\x00\x00\xff\xff\xe0\x3f\xff\xff\xfe\x00\x00\x03\xff\xff\xfe\x00\x00\x00\x1f\xff\xfc\x00\x7f\xff\x00\x00\x00\x1f\xff\xff\x00\x02\x00\x00\x3f\xff\xfc\x00\x00\x00\xff\xf0\x1f\xfe\xff\xff\xc0\x01\xff\xff\xff\xf0\x00\x00\x00\xff\xff\xfe\x00\x00\x00\x0f\xff\xc0\x00\x00\x7f\x7f\xc0\x00\x00\x01\xff\xff\xfe\x0f\xff\xff\xff\xc0\x00\x0f\x80\x00\x00\x3f\xff\xff\xff", + "\xff\xff\xff\xfe\x00\x00\x00\xff\xf8\x0f\xff\xff\xf0\x00\x00\x07\xff\xff\xfb\xff\xc0\x00\x07\xfc\x07\xe0\x00\x00\x01\xff\xff\xe0\x00\x7f\xff\xff\xf8\x00\x00\x3f\xff\xf8\x00\x00\x00\x0f\xff\xff\x00\x00\x0f\xc0\x00\x00\x3f\xff\xf0\x00\x00\x01\xff\xff\xfe\x1f\xff\xf8\x00\x00\x20\x00\x00\x00\x3f\xff\xbf\xff\x9f\xff\xff\xfc\x3f\xff\xff\xf0\x00\x00\x07\x80\x00\x00\xff\xff\xe7\xff\xff\xff\xf0\x00\x00\x3f\xff\xff\xff\x00\x00\x07\xff\xff\xe0\xff\xff\xff\xe0\x01\xff\xff\xff\xf8\x00\x00\x00\x7f\xff\xff\xff", + "\x00\x00\x00\x1f\xff\xc0\x00\x0f\xff\xc0\x00\x00\x1f\xff\xc0\x00\x1f\xc0\x00\x00\x03\xff\x80\x00\x00\x07\xff\xff\xff\xc0\x00\x00\x00\x80\x00\x3f\xfc\x00\xc0\x00\x0f\xff\xff\x00\x00\x06\x00\x3f\xfc\x1e\x00\x1f\xff\xff\xf0\x00\x3e\x0f\xff\xff\xf0\x00\x00\x3f\xff\xf0\x00\x00\x00\x1f\xff\xc0\x00\x00\xff\xff\xff\xf8\x00\xe0\x00\x00\xff\xff\xff\xfe\x00\x00\x0f\xff\xff\xfe\x3f\xff\xff\xfc\x07\xfe\x00\x00\x00\xc0\x00\x7f\xff\xff\xfe\x00\xff\xff\xff\xfc\x07\xff\xff\xe0\x00\x3f\xe3\xff\xff\xc0\x00\x00\x3f\xff", + "\x00\x00\x00\x0f\xff\xc0\x00\x0f\xf8\x00\x00\x00\xff\xfc\x00\x00\x0f\xff\xff\xfe\x00\x00\x00\x03\xff\x00\xff\xfc\x07\xff\xf0\x1f\xff\xfe\x0f\xff\xff\xfd\xff\xff\xff\xf0\x1f\xff\xf0\x00\x07\xf8\xff\xf8\x00\x00\x00\xff\xff\xc0\x3f\xff\xff\xff\x80\x00\x00\x0f\xff\xff\xff\x00\x00\x03\xff\xff\xf0\x00\x07\xff\xff\x00\x00\x3f\xff\xf0\x01\xff\xff\xc0\x01\xff\xff\xff\x00\x3f\xff\xf8\x1f\xff\xff\xfe\x1f\xff\xff\xff\xc0\x00\x00\x7f\xe0\x00\x07\xff\xff\xfe\x00\x00\x00\x03\xe0\x07\xff\xc0\x03\xfc\x00\x07\xff\xff\xff", + "\xff\xff\xff\x00\x00\x03\xc0\x00\x00\x01\xff\xff\xf8\x00\x00\x00\x0f\xff\xff\xff\x00\x00\x0f\x00\x00\xff\xff\xf8\x80\x00\xf8\x00\x0f\xc0\x00\x00\x00\xe0\x00\x00\x00\xff\xff\xff\xf8\x0f\xff\xff\xfe\x00\x00\x18\x00\x00\x7f\xff\xff\xff\x00\x00\x03\xff\xff\xff\x00\x7f\xff\xff\xfc\x00\x03\xc0\x00\x00\x0f\xff\xff\xff\xf0\x00\x07\xff\xff\x80\x01\xff\xff\xff\xe0\x00\x0f\xff\xfe\x07\xff\xff\xf8\x00\xff\xff\xff\xc0\x00\x00\x03\xe0\x00\x07\xff\xf0\x0f\xff\xf0\x00\x00\xff\xff\xf8\x7f\xc0\x03\xc0\x3f\xff\xe0\x00\x00\x00", + + /* These are zero-terminated strings */ + + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" +}; + +static uschar *hashes[] = { + "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", + "3CDF2936DA2FC556BFA533AB1EB59CE710AC80E5", + "19C1E2048FA7393CFBF2D310AD8209EC11D996E5", + "CA775D8C80FAA6F87FA62BECA6CA6089D63B56E5", + "71AC973D0E4B50AE9E5043FF4D615381120A25A0", + "A6B5B9F854CFB76701C3BDDBF374B3094EA49CBA", + "D87A0EE74E4B9AD72E6847C87BDEEB3D07844380", + "1976B8DD509FE66BF09C9A8D33534D4EF4F63BFD", + "5A78F439B6DB845BB8A558E4CEB106CD7B7FF783", + "F871BCE62436C1E280357416695EE2EF9B83695C", + "62B243D1B780E1D31CF1BA2DE3F01C72AEEA0E47", + "1698994A273404848E56E7FDA4457B5900DE1342", + "056F4CDC02791DA7ED1EB2303314F7667518DEEF", + "9FE2DA967BD8441EEA1C32DF68DDAA9DC1FC8E4B", + "73A31777B4ACE9384EFA8BBEAD45C51A71ABA6DD", + "3F9D7C4E2384EDDABFF5DD8A31E23DE3D03F42AC", + "4814908F72B93FFD011135BEE347DE9A08DA838F", + "0978374B67A412A3102C5AA0B10E1A6596FC68EB", + "44AD6CB618BD935460D46D3F921D87B99AB91C1E", + "02DC989AF265B09CF8485640842128DCF95E9F39", + "67507B8D497B35D6E99FC01976D73F54AECA75CF", + "1EAE0373C1317CB60C36A42A867B716039D441F5", + "9C3834589E5BFFAC9F50950E0199B3EC2620BEC8", + "209F7ABC7F3B878EE46CDF3A1FBB9C21C3474F32", + "05FC054B00D97753A9B3E2DA8FBBA3EE808CEF22", + "0C4980EA3A46C757DFBFC5BAA38AC6C8E72DDCE7", + "96A460D2972D276928B69864445BEA353BDCFFD2", + "F3EF04D8FA8C6FA9850F394A4554C080956FA64B", + "F2A31D875D1D7B30874D416C4D2EA6BAF0FFBAFE", + "F4942D3B9E9588DCFDC6312A84DF75D05F111C20", + "310207DF35B014E4676D30806FA34424813734DD", + "4DA1955B2FA7C7E74E3F47D7360CE530BBF57CA3", + "74C4BC5B26FB4A08602D40CCEC6C6161B6C11478", + "0B103CE297338DFC7395F7715EE47539B556DDB6", + "EFC72D99E3D2311CE14190C0B726BDC68F4B0821", + "660EDAC0A8F4CE33DA0D8DBAE597650E97687250", + "FE0A55A988B3B93946A63EB36B23785A5E6EFC3E", + "0CBDF2A5781C59F907513147A0DE3CC774B54BF3", + "663E40FEE5A44BFCB1C99EA5935A6B5BC9F583B0", + "00162134256952DD9AE6B51EFB159B35C3C138C7", + "CEB88E4736E354416E2010FC1061B3B53B81664B", + "A6A2C4B6BCC41DDC67278F3DF4D8D0B9DD7784EF", + "C23D083CD8820B57800A869F5F261D45E02DC55D", + "E8AC31927B78DDEC41A31CA7A44EB7177165E7AB", + "E864EC5DBAB0F9FF6984AB6AD43A8C9B81CC9F9C", + "CFED6269069417A84D6DE2347220F4B858BCD530", + "D9217BFB46C96348722C3783D29D4B1A3FEDA38C", + "DEC24E5554F79697218D317315FA986229CE3350", + "83A099DF7071437BA5495A5B0BFBFEFE1C0EF7F3", + "AA3198E30891A83E33CE3BFA0587D86A197D4F80", + "9B6ACBEB4989CBEE7015C7D515A75672FFDE3442", + "B021EB08A436B02658EAA7BA3C88D49F1219C035", + "CAE36DAB8AEA29F62E0855D9CB3CD8E7D39094B1", + "02DE8BA699F3C1B0CB5AD89A01F2346E630459D7", + "88021458847DD39B4495368F7254941859FAD44B", + "91A165295C666FE85C2ADBC5A10329DAF0CB81A0", + "4B31312EAF8B506811151A9DBD162961F7548C4B", + "3FE70971B20558F7E9BAC303ED2BC14BDE659A62", + "93FB769D5BF49D6C563685954E2AECC024DC02D6", + "BC8827C3E614D515E83DEA503989DEA4FDA6EA13", + "E83868DBE4A389AB48E61CFC4ED894F32AE112AC", + "55C95459CDE4B33791B4B2BCAAF840930AF3F3BD", + "36BB0E2BA438A3E03214D9ED2B28A4D5C578FCAA", + "3ACBF874199763EBA20F3789DFC59572ACA4CF33", + "86BE037C4D509C9202020767D860DAB039CADACE", + "51B57D7080A87394EEC3EB2E0B242E553F2827C9", + "1EFBFA78866315CE6A71E457F3A750A38FACAB41", + "57D6CB41AEEC20236F365B3A490C61D0CFA39611", + "C532CB64B4BA826372BCCF2B4B5793D5B88BB715", + "15833B5631032663E783686A209C6A2B47A1080E", + "D04F2043C96E10CD83B574B1E1C217052CD4A6B2", + "E8882627C64DB743F7DB8B4413DD033FC63BEB20", + "CD2D32286B8867BC124A0AF2236FC74BE3622199", + "019B70D745375091ED5C7B218445EC986D0F5A82", + "E5FF5FEC1DADBAED02BF2DAD4026BE6A96B3F2AF", + "6F4E23B3F2E2C068D13921FE4E5E053FFED4E146", + "25E179602A575C915067566FBA6DA930E97F8678", + "67DED0E68E235C8A523E051E86108EEB757EFBFD", + "AF78536EA83C822796745556D62A3EE82C7BE098", + "64D7AC52E47834BE72455F6C64325F9C358B610D", + "9D4866BAA3639C13E541F250FFA3D8BC157A491F", + "2E258811961D3EB876F30E7019241A01F9517BEC", + "8E0EBC487146F83BC9077A1630E0FB3AB3C89E63", + "CE8953741FFF3425D2311FBBF4AB481B669DEF70", + "789D1D2DAB52086BD90C0E137E2515ED9C6B59B5", + "B76CE7472700DD68D6328B7AA8437FB051D15745", + "F218669B596C5FFB0B1C14BD03C467FC873230A0", + "1FF3BDBE0D504CB0CDFAB17E6C37ABA6B3CFFDED", + "2F3CBACBB14405A4652ED52793C1814FD8C4FCE0", + "982C8AB6CE164F481915AF59AAED9FFF2A391752", + "5CD92012D488A07ECE0E47901D0E083B6BD93E3F", + "69603FEC02920851D4B3B8782E07B92BB2963009", + "3E90F76437B1EA44CF98A08D83EA24CECF6E6191", + "34C09F107C42D990EB4881D4BF2DDDCAB01563AE", + "474BE0E5892EB2382109BFC5E3C8249A9283B03D", + "A04B4F75051786682483252438F6A75BF4705EC6", + "BE88A6716083EB50ED9416719D6A247661299383", + "C67E38717FEE1A5F65EC6C7C7C42AFC00CD37F04", + "959AC4082388E19E9BE5DE571C047EF10C174A8D", + "BAA7AA7B7753FA0ABDC4A541842B5D238D949F0A", + "351394DCEBC08155D100FCD488578E6AE71D0E9C", + "AB8BE94C5AF60D9477EF1252D604E58E27B2A9EE", + "3429EC74A695FDD3228F152564952308AFE0680A", + "907FA46C029BC67EAA8E4F46E3C2A232F85BD122", + "2644C87D1FBBBC0FC8D65F64BCA2492DA15BAAE4", + "110A3EEB408756E2E81ABAF4C5DCD4D4C6AFCF6D", + "CD4FDC35FAC7E1ADB5DE40F47F256EF74D584959", + "8E6E273208AC256F9ECCF296F3F5A37BC8A0F9F7", + "FE0606100BDBC268DB39B503E0FDFE3766185828", + "6C63C3E58047BCDB35A17F74EEBA4E9B14420809", + "BCC2BD305F0BCDA8CF2D478EF9FE080486CB265F", + "CE5223FD3DD920A3B666481D5625B16457DCB5E8", + "948886776E42E4F5FAE1B2D0C906AC3759E3F8B0", + "4C12A51FCFE242F832E3D7329304B11B75161EFB", + "C54BDD2050504D92F551D378AD5FC72C9ED03932", + "8F53E8FA79EA09FD1B682AF5ED1515ECA965604C", + "2D7E17F6294524CE78B33EAB72CDD08E5FF6E313", + "64582B4B57F782C9302BFE7D07F74AA176627A3A", + "6D88795B71D3E386BBD1EB830FB9F161BA98869F", + "86AD34A6463F12CEE6DE9596ABA72F0DF1397FD1", + "7EB46685A57C0D466152DC339C8122548C757ED1", + "E7A98FB0692684054407CC221ABC60C199D6F52A", + "34DF1306662206FD0A5FC2969A4BEEC4EB0197F7", + "56CF7EBF08D10F0CB9FE7EE3B63A5C3A02BCB450", + "3BAE5CB8226642088DA760A6F78B0CF8EDDEA9F1", + "6475DF681E061FA506672C27CBABFA9AA6DDFF62", + "79D81991FA4E4957C8062753439DBFD47BBB277D", + "BAE224477B20302E881F5249F52EC6C34DA8ECEF", + "EDE4DEB4293CFE4138C2C056B7C46FF821CC0ACC", + + "A9993E364706816ABA3E25717850C26C9CD0D89D", + "84983E441C3BD26EBAAE4AA1F95129E5E54670F1" +}; + +static uschar *atest = "34AA973CD4C4DAA4F61EEB2BDBAD27316534016F"; + +int main(void) +{ +sha1 base; +int j; +int i = 0x01020304; +uschar *ctest = (uschar *)(&i); +uschar buffer[256]; +uschar digest[20]; +uschar s[41]; +printf("Checking sha1: %s-endian\n\n", (ctest[0] == 0x04)? "little" : "big"); + +for (i = 0; i < sizeof(tests)/sizeof(uschar *); i ++) + { + printf("%d.\nShould be: %s\n", i, hashes[i]); + sha1_start(&base); + sha1_end(&base, tests[i], (i <= 128)? i : strlen(tests[i]), digest); + for (j = 0; j < 20; j++) sprintf(s+2*j, "%02X", digest[j]); + printf("Computed: %s\n", s); + if (strcmp(s, hashes[i]) != 0) printf("*** No match ***\n"); + printf("\n"); + } + +/* 1 000 000 repetitions of "a" */ + +ctest = malloc(1000000); +memset(ctest, 'a', 1000000); + +printf("1 000 000 repetitions of 'a'\n"); +printf("Should be: %s\n", atest); +sha1_start(&base); +sha1_end(&base, ctest, 1000000, digest); +for (j = 0; j < 20; j++) sprintf(s+2*j, "%02X", digest[j]); +printf("Computed: %s\n", s); +if (strcmp(s, atest) != 0) printf("*** No match ***\n"); + +} +#endif + +/* End of sha1.c */ diff --git a/src/src/auths/spa.c b/src/src/auths/spa.c new file mode 100644 index 000000000..31451344e --- /dev/null +++ b/src/src/auths/spa.c @@ -0,0 +1,352 @@ +/* $Cambridge: exim/src/src/auths/spa.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file, which provides support for Microsoft's Secure Password +Authentication, was contributed by Marc Prud'hommeaux. Tom Kistner added SPA +server support. I (PH) have only modified it in very trivial ways. + +References: + http://www.innovation.ch/java/ntlm.html + http://www.kuro5hin.org/story/2002/4/28/1436/66154 + + * It seems that some systems have existing but different definitions of some + * of the following types. I received a complaint about "int16" causing + * compilation problems. So I (PH) have renamed them all, to be on the safe + * side, by adding 'x' on the end. See auths/auth-spa.h. + + * typedef signed short int16; + * typedef unsigned short uint16; + * typedef unsigned uint32; + * typedef unsigned char uint8; + +07-August-2003: PH: Patched up the code to avoid assert bombouts for stupid + input data. Find appropriate comment by grepping for "PH". +*/ + + +#include "../exim.h" +#include "spa.h" + +/* #define DEBUG_SPA */ + +#ifdef DEBUG_SPA +#define DSPA(x,y,z) debug_printf(x,y,z) +#else +#define DSPA(x,y,z) +#endif + +/* Options specific to the spa authentication mechanism. */ + +optionlist auth_spa_options[] = { + { "client_domain", opt_stringptr, + (void *)(offsetof(auth_spa_options_block, spa_domain)) }, + { "client_password", opt_stringptr, + (void *)(offsetof(auth_spa_options_block, spa_password)) }, + { "client_username", opt_stringptr, + (void *)(offsetof(auth_spa_options_block, spa_username)) }, + { "server_password", opt_stringptr, + (void *)(offsetof(auth_spa_options_block, spa_serverpassword)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_spa_options_count = + sizeof(auth_spa_options)/sizeof(optionlist); + +/* Default private options block for the contidion authentication method. */ + +auth_spa_options_block auth_spa_option_defaults = { + NULL, /* spa_password */ + NULL, /* spa_username */ + NULL, /* spa_domain */ + NULL /* spa_serverpassword (for server side use) */ +}; + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_spa_init(auth_instance *ablock) +{ +auth_spa_options_block *ob = + (auth_spa_options_block *)(ablock->options_block); + +/* The public name defaults to the authenticator name */ + +if (ablock->public_name == NULL) ablock->public_name = ablock->name; + +/* Both username and password must be set for a client */ + +if ((ob->spa_username == NULL) != (ob->spa_password == NULL)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator:\n " + "one of client_username and client_password cannot be set without " + "the other", ablock->name); +ablock->client = ob->spa_username != NULL; + +/* For a server we have just one option */ + +ablock->server = ob->spa_serverpassword != NULL; +} + + + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +#define CVAL(buf,pos) (((unsigned char *)(buf))[pos]) +#define PVAL(buf,pos) ((unsigned)CVAL(buf,pos)) +#define SVAL(buf,pos) (PVAL(buf,pos)|PVAL(buf,(pos)+1)<<8) +#define IVAL(buf,pos) (SVAL(buf,pos)|SVAL(buf,(pos)+2)<<16) + +int +auth_spa_server(auth_instance *ablock, uschar *data) +{ +auth_spa_options_block *ob = (auth_spa_options_block *)(ablock->options_block); +uint8x lmRespData[24]; +uint8x ntRespData[24]; +SPAAuthRequest request; +SPAAuthChallenge challenge; +SPAAuthResponse response; +SPAAuthResponse *responseptr = &response; +uschar msgbuf[2048]; +uschar *clearpass; + +/* send a 334, MS Exchange style, and grab the client's request */ + +if (auth_get_no64_data(&data, US"NTLM supported") != OK) + { + /* something borked */ + return FAIL; + } + +if (spa_base64_to_bits((char *)(&request), (const char *)(data)) < 0) + { + DEBUG(D_auth) debug_printf("auth_spa_server(): bad base64 data in " + "request: %s\n", data); + return FAIL; + } + +/* create a challenge and send it back */ + +spa_build_auth_challenge(&request,&challenge); +spa_bits_to_base64 (msgbuf, (unsigned char*)&challenge, + spa_request_length(&challenge)); + +if (auth_get_no64_data(&data, msgbuf) != OK) + { + /* something borked */ + return FAIL; + } + +/* dump client response */ +if (spa_base64_to_bits((char *)(&response), (const char *)(data)) < 0) + { + DEBUG(D_auth) debug_printf("auth_spa_server(): bad base64 data in " + "response: %s\n", data); + return FAIL; + } + +/* get username and put it in $1 */ + +/*************************************************************** +PH 07-Aug-2003: The original code here was this: + +Ustrcpy(msgbuf, unicodeToString(((char*)responseptr) + + IVAL(&responseptr->uUser.offset,0), + SVAL(&responseptr->uUser.len,0)/2) ); + +However, if the response data is too long, unicodeToString bombs out on +an assertion failure. It uses a 1024 fixed buffer. Bombing out is not a good +idea. It's too messy to try to rework that function to return an error because +it is called from a number of other places in the auth-spa.c module. Instead, +since it is a very small function, I reproduce its code here, with a size check +that causes failure if the size of msgbuf is exceeded. ****/ + + { + int i; + char *p = ((char*)responseptr) + IVAL(&responseptr->uUser.offset,0); + int len = SVAL(&responseptr->uUser.len,0)/2; + + if (len + 1 >= sizeof(msgbuf)) return FAIL; + for (i = 0; i < len; ++i) + { + msgbuf[i] = *p & 0x7f; + p += 2; + } + msgbuf[i] = 0; + } + +/***************************************************************/ + +expand_nstring[1] = msgbuf; +expand_nlength[1] = Ustrlen(msgbuf); +expand_nmax = 1; + +/* look up password */ + +clearpass = expand_string(ob->spa_serverpassword); +if (clearpass == NULL) + { + if (expand_string_forcedfail) + { + DEBUG(D_auth) debug_printf("auth_spa_server(): forced failure while " + "expanding spa_serverpassword\n"); + return FAIL; + } + else + { + DEBUG(D_auth) debug_printf("auth_spa_server(): error while expanding " + "spa_serverpassword: %s\n", expand_string_message); + return DEFER; + } + } + +/* create local hash copy */ + +spa_smb_encrypt (clearpass, challenge.challengeData, lmRespData); +spa_smb_nt_encrypt (clearpass, challenge.challengeData, ntRespData); + +/* compare NT hash (LM may not be available) */ + +if (memcmp(ntRespData, + ((unsigned char*)responseptr)+IVAL(&responseptr->ntResponse.offset,0), + 24) == 0) + /* success. we have a winner. */ + return OK; + +return FAIL; +} + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_spa_client( + auth_instance *ablock, /* authenticator block */ + smtp_inblock *inblock, /* connection inblock */ + smtp_outblock *outblock, /* connection outblock */ + int timeout, /* command timeout */ + uschar *buffer, /* buffer for reading response */ + int buffsize) /* size of buffer */ +{ + auth_spa_options_block *ob = + (auth_spa_options_block *)(ablock->options_block); + SPAAuthRequest request; + SPAAuthChallenge challenge; + SPAAuthResponse response; + char msgbuf[2048]; + char *domain = NULL; + char *username, *password; + + if (smtp_write_command(outblock, FALSE, "AUTH %s\r\n", + ablock->public_name) < 0) + return FAIL_SEND; + + /* wait for the 3XX OK message */ + if (!smtp_read_response(inblock, (uschar *)buffer, buffsize, '3', timeout)) + return FAIL; + + /* Code added by PH to expand the options */ + + username = CS expand_string(ob->spa_username); + if (username == NULL) + { + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", ob->spa_username, ablock->name, + expand_string_message); + return ERROR; + } + + password = CS expand_string(ob->spa_password); + if (password == NULL) + { + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", ob->spa_password, ablock->name, + expand_string_message); + return ERROR; + } + + if (ob->spa_domain != NULL) + { + domain = CS expand_string(ob->spa_domain); + if (domain == NULL) + { + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", ob->spa_domain, ablock->name, + expand_string_message); + return ERROR; + } + } + + /* Original code */ + + DSPA("\n\n%s authenticator: using domain %s\n\n", + ablock->name, domain); + + spa_build_auth_request (&request, CS username, domain); + spa_bits_to_base64 (US msgbuf, (unsigned char*)&request, + spa_request_length(&request)); + + DSPA("\n\n%s authenticator: sending request (%s)\n\n", ablock->name, + msgbuf); + + /* send the encrypted password */ + if (smtp_write_command(outblock, FALSE, "%s\r\n", msgbuf) < 0) + return FAIL_SEND; + + /* wait for the auth challenge */ + if (!smtp_read_response(inblock, (uschar *)buffer, buffsize, '3', timeout)) + return FAIL; + + /* convert the challenge into the challenge struct */ + DSPA("\n\n%s authenticator: challenge (%s)\n\n", + ablock->name, buffer + 4); + spa_base64_to_bits ((char *)(&challenge), (const char *)(buffer + 4)); + + spa_build_auth_response (&challenge, &response, + CS username, CS password); + spa_bits_to_base64 (US msgbuf, (unsigned char*)&response, + spa_request_length(&response)); + DSPA("\n\n%s authenticator: challenge response (%s)\n\n", ablock->name, + msgbuf); + + /* send the challenge response */ + if (smtp_write_command(outblock, FALSE, "%s\r\n", msgbuf) < 0) + return FAIL_SEND; + + /* If we receive a success response from the server, authentication + has succeeded. There may be more data to send, but is there any point + in provoking an error here? */ + if (smtp_read_response(inblock, US buffer, buffsize, '2', timeout)) + return OK; + + /* Not a success response. If errno != 0 there is some kind of transmission + error. Otherwise, check the response code in the buffer. If it starts with + '3', more data is expected. */ + if (errno != 0 || buffer[0] != '3') + return FAIL; + + return FAIL; +} + +/* End of spa.c */ diff --git a/src/src/auths/spa.h b/src/src/auths/spa.h new file mode 100644 index 000000000..b565ca183 --- /dev/null +++ b/src/src/auths/spa.h @@ -0,0 +1,41 @@ +/* $Cambridge: exim/src/src/auths/spa.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file, which provides support for Microsoft's Secure Password +Authentication, was contributed by Marc Prud'hommeaux. */ + + +#include "auth-spa.h" + +/* Private structure for the private options. */ + +typedef struct { + uschar *spa_username; + uschar *spa_password; + uschar *spa_domain; + uschar *spa_serverpassword; +} auth_spa_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_spa_options[]; +extern int auth_spa_options_count; + +/* Block containing default values. */ + +extern auth_spa_options_block auth_spa_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_spa_init(auth_instance *); +extern int auth_spa_server(auth_instance *, uschar *); +extern int auth_spa_client(auth_instance *, smtp_inblock *, + smtp_outblock *, int, uschar *, int); + +/* End of spa.h */ diff --git a/src/src/auths/xtextdecode.c b/src/src/auths/xtextdecode.c new file mode 100644 index 000000000..4e3ec181e --- /dev/null +++ b/src/src/auths/xtextdecode.c @@ -0,0 +1,59 @@ +/* $Cambridge: exim/src/src/auths/xtextdecode.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Decode byte-string in xtext * +*************************************************/ + +/* This function decodes a string in xtextformat as defined in RFC 1891 and +required by the SMTP AUTH extension (RFC 2554). We put the result in a piece of +store of equal length - it cannot be longer than this. Although in general the +result of decoding an xtext may be binary, in the context in which it is used +by Exim (for decoding the value of AUTH on a MAIL command), the result is +expected to be an addr-spec. We therefore add on a terminating zero, for +convenience. + +Arguments: + code points to the coded string, zero-terminated + ptr where to put the pointer to the result, which is in + dynamic store + +Returns: the number of bytes in the result, excluding the final zero; + -1 if the input is malformed +*/ + +int +auth_xtextdecode(uschar *code, uschar **ptr) +{ +register int x; +uschar *result = store_get(Ustrlen(code) + 1); +*ptr = result; + +while ((x = (*code++)) != 0) + { + if (x < 33 || x > 127 || x == '=') return -1; + if (x == '+') + { + register int y; + if (!isxdigit((x = (*code++)))) return -1; + y = ((isdigit(x))? x - '0' : (tolower(x) - 'a' + 10)) << 4; + if (!isxdigit((x = (*code++)))) return -1; + *result++ = y | ((isdigit(x))? x - '0' : (tolower(x) - 'a' + 10)); + } + else *result++ = x; + } + +*result = 0; +return result - *ptr; +} + +/* End of xtextdecode.c */ diff --git a/src/src/auths/xtextencode.c b/src/src/auths/xtextencode.c new file mode 100644 index 000000000..424453dc0 --- /dev/null +++ b/src/src/auths/xtextencode.c @@ -0,0 +1,63 @@ +/* $Cambridge: exim/src/src/auths/xtextencode.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Encode byte-string in xtext * +*************************************************/ + +/* This function encodes a string of bytes, containing any values whatsoever, +as "xtext", as defined in RFC 1891 and required by the SMTP AUTH extension (RFC +2554). + +Arguments: + clear points to the clear text bytes + len the number of bytes to encode + +Returns: a pointer to the zero-terminated xtext string, which + is in working store +*/ + +uschar * +auth_xtextencode(uschar *clear, int len) +{ +uschar *code; +uschar *p = (uschar *)clear; +uschar *pp; +int c = len; +int count = 1; +register int x; + +/* We have to do a prepass to find out how many specials there are, +in order to get the right amount of store. */ + +while (c -- > 0) + count += ((x = *p++) < 33 || x > 127 || x == '+' || x == '=')? 3 : 1; + +pp = code = store_get(count); + +p = (uschar *)clear; +c = len; +while (c-- > 0) + { + if ((x = *p++) < 33 || x > 127 || x == '+' || x == '=') + { + sprintf(CS pp, "+%.02x", x); /* There's always room */ + pp += 3; + } + else *pp++ = x; + } + +*pp = 0; +return code; +} + +/* End of xtextencode.c */ diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile new file mode 100644 index 000000000..395ded42e --- /dev/null +++ b/src/src/lookups/Makefile @@ -0,0 +1,40 @@ +# $Cambridge: exim/src/src/lookups/Makefile,v 1.1 2004/10/07 13:10:01 ph10 Exp $ + +# Make file for building a library containing all the available lookups and +# calling it lookups.a. This is called from the main make file, after cd'ing +# to the lookups subdirectory. When the relevant LOOKUP_ macros are not +# defined, dummy modules get compiled. + +OBJ = cdb.o dbmdb.o dnsdb.o dsearch.o ibase.o ldap.o lsearch.o mysql.o nis.o \ + nisplus.o oracle.o passwd.o pgsql.o testdb.o whoson.o lf_check_file.o \ + lf_quote.o + +lookups.a: $(OBJ) + /bin/rm -f lookups.a + $(AR) lookups.a $(OBJ) + $(RANLIB) $@ + /bin/rm -rf ../drtables.o + +.SUFFIXES: .o .c +.c.o:; $(CC) -c $(CFLAGS) $(INCLUDE) $*.c + +lf_check_file.o: $(HDRS) lf_check_file.c lf_functions.h +lf_quote.o: $(HDRS) lf_quote.c lf_functions.h + +cdb.o: $(HDRS) cdb.c cdb.h +dbmdb.o: $(HDRS) dbmdb.c dbmdb.h +dnsdb.o: $(HDRS) dnsdb.c dnsdb.h +dsearch.o: $(HDRS) dsearch.c dsearch.h +ibase.o: $(HDRS) ibase.c ibase.h +ldap.o: $(HDRS) ldap.c ldap.h +lsearch.o: $(HDRS) lsearch.c lsearch.h +mysql.o: $(HDRS) mysql.c mysql.h +nis.o: $(HDRS) nis.c nis.h +nisplus.o: $(HDRS) nisplus.c nisplus.h +oracle.o: $(HDRS) oracle.c oracle.h +passwd.o: $(HDRS) passwd.c passwd.h +pgsql.o: $(HDRS) pgsql.c pgsql.h +testdb.o: $(HDRS) testdb.c testdb.h +whoson.o: $(HDRS) whoson.c whoson.h + +# End diff --git a/src/src/lookups/README b/src/src/lookups/README new file mode 100644 index 000000000..7a16b83a2 --- /dev/null +++ b/src/src/lookups/README @@ -0,0 +1,156 @@ +$Cambridge: exim/src/src/lookups/README,v 1.1 2004/10/07 13:10:01 ph10 Exp $ + +LOOKUPS +------- + +Each lookup type is implemented by 6 functions, xxx_open(), xxx_check(), +xxx_find(), xxx_close(), xxx_tidy(), and xxx_quote(), where xxx is the name of +the lookup type (e.g. lsearch, dbm, or whatever). + +The xxx_check(), xxx_close(), xxx_tidy(), and xxx_quote() functions need not +exist. There is a table in drtables.c which links the lookup names to the +various sets of functions, with NULL entries for any that don't exist. When +the code for a lookup type is omitted from the binary, all its entries are +NULL. + +One of the fields in the table contains flags describing the kind of lookup. +These are + + lookup_querystyle + +This is a "query style" lookup without a file name, as opposed to the "single +key" style, where the key is associated with a "file name". + + lookup_absfile + +For single key lookups, this means that the file name must be an absolute path. +It is set for lsearch and dbm, but not for NIS, for example. + +When a single-key lookup file is opened, the handle returned by the xxx_open() +function is saved, along with the file name and lookup type, in a tree. The +xxx_close() function is not called when the first lookup is completed. If there +are subsequent lookups of the same type that quote the same file name, +xxx_open() isn't called; instead the cached handle is re-used. + +Exim calls the function search_tidyup() at strategic points in its processing +(e.g. after all routing and directing has been done) and this function walks +the tree and calls the xxx_close() functions for all the cached handles. + +Query-style lookups don't have the concept of an open file that can be cached +this way. Of course, the local code for the lookup can manage its own caching +information in any way it pleases. This means that the xxx_close() +function, even it it exists, is never called. However, if an xxx_tidy() +function exists, it is called once whenever Exim calls search_tidyup(). + +A single-key lookup type may also have an xxx_tidy() function, which is called +by search_tidyup() after all cached handles have been closed via the +xxx_close() function. + +The lookup functions are wrapped into a special store pool (POOL_SEARCH). You +can safely use store_get to allocate store for your handle caching. The store +will be reset after all xxx_tidy() functions are called. + +The function interfaces are as follows: + + +xxx_open() +---------- + +This function is called to initiate the lookup. For things that involve files +it should do a real open; for other kinds of lookup it may do nothing at all. +The arguments are: + + uschar *filename the name of the "file" to open, for non-query-style + lookups; NULL for query-style lookups + uschar **errmsg where to put an error message if there is a problem + +The yield of xxx_open() is a void * value representing the open file or +database. For real files is is normally the FILE or DBM value. For other +kinds of lookup, if there is no natural value to use, (-1) is recommended. +The value should not be NULL (or 0) as that is taken to indicate failure of +the xxx_open() function. For single-key lookups, the handle is cached along +with the filename and type, and may be used for several lookups. + + +xxx_check() +----------- + +If this function exists, it is called after a successful open to check on the +ownership and mode of the file(s). The arguments are: + + void *handle the handle passed back from xxx_open() + uschar *filename the filename passed to xxx_open() + int modemask mode bits that must not be set + int *owners permitted owners of the file(s) + int *owngroups permitted group owners of the file(s) + uschar **errmsg where to put an error message if there is a problem + +In the owners and owngroups vectors, the first element is the count of the +remaining elements. There is a common function that can be called to check +a file: + +int search_check_file(int fd, char *filename, int modemask, int *owners, + int *owngroups, char *type, char **errmsg); + +If fd is >= 0, it is checked using fstat(), and filename is used only in +error messages. If fd is < 0 then filename is checked using stat(). The yield +is zero if all is well, +1 if the mode or uid or gid is wrong, or -1 if the +stat() fails. + +The yield of xxx_check() is TRUE if all is well, FALSE otherwise. The +function should not close the file(s) on failure. That is done from outside +by calling the xxx_close() function. + + +xxx_find() +---------- + +This is called to search an open file/database. The result is OK, FAIL, or +DEFER. The arguments are: + + void *handle the handle passed back from xxx_open() + uschar *filename the filename passed to xxx_open() (NULL for querystyle) + uschar *keyquery the key to look up, or query to process, zero-terminated + int length the length of the key + uschar **result point to the yield, in dynamic store, on success + uschar **errmsg where to put an error message on failure; + this is initially set to "", and should be left + as that for a standard "entry not found" error + BOOL *do_cache the lookup should set this to FALSE when it changes data. + This is TRUE by default. When set to FALSE the cache tree + of the current search handle will be cleaned and the + current result will NOT be cached. Currently the mysql + and pgsql lookups use this when UPDATE/INSERT queries are + executed. + +Even though the key is zero-terminated, the length is passed because in the +common case it has been computed already and is often needed. + + +xxx_close() +----------- + +This is called for single-key lookups when a file is finished with. There is no +yield, and the only argument is the handle that was passed back from +xxx_open(). It is not called for query style lookups. + + +xxx_tidy() +---------- + +This function is called once at the end of search_tidyup() for every lookup +type for which it exists. + + +xxx_quote() +----------- + +This is called by the string expansion code for expansions of the form +${quote_xxx:}, if it exists. If not, the expansion code makes no change +to the string. The function must apply any quoting rules that are specific to +the lookup, and return a pointer to the revised string. If quoting is not +needed, it can return its single argument, which is a uschar *. This function +does NOT use the POOL_SEARCH store, because it's usually never called from any +lookup code. + +**** diff --git a/src/src/lookups/cdb.c b/src/src/lookups/cdb.c new file mode 100644 index 000000000..1daa2bbd4 --- /dev/null +++ b/src/src/lookups/cdb.c @@ -0,0 +1,437 @@ +/* $Cambridge: exim/src/src/lookups/cdb.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * Exim - CDB database lookup module + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 1998 Nigel Metheringham, Planet Online Ltd + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * -------------------------------------------------------------- + * Modified by PH for Exim 4: + * Changed over to using unsigned chars + * Makes use of lf_check_file() for file checking + * -------------------------------------------------------------- + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * + * This code implements Dan Bernstein's Constant DataBase (cdb) spec. + * Information, the spec and sample code for cdb can be obtained from + * http://www.pobox.com/~djb/cdb.html + * + * This implementation borrows some code from Dan Bernstein's + * implementation (which has no license restrictions applied to it). + * This (read-only) implementation is completely contained within + * cdb.[ch] it does *not* link against an external cdb library. + * + * + * There are 2 varients included within this code. One uses MMAP and + * should give better performance especially for multiple lookups on a + * modern machine. The other is the default implementation which is + * used in the case where the MMAP fails or if MMAP was not compiled + * in. this implementation is the same as the original reference cdb + * implementation. The MMAP version is compiled in if the HAVE_MMAP + * preprocessor define is defined - this should be set in the system + * specific os.h file. + * + */ + + +#include "../exim.h" +#include "lf_functions.h" +#include "cdb.h" + +#ifdef HAVE_MMAP +# include +/* Not all implementations declare MAP_FAILED */ +# ifndef MAP_FAILED +# define MAP_FAILED ((void *) -1) +# endif /* MAP_FAILED */ +#endif /* HAVE_MMAP */ + + +#define CDB_HASH_SPLIT 256 /* num pieces the hash table is split into */ +#define CDB_HASH_MASK 255 /* mask to and off split value */ +#define CDB_HASH_ENTRY 8 /* how big each offset it */ +#define CDB_HASH_TABLE (CDB_HASH_SPLIT * CDB_HASH_ENTRY) + +/* State information for cdb databases that are open NB while the db + * is open its contents will not change (cdb dbs are normally updated + * atomically by renaming). However the lifetime of one of these + * state structures should be limited - ie a long running daemon + * that opens one may hit problems.... + */ + +struct cdb_state { + int fileno; + off_t filelen; + uschar *cdb_map; + uschar *cdb_offsets; +}; + +/* 32 bit unsigned type - this is an int on all modern machines */ +typedef unsigned int uint32; + +/* + * cdb_hash() + * Internal function to make hash value */ + +static uint32 +cdb_hash(uschar *buf, unsigned int len) +{ + uint32 h; + + h = 5381; + while (len) { + --len; + h += (h << 5); + h ^= (uint32) *buf++; + } + return h; +} + +/* + * cdb_bread() + * Internal function to read len bytes from disk, coping with oddities */ + +static int +cdb_bread(int fd, + uschar *buf, + int len) +{ + int r; + while (len > 0) { + do + r = Uread(fd,buf,len); + while ((r == -1) && (errno == EINTR)); + if (r == -1) return -1; + if (r == 0) { errno = EIO; return -1; } + buf += r; + len -= r; + } + return 0; +} + +/* + * cdb_bread() + * Internal function to parse 4 byte number (endian independant) */ + +static uint32 +cdb_unpack(uschar *buf) +{ + uint32 num; + num = buf[3]; num <<= 8; + num += buf[2]; num <<= 8; + num += buf[1]; num <<= 8; + num += buf[0]; + return num; +} + +void * +cdb_open(uschar *filename, + uschar **errmsg) +{ + int fileno; + struct cdb_state *cdbp; + struct stat statbuf; + void * mapbuf; + + fileno = Uopen(filename, O_RDONLY, 0); + if (fileno == -1) { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s for cdb lookup", filename); + errno = save_errno; + return NULL; + } + + if (fstat(fileno, &statbuf) == 0) { + /* If this is a valid file, then it *must* be at least + * CDB_HASH_TABLE bytes long */ + if (statbuf.st_size < CDB_HASH_TABLE) { + int save_errno = errno; + *errmsg = string_open_failed(errno, + "%s to short for cdb lookup", + filename); + errno = save_errno; + return NULL; + } + } else { + int save_errno = errno; + *errmsg = string_open_failed(errno, + "fstat(%s) failed - cannot do cdb lookup", + filename); + errno = save_errno; + return NULL; + } + + /* Having got a file open we need the structure to put things in */ + cdbp = store_get(sizeof(struct cdb_state)); + /* store_get() does not return if memory was not available... */ + /* preload the structure.... */ + cdbp->fileno = fileno; + cdbp->filelen = statbuf.st_size; + cdbp->cdb_map = NULL; + cdbp->cdb_offsets = NULL; + + /* if we are allowed to we use mmap here.... */ +#ifdef HAVE_MMAP + mapbuf = mmap(NULL, + statbuf.st_size, + PROT_READ, + MAP_SHARED, + fileno, + 0); + if (mapbuf != MAP_FAILED) { + /* We have an mmap-ed section. Now we can just use it */ + cdbp->cdb_map = mapbuf; + /* The offsets can be set to the same value since they should + * effectively be cached as well + */ + cdbp->cdb_offsets = mapbuf; + + /* Now return the state struct */ + return(cdbp); + } else { + /* If we got here the map failed. Basically we can ignore + * this since we fall back to slower methods.... + * However lets debug log it... + */ + DEBUG(D_lookup) debug_printf("cdb mmap failed - %d\n", errno); + } +#endif /* HAVE_MMAP */ + + /* In this case we have either not got MMAP allowed, or it failed */ + + /* get a buffer to stash the basic offsets in - this should speed + * things up a lot - especially on multiple lookups */ + cdbp->cdb_offsets = store_get(CDB_HASH_TABLE); + + /* now fill the buffer up... */ + if (cdb_bread(fileno, cdbp->cdb_offsets, CDB_HASH_TABLE) == -1) { + /* read of hash table failed, oh dear, oh..... + * time to give up I think.... + * call the close routine (deallocs the memory), and return NULL */ + *errmsg = string_open_failed(errno, + "cannot read header from %s for cdb lookup", + filename); + cdb_close(cdbp); + return NULL; + } + + /* Everything else done - return the cache structure */ + return cdbp; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +BOOL +cdb_check(void *handle, + uschar *filename, + int modemask, + uid_t *owners, + gid_t *owngroups, + uschar **errmsg) +{ + struct cdb_state * cdbp = handle; + return lf_check_file(cdbp->fileno, + filename, + S_IFREG, + modemask, + owners, + owngroups, + "cdb", + errmsg) == 0; +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +int +cdb_find(void *handle, + uschar *filename, + uschar *keystring, + int key_len, + uschar **result, + uschar **errmsg, + BOOL *do_cache) +{ + struct cdb_state * cdbp = handle; + uint32 item_key_len, + item_dat_len, + key_hash, + item_hash, + item_posn, + cur_offset, + end_offset, + hash_offset_entry, + hash_offset, + hash_offlen, + hash_slotnm; + int loop; + + /* Keep picky compilers happy */ + do_cache = do_cache; + + key_hash = cdb_hash((uschar *)keystring, key_len); + + hash_offset_entry = CDB_HASH_ENTRY * (key_hash & CDB_HASH_MASK); + hash_offset = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry); + hash_offlen = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry + 4); + + /* If the offset length is zero this key cannot be in the file */ + if (hash_offlen == 0) { + return FAIL; + } + hash_slotnm = (key_hash >> 8) % hash_offlen; + + /* check to ensure that the file is not corrupt + * if the hash_offset + (hash_offlen * CDB_HASH_ENTRY) is longer + * than the file, then we have problems.... */ + if ((hash_offset + (hash_offlen * CDB_HASH_ENTRY)) > cdbp->filelen) { + *errmsg = string_sprintf("cdb: corrupt cdb file %s (too short)", + filename); + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + + cur_offset = hash_offset + (hash_slotnm * CDB_HASH_ENTRY); + end_offset = hash_offset + (hash_offlen * CDB_HASH_ENTRY); + /* if we are allowed to we use mmap here.... */ +#ifdef HAVE_MMAP + /* make sure the mmap was OK */ + if (cdbp->cdb_map != NULL) { + uschar * cur_pos = cur_offset + cdbp->cdb_map; + uschar * end_pos = end_offset + cdbp->cdb_map; + for (loop = 0; (loop < hash_offlen); ++loop) { + item_hash = cdb_unpack(cur_pos); + cur_pos += 4; + item_posn = cdb_unpack(cur_pos); + cur_pos += 4; + /* if the position is zero then we have a definite miss */ + if (item_posn == 0) + return FAIL; + + if (item_hash == key_hash) { + /* matching hash value */ + uschar * item_ptr = cdbp->cdb_map + item_posn; + item_key_len = cdb_unpack(item_ptr); + item_ptr += 4; + item_dat_len = cdb_unpack(item_ptr); + item_ptr += 4; + /* check key length matches */ + if (item_key_len == key_len) { + /* finally check if key matches */ + if (Ustrncmp(keystring, item_ptr, key_len) == 0) { + /* we have a match.... + * make item_ptr point to data */ + item_ptr += item_key_len; + /* ... and the returned result */ + *result = store_get(item_dat_len + 1); + memcpy(*result, item_ptr, item_dat_len); + (*result)[item_dat_len] = 0; + return OK; + } + } + } + /* handle warp round of table */ + if (cur_pos == end_pos) + cur_pos = cdbp->cdb_map + hash_offset; + } + /* looks like we failed... */ + return FAIL; + } +#endif /* HAVE_MMAP */ + for (loop = 0; (loop < hash_offlen); ++loop) { + uschar packbuf[8]; + if (lseek(cdbp->fileno, (off_t) cur_offset,SEEK_SET) == -1) return DEFER; + if (cdb_bread(cdbp->fileno, packbuf,8) == -1) return DEFER; + item_hash = cdb_unpack(packbuf); + item_posn = cdb_unpack(packbuf + 4); + /* if the position is zero then we have a definite miss */ + if (item_posn == 0) + return FAIL; + + if (item_hash == key_hash) { + /* matching hash value */ + if (lseek(cdbp->fileno, (off_t) item_posn, SEEK_SET) == -1) return DEFER; + if (cdb_bread(cdbp->fileno, packbuf, 8) == -1) return DEFER; + item_key_len = cdb_unpack(packbuf); + /* check key length matches */ + if (item_key_len == key_len) { + /* finally check if key matches */ + uschar * item_key = store_get(key_len); + if (cdb_bread(cdbp->fileno, item_key, key_len) == -1) return DEFER; + if (Ustrncmp(keystring, item_key, key_len) == 0) { + /* Reclaim some store */ + store_reset(item_key); + /* matches - get data length */ + item_dat_len = cdb_unpack(packbuf + 4); + /* then we build a new result string */ + *result = store_get(item_dat_len + 1); + if (cdb_bread(cdbp->fileno, *result, item_dat_len) == -1) + return DEFER; + (*result)[item_dat_len] = 0; + return OK; + } + /* Reclaim some store */ + store_reset(item_key); + } + } + cur_offset += 8; + + /* handle warp round of table */ + if (cur_offset == end_offset) + cur_offset = hash_offset; + } + return FAIL; +} + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +void +cdb_close(void *handle) +{ +struct cdb_state * cdbp = handle; + +#ifdef HAVE_MMAP + if (cdbp->cdb_map) { + munmap(CS cdbp->cdb_map, cdbp->filelen); + if (cdbp->cdb_map == cdbp->cdb_offsets) + cdbp->cdb_offsets = NULL; + } +#endif /* HAVE_MMAP */ + + close(cdbp->fileno); +} + +/* End of lookups/cdb.c */ diff --git a/src/src/lookups/cdb.h b/src/src/lookups/cdb.h new file mode 100644 index 000000000..084cafcf6 --- /dev/null +++ b/src/src/lookups/cdb.h @@ -0,0 +1,59 @@ +/* $Cambridge: exim/src/src/lookups/cdb.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * $Id: cdb.h,v 1.2.2.1 1998/05/29 16:21:36 cvs Exp $ + * + * Exim - CDB database lookup module + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Copyright (c) 1998 Nigel Metheringham, Planet Online Ltd + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * + * This code implements Dan Bernstein's Constant DataBase (cdb) spec. + * Information, the spec and sample code for cdb can be obtained from + * http://www.pobox.com/~djb/cdb.html + * + * This implementation borrows some code from Dan Bernstein's + * implementation (which has no license restrictions applied to it). + * This (read-only) implementation is completely contained within + * cdb.[ch] it does *not* link against an external cdb library. + * + * + * There are 2 varients included within this code. One uses MMAP and + * should give better performance especially for multiple lookups on a + * modern machine. The other is the default implementation which is + * used in the case where the MMAP fails or if MMAP was not compiled + * in. this implementation is the same as the original reference cdb + * implementation. + * + */ + + +/* Functions for reading exim cdb files */ + +extern void *cdb_open(uschar *, uschar **); +extern BOOL cdb_check(void *, uschar *, int, uid_t *, gid_t *, uschar **); +extern int cdb_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern void cdb_close(void *); + +/* End of cdb.h */ diff --git a/src/src/lookups/dbmdb.c b/src/src/lookups/dbmdb.c new file mode 100644 index 000000000..fa6484d34 --- /dev/null +++ b/src/src/lookups/dbmdb.c @@ -0,0 +1,146 @@ +/* $Cambridge: exim/src/src/lookups/dbmdb.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "dbmdb.h" + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ + +void * +dbmdb_open(uschar *filename, uschar **errmsg) +{ +EXIM_DB *yield; +EXIM_DBOPEN(filename, O_RDONLY, 0, &yield); +if (yield == NULL) + { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s as a %s file", filename, EXIM_DBTYPE); + errno = save_errno; + } +return yield; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +/* This needs to know more about the underlying files than is good for it! +We need to know what the real file names are in order to check the owners and +modes. If USE_DB is set, we know it is Berkeley DB, which uses an unmodified +file name. If USE_TDB or USE_GDBM is set, we know it is tdb or gdbm, which do +the same. Otherwise, for safety, we have to check for x.db or x.dir and x.pag. +*/ + +BOOL +dbmdb_check(void *handle, uschar *filename, int modemask, uid_t *owners, + gid_t *owngroups, uschar **errmsg) +{ +int rc; +handle = handle; /* Keep picky compilers happy */ + +#if defined(USE_DB) || defined(USE_TDB) || defined(USE_GDBM) +rc = lf_check_file(-1, filename, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); +#else + { + uschar filebuffer[256]; + sprintf(CS filebuffer, "%.250s.db", filename); + rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); + if (rc < 0) /* stat() failed */ + { + sprintf(CS filebuffer, "%.250s.dir", filename); + rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); + if (rc == 0) /* x.dir was OK */ + { + sprintf(CS filebuffer, "%.250s.pag", filename); + rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups, + "dbm", errmsg); + } + } + } +#endif + +return rc == 0; +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. This function adds 1 to +the keylength in order to include the terminating zero. */ + +int +dbmdb_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +EXIM_DB *d = (EXIM_DB *)handle; +EXIM_DATUM key, data; + +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +do_cache = do_cache; + +EXIM_DATUM_INIT(key); /* Some DBM libraries require datums to */ +EXIM_DATUM_INIT(data); /* be cleared before use. */ +EXIM_DATUM_DATA(key) = CS keystring; +EXIM_DATUM_SIZE(key) = length + 1; + +if (EXIM_DBGET(d, key, data)) + { + *result = string_copyn(US EXIM_DATUM_DATA(data), EXIM_DATUM_SIZE(data)); + EXIM_DATUM_FREE(data); /* Some DBM libraries need a free() call */ + return OK; + } +return FAIL; +} + + + +/************************************************* +* Find entry point - no zero on key * +*************************************************/ + +/* See local README for interface description */ + +int +dbmnz_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +return dbmdb_find(handle, filename, keystring, length-1, result, errmsg, + do_cache); +} + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +void +dbmdb_close(void *handle) +{ +EXIM_DBCLOSE((EXIM_DB *)handle); +} + +/* End of lookups/dbmdb.c */ diff --git a/src/src/lookups/dbmdb.h b/src/src/lookups/dbmdb.h new file mode 100644 index 000000000..90f8b55d7 --- /dev/null +++ b/src/src/lookups/dbmdb.h @@ -0,0 +1,21 @@ +/* $Cambridge: exim/src/src/lookups/dbmdb.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the dbm lookup. Use dbmdb in the code to avoid name +clashes with external library names. */ + +extern void *dbmdb_open(uschar *, uschar **); +extern BOOL dbmdb_check(void *, uschar *, int, uid_t *, gid_t *, uschar **); +extern int dbmdb_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern int dbmnz_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern void dbmdb_close(void *); + +/* End of lookups/dbmdb.h */ diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c new file mode 100644 index 000000000..14fdc5a2e --- /dev/null +++ b/src/src/lookups/dnsdb.c @@ -0,0 +1,223 @@ +/* $Cambridge: exim/src/src/lookups/dnsdb.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "dnsdb.h" + + + +/* Ancient systems (e.g. SunOS4) don't appear to have T_TXT defined in their +header files. */ + +#ifndef T_TXT +#define T_TXT 16 +#endif + +/* Table of recognized DNS record types and their integer values. */ + +static char *type_names[] = { + "a", +#if HAVE_IPV6 + "aaaa", + #ifdef SUPPORT_A6 + "a6", + #endif +#endif + "cname", + "mx", + "ns", + "ptr", + "srv", + "txt" }; + +static int type_values[] = { + T_A, +#if HAVE_IPV6 + T_AAAA, + #ifdef SUPPORT_A6 + T_A6, + #endif +#endif + T_CNAME, + T_MX, + T_NS, + T_PTR, + T_SRV, + T_TXT }; + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +dnsdb_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; /* Ditto */ +return (void *)(-1); /* Any non-0 value */ +} + + + +/************************************************* +* Find entry point for dnsdb * +*************************************************/ + +/* See local README for interface description. */ + +int +dnsdb_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +int rc; +int size = 256; +int ptr = 0; +int type = T_TXT; +uschar *orig_keystring = keystring; +uschar *equals = Ustrchr(keystring, '='); +uschar buffer[256]; + +/* Because we're the working in the search pool, we try to reclaim as much +store as possible later, so we preallocate the result here */ + +uschar *yield = store_get(size); + +dns_record *rr; +dns_answer dnsa; +dns_scan dnss; + +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; +do_cache = do_cache; + +/* If the keystring contains an = this is preceded by a type name. */ + +if (equals != NULL) + { + int i; + int len = equals - keystring; + for (i = 0; i < sizeof(type_names)/sizeof(uschar *); i++) + { + if (len == Ustrlen(type_names[i]) && + strncmpic(keystring, US type_names[i], len) == 0) + { + type = type_values[i]; + break; + } + } + if (i >= sizeof(type_names)/sizeof(uschar *)) + { + *errmsg = US"unsupported DNS record type"; + return DEFER; + } + keystring += len + 1; + } + +/* If the type is PTR, we have to construct the relevant magic lookup +key. This code is now in a separate function. */ + +if (type == T_PTR) + { + dns_build_reverse(keystring, buffer); + keystring = buffer; + } + +DEBUG(D_lookup) debug_printf("dnsdb key: %s\n", keystring); + +/* Initialize the resolver, in case this is the first time it is used +in this run. Then do the lookup and sort out the result. */ + +dns_init(FALSE, FALSE); +rc = dns_lookup(&dnsa, keystring, type, NULL); + +if (rc == DNS_NOMATCH || rc == DNS_NODATA) return FAIL; +if (rc != DNS_SUCCEED) return DEFER; + +for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); + rr != NULL; + rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) + { + if (rr->type != type) continue; + + /* There may be several addresses from an A6 record. Put newlines between + them, just as for between several records. */ + + if (type == T_A || + #ifdef SUPPORT_A6 + type == T_A6 || + #endif + type == T_AAAA) + { + dns_address *da; + for (da = dns_address_from_rr(&dnsa, rr); da != NULL; da = da->next) + { + if (ptr != 0) yield = string_cat(yield, &size, &ptr, US"\n", 1); + yield = string_cat(yield, &size, &ptr, da->address, Ustrlen(da->address)); + } + continue; + } + + /* Other kinds of record just have one piece of data each. */ + + if (ptr != 0) yield = string_cat(yield, &size, &ptr, US"\n", 1); + + if (type == T_TXT) + { + yield = string_cat(yield, &size, &ptr, (uschar *)(rr->data+1), + (rr->data)[0]); + } + else /* T_CNAME, T_MX, T_NS, T_PTR */ + { + uschar s[264]; + uschar *p = (uschar *)(rr->data); + if (type == T_MX) + { + int num; + GETSHORT(num, p); /* pointer is advanced */ + sprintf(CS s, "%d ", num); + yield = string_cat(yield, &size, &ptr, s, Ustrlen(s)); + } + else if (type == T_SRV) + { + int num, weight, port; + GETSHORT(num, p); /* pointer is advanced */ + GETSHORT(weight, p); + GETSHORT(port, p); + sprintf(CS s, "%d %d %d ", num, weight, port); + yield = string_cat(yield, &size, &ptr, s, Ustrlen(s)); + } + rc = dn_expand(dnsa.answer, dnsa.answer + dnsa.answerlen, p, + (DN_EXPAND_ARG4_TYPE)(s), sizeof(s)); + + /* If an overlong response was received, the data will have been + truncated and dn_expand may fail. */ + + if (rc < 0) + { + log_write(0, LOG_MAIN, "host name alias list truncated for %s", + orig_keystring); + break; + } + else yield = string_cat(yield, &size, &ptr, s, Ustrlen(s)); + } + } + +yield[ptr] = 0; +store_reset(yield + ptr + 1); /* Reclaim unused */ +*result = yield; + +return OK; +} + +/* End of lookups/dnsdb.c */ diff --git a/src/src/lookups/dnsdb.h b/src/src/lookups/dnsdb.h new file mode 100644 index 000000000..e68ec63e3 --- /dev/null +++ b/src/src/lookups/dnsdb.h @@ -0,0 +1,16 @@ +/* $Cambridge: exim/src/src/lookups/dnsdb.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the dnsdb lookup */ + +extern void *dnsdb_open(uschar *, uschar **); +extern int dnsdb_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); + +/* End of lookups/dnsdb.h */ diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c new file mode 100644 index 000000000..46e74f71a --- /dev/null +++ b/src/src/lookups/dsearch.c @@ -0,0 +1,123 @@ +/* $Cambridge: exim/src/src/lookups/dsearch.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* The idea for this code came from Matthew Byng-Maddick, but his original has +been heavily reworked a lot for Exim 4 (and it now uses stat() rather than a +directory scan). */ + + +#include "../exim.h" +#include "lf_functions.h" +#include "dsearch.h" + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. We open the directory to test +whether it exists and whether it is searchable. However, we don't need to keep +it open, because the "search" can be done by a call to stat() rather than +actually scanning through the list of files. */ + +void * +dsearch_open(uschar *dirname, uschar **errmsg) +{ +DIR *dp = opendir(CS dirname); +if (dp == NULL) + { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s for directory search", dirname); + errno = save_errno; + return NULL; + } +closedir(dp); +return (void *)(-1); +} + + +/************************************************* +* Check entry point * +*************************************************/ + +/* The handle will always be (void *)(-1), but don't try casting it to an +integer as this gives warnings on 64-bit systems. */ + +BOOL +dsearch_check(void *handle, uschar *filename, int modemask, uid_t *owners, + gid_t *owngroups, uschar **errmsg) +{ +handle = handle; +return lf_check_file(-1, filename, S_IFDIR, modemask, owners, owngroups, + "dsearch", errmsg) == 0; +} + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. We use stat() instead of +scanning the directory, as it is hopefully faster to let the OS do the scanning +for us. */ + +int +dsearch_find(void *handle, uschar *dirname, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +struct stat statbuf; +int save_errno; +uschar filename[PATH_MAX]; + +handle = handle; /* Keep picky compilers happy */ +length = length; +do_cache = do_cache; + +if (Ustrchr(keystring, '/') != 0) + { + *errmsg = string_sprintf("key for dsearch lookup contains a slash: %s", + keystring); + return DEFER; + } + +if (!string_format(filename, sizeof(filename), "%s/%s", dirname, keystring)) + { + *errmsg = US"path name too long"; + return DEFER; + } + +if (Ustat(filename, &statbuf) >= 0) + { + *result = string_copy(keystring); + return OK; + } + +if (errno == ENOENT) return FAIL; + +save_errno = errno; +*errmsg = string_sprintf("%s: stat failed", filename); +errno = save_errno; +return DEFER; +} + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +void +dsearch_close(void *handle) +{ +handle = handle; /* Avoid compiler warning */ +} + +/* End of lookups/dsearch.c */ diff --git a/src/src/lookups/dsearch.h b/src/src/lookups/dsearch.h new file mode 100644 index 000000000..c33538bba --- /dev/null +++ b/src/src/lookups/dsearch.h @@ -0,0 +1,18 @@ +/* $Cambridge: exim/src/src/lookups/dsearch.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the dsearch lookup */ + +extern void *dsearch_open(uschar *, uschar **); +extern BOOL dsearch_check(void *, uschar *, int, uid_t *, gid_t *, uschar **); +extern int dsearch_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern void dsearch_close(void *); + +/* End of lookups/dsearch.h */ diff --git a/src/src/lookups/ibase.c b/src/src/lookups/ibase.c new file mode 100644 index 000000000..4057151ec --- /dev/null +++ b/src/src/lookups/ibase.c @@ -0,0 +1,558 @@ +/* $Cambridge: exim/src/src/lookups/ibase.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* The code in this module was contributed by Ard Biesheuvel. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "ibase.h" + +#ifndef LOOKUP_IBASE +static void dummy(int x) +{ + dummy(x - 1); +} +#else +#include /* The system header */ + +/* Structure and anchor for caching connections. */ + +typedef struct ibase_connection { + struct ibase_connection *next; + uschar *server; + isc_db_handle dbh; + isc_tr_handle transh; +} ibase_connection; + +static ibase_connection *ibase_connections = NULL; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void *ibase_open(uschar * filename, uschar ** errmsg) +{ + return (void *) (1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +void ibase_tidy(void) +{ + ibase_connection *cn; + ISC_STATUS status[20]; + + while ((cn = ibase_connections) != NULL) { + ibase_connections = cn->next; + DEBUG(D_lookup) debug_printf("close Interbase connection: %s\n", + cn->server); + isc_commit_transaction(status, &cn->transh); + isc_detach_database(status, &cn->dbh); + } +} + +static int fetch_field(char *buffer, int buffer_size, XSQLVAR * var) +{ + if (buffer_size < var->sqllen) + return 0; + + switch (var->sqltype & ~1) { + case SQL_VARYING: + strncpy(buffer, &var->sqldata[2], *(short *) var->sqldata); + return *(short *) var->sqldata; + case SQL_TEXT: + strncpy(buffer, var->sqldata, var->sqllen); + return var->sqllen; + case SQL_SHORT: + return sprintf(buffer, "%d", *(short *) var->sqldata); + case SQL_LONG: + return sprintf(buffer, "%ld", *(ISC_LONG *) var->sqldata); +#ifdef SQL_INT64 + case SQL_INT64: + return sprintf(buffer, "%lld", *(ISC_INT64 *) var->sqldata); +#endif + default: + /* not implemented */ + return 0; + } +} + +/************************************************* +* Internal search function * +*************************************************/ + +/* This function is called from the find entry point to do the search for a +single server. + +Arguments: + query the query string + server the server string + resultptr where to store the result + errmsg where to point an error message + defer_break TRUE if no more servers are to be tried after DEFER + +The server string is of the form "host:dbname|user|password". The host can be +host:port. This string is in a nextinlist temporary buffer, so can be +overwritten. + +Returns: OK, FAIL, or DEFER +*/ + +static int +perform_ibase_search(uschar * query, uschar * server, uschar ** resultptr, + uschar ** errmsg, BOOL * defer_break) +{ + isc_stmt_handle stmth = NULL; + XSQLDA *out_sqlda; + XSQLVAR *var; + + char buffer[256]; + ISC_STATUS status[20], *statusp = status; + + int i; + int ssize = 0; + int offset = 0; + int yield = DEFER; + uschar *result = NULL; + ibase_connection *cn; + uschar *server_copy = NULL; + uschar *sdata[3]; + +/* Disaggregate the parameters from the server argument. The order is host, +database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + + for (i = 2; i > 0; i--) { + uschar *pp = Ustrrchr(server, '|'); + if (pp == NULL) { + *errmsg = + string_sprintf("incomplete Interbase server data: %s", + (i == 3) ? server : server_copy); + *defer_break = TRUE; + return DEFER; + } + *pp++ = 0; + sdata[i] = pp; + if (i == 2) + server_copy = string_copy(server); /* sans password */ + } + sdata[0] = server; /* What's left at the start */ + +/* See if we have a cached connection to the server */ + + for (cn = ibase_connections; cn != NULL; cn = cn->next) { + if (Ustrcmp(cn->server, server_copy) == 0) { + break; + } + } + +/* Use a previously cached connection ? */ + + if (cn != NULL) { + static char db_info_options[] = { isc_info_base_level }; + + /* test if the connection is alive */ + if (isc_database_info + (status, &cn->dbh, sizeof(db_info_options), db_info_options, + sizeof(buffer), buffer)) { + /* error occurred: assume connection is down */ + DEBUG(D_lookup) + debug_printf + ("Interbase cleaning up cached connection: %s\n", + cn->server); + isc_detach_database(status, &cn->dbh); + } else { + DEBUG(D_lookup) + debug_printf("Interbase using cached connection for %s\n", + server_copy); + } + } else { + cn = store_get(sizeof(ibase_connection)); + cn->server = server_copy; + cn->dbh = NULL; + cn->transh = NULL; + cn->next = ibase_connections; + ibase_connections = cn; + } + +/* If no cached connection, we must set one up. */ + + if (cn->dbh == NULL || cn->transh == NULL) { + + char *dpb, *p; + short dpb_length; + static char trans_options[] = + { isc_tpb_version3, isc_tpb_read, isc_tpb_read_committed, + isc_tpb_rec_version + }; + + /* Construct the database parameter buffer. */ + dpb = buffer; + *dpb++ = isc_dpb_version1; + *dpb++ = isc_dpb_user_name; + *dpb++ = strlen(sdata[1]); + for (p = sdata[1]; *p;) + *dpb++ = *p++; + *dpb++ = isc_dpb_password; + *dpb++ = strlen(sdata[2]); + for (p = sdata[2]; *p;) + *dpb++ = *p++; + dpb_length = dpb - buffer; + + DEBUG(D_lookup) + debug_printf("new Interbase connection: database=%s user=%s\n", + sdata[0], sdata[1]); + + /* Connect to the database */ + if (isc_attach_database + (status, 0, sdata[0], &cn->dbh, dpb_length, buffer)) { + isc_interprete(buffer, &statusp); + *errmsg = + string_sprintf("Interbase attach() failed: %s", buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + + /* Now start a read-only read-committed transaction */ + if (isc_start_transaction + (status, &cn->transh, 1, &cn->dbh, sizeof(trans_options), + trans_options)) { + isc_interprete(buffer, &statusp); + isc_detach_database(status, &cn->dbh); + *errmsg = + string_sprintf("Interbase start_transaction() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + } + +/* Run the query */ + if (isc_dsql_allocate_statement(status, &cn->dbh, &stmth)) { + isc_interprete(buffer, &statusp); + *errmsg = + string_sprintf("Interbase alloc_statement() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + + out_sqlda = store_get(XSQLDA_LENGTH(1)); + out_sqlda->version = SQLDA_VERSION1; + out_sqlda->sqln = 1; + + if (isc_dsql_prepare + (status, &cn->transh, &stmth, 0, query, 1, out_sqlda)) { + isc_interprete(buffer, &statusp); + store_reset(out_sqlda); + out_sqlda = NULL; + *errmsg = + string_sprintf("Interbase prepare_statement() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + +/* re-allocate the output structure if there's more than one field */ + if (out_sqlda->sqln < out_sqlda->sqld) { + XSQLDA *new_sqlda = store_get(XSQLDA_LENGTH(out_sqlda->sqld)); + if (isc_dsql_describe + (status, &stmth, out_sqlda->version, new_sqlda)) { + isc_interprete(buffer, &statusp); + isc_dsql_free_statement(status, &stmth, DSQL_drop); + store_reset(out_sqlda); + out_sqlda = NULL; + *errmsg = + string_sprintf("Interbase describe_statement() failed: %s", + buffer); + *defer_break = FALSE; + goto IBASE_EXIT; + } + out_sqlda = new_sqlda; + } + +/* allocate storage for every returned field */ + for (i = 0, var = out_sqlda->sqlvar; i < out_sqlda->sqld; i++, var++) { + switch (var->sqltype & ~1) { + case SQL_VARYING: + var->sqldata = + (char *) store_get(sizeof(char) * var->sqllen + 2); + break; + case SQL_TEXT: + var->sqldata = + (char *) store_get(sizeof(char) * var->sqllen); + break; + case SQL_SHORT: + var->sqldata = (char *) store_get(sizeof(short)); + break; + case SQL_LONG: + var->sqldata = (char *) store_get(sizeof(ISC_LONG)); + break; +#ifdef SQL_INT64 + case SQL_INT64: + var->sqldata = (char *) store_get(sizeof(ISC_INT64)); + break; +#endif + case SQL_FLOAT: + var->sqldata = (char *) store_get(sizeof(float)); + break; + case SQL_DOUBLE: + var->sqldata = (char *) store_get(sizeof(double)); + break; +#ifdef SQL_TIMESTAMP + case SQL_DATE: + var->sqldata = (char *) store_get(sizeof(ISC_QUAD)); + break; +#else + case SQL_TIMESTAMP: + var->sqldata = (char *) store_get(sizeof(ISC_TIMESTAMP)); + break; + case SQL_TYPE_DATE: + var->sqldata = (char *) store_get(sizeof(ISC_DATE)); + break; + case SQL_TYPE_TIME: + var->sqldata = (char *) store_get(sizeof(ISC_TIME)); + break; +#endif + } + if (var->sqltype & 1) { + var->sqlind = (short *) store_get(sizeof(short)); + } + } + + /* finally, we're ready to execute the statement */ + if (isc_dsql_execute + (status, &cn->transh, &stmth, out_sqlda->version, NULL)) { + isc_interprete(buffer, &statusp); + *errmsg = + string_sprintf("Interbase describe_statement() failed: %s", + buffer); + isc_dsql_free_statement(status, &stmth, DSQL_drop); + *defer_break = FALSE; + goto IBASE_EXIT; + } + + while (isc_dsql_fetch(status, &stmth, out_sqlda->version, out_sqlda) != + 100L) { + /* check if an error occurred */ + if (status[0] & status[1]) { + isc_interprete(buffer, &statusp); + *errmsg = + string_sprintf("Interbase fetch() failed: %s", buffer); + isc_dsql_free_statement(status, &stmth, DSQL_drop); + *defer_break = FALSE; + goto IBASE_EXIT; + } + + if (result != NULL) + result = string_cat(result, &ssize, &offset, US "\n", 1); + + /* Find the number of fields returned. If this is one, we don't add field + names to the data. Otherwise we do. */ + if (out_sqlda->sqld == 1) { + if (out_sqlda->sqlvar[0].sqlind == NULL || *out_sqlda->sqlvar[0].sqlind != -1) /* NULL value yields nothing */ + result = + string_cat(result, &ssize, &offset, US buffer, + fetch_field(buffer, sizeof(buffer), + &out_sqlda->sqlvar[0])); + } + + else + for (i = 0; i < out_sqlda->sqld; i++) { + int len = fetch_field(buffer, sizeof(buffer), + &out_sqlda->sqlvar[i]); + + result = + string_cat(result, &ssize, &offset, + US out_sqlda->sqlvar[i].aliasname, + out_sqlda->sqlvar[i].aliasname_length); + result = string_cat(result, &ssize, &offset, US "=", 1); + + /* Quote the value if it contains spaces or is empty */ + + if (*out_sqlda->sqlvar[i].sqlind == -1) { /* NULL value */ + result = + string_cat(result, &ssize, &offset, US "\"\"", 2); + } + + else if (buffer[0] == 0 || Ustrchr(buffer, ' ') != NULL) { + int j; + result = + string_cat(result, &ssize, &offset, US "\"", 1); + for (j = 0; j < len; j++) { + if (buffer[j] == '\"' || buffer[j] == '\\') + result = + string_cat(result, &ssize, &offset, + US "\\", 1); + result = + string_cat(result, &ssize, &offset, + US buffer + j, 1); + } + result = + string_cat(result, &ssize, &offset, US "\"", 1); + } else { + result = + string_cat(result, &ssize, &offset, US buffer, + len); + } + result = string_cat(result, &ssize, &offset, US " ", 1); + } + } + +/* If result is NULL then no data has been found and so we return FAIL. +Otherwise, we must terminate the string which has been built; string_cat() +always leaves enough room for a terminating zero. */ + + if (result == NULL) { + yield = FAIL; + *errmsg = US "Interbase: no data found"; + } else { + result[offset] = 0; + store_reset(result + offset + 1); + } + + +/* Get here by goto from various error checks. */ + + IBASE_EXIT: + + if (stmth != NULL) + isc_dsql_free_statement(status, &stmth, DSQL_drop); + +/* Non-NULL result indicates a sucessful result */ + + if (result != NULL) { + *resultptr = result; + return OK; + } else { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return yield; /* FAIL or DEFER */ + } +} + + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The handle and filename +arguments are not used. Loop through a list of servers while the query is +deferred with a retryable error. */ + +int +ibase_find(void *handle, uschar * filename, uschar * query, int length, + uschar ** result, uschar ** errmsg, BOOL *do_cache) +{ + int sep = 0; + uschar *server; + uschar *list = ibase_servers; + uschar buffer[512]; + + /* Keep picky compilers happy */ + do_cache = do_cache; + + DEBUG(D_lookup) debug_printf("Interbase query: %s\n", query); + + while ((server = + string_nextinlist(&list, &sep, buffer, + sizeof(buffer))) != NULL) { + BOOL defer_break = FALSE; + int rc = perform_ibase_search(query, server, result, errmsg, + &defer_break); + if (rc != DEFER || defer_break) + return rc; + } + + if (ibase_servers == NULL) + *errmsg = US "no Interbase servers defined (ibase_servers option)"; + + return DEFER; +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only characters that need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. +Percent, and underscore and not escaped. They are only special in contexts +where they can be wild cards, and this isn't usually the case for data inserted +from messages, since that isn't likely to be treated as a pattern of any kind. +Sadly, MySQL doesn't seem to behave like other programs. If you use something +like "where id="ab\%cd" it does not treat the string as "ab%cd". So you really +can't quote "on spec". + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +uschar *ibase_quote(uschar * s, uschar * opt) +{ + register int c; + int count = 0; + uschar *t = s; + uschar *quoted; + + if (opt != NULL) + return NULL; /* No options recognized */ + + while ((c = *t++) != 0) + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) + count++; + + if (count == 0) + return s; + t = quoted = store_get(Ustrlen(s) + count + 1); + + while ((c = *s++) != 0) { + if (Ustrchr("'", c) != NULL) { + *t++ = '\''; + *t++ = '\''; +/* switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + }*/ + } else + *t++ = c; + } + + *t = 0; + return quoted; +} + +#endif + +/* End of lookups/ibase.c */ diff --git a/src/src/lookups/ibase.h b/src/src/lookups/ibase.h new file mode 100644 index 000000000..37a61f67e --- /dev/null +++ b/src/src/lookups/ibase.h @@ -0,0 +1,18 @@ +/* $Cambridge: exim/src/src/lookups/ibase.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the Interbase lookup functions */ + +extern void *ibase_open(uschar *, uschar **); +extern int ibase_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern void ibase_tidy(void); +extern uschar *ibase_quote(uschar *, uschar *); + +/* End of lookups/ibase.h */ diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c new file mode 100644 index 000000000..aa2e7bfd8 --- /dev/null +++ b/src/src/lookups/ldap.c @@ -0,0 +1,1371 @@ +/* $Cambridge: exim/src/src/lookups/ldap.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Many thanks to Stuart Lynne for contributing the original code for this +driver. Further contibutions from Michael Haardt, Brian Candler, Barry +Pederson, Peter Savitch and Christian Kellner. Particular thanks to Brian for +researching how to handle the different kinds of error. */ + + +#include "../exim.h" +#include "lf_functions.h" +#include "ldap.h" + + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the LDAP headers +available for compiling. Therefore, compile these functions only if LOOKUP_LDAP +is defined. However, some compilers don't like compiling empty modules, so keep +them happy with a dummy when skipping the rest. Make it reference itself to +stop picky compilers complaining that it is unused, and put in a dummy argument +to stop even pickier compilers complaining about infinite loops. */ + +#ifndef LOOKUP_LDAP +static void dummy(int x) { dummy(x-1); } +#else + + +/* Include LDAP headers */ + +#include +#include + + +/* Annoyingly, the different LDAP libraries handle errors in different ways, +and some other things too. There doesn't seem to be an automatic way of +distinguishing between them. Local/Makefile should contain a setting of +LDAP_LIB_TYPE, which in turn causes appropriate macros to be defined for the +different kinds. Those that matter are: + +LDAP_LIB_NETSCAPE +LDAP_LIB_SOLARIS with synonym LDAP_LIB_SOLARIS7 +LDAP_LIB_OPENLDAP2 + +These others may be defined, but are in fact the default, so are not tested: + +LDAP_LIB_UMICHIGAN +LDAP_LIB_OPENLDAP1 +*/ + +#if defined(LDAP_LIB_SOLARIS7) && ! defined(LDAP_LIB_SOLARIS) +#define LDAP_LIB_SOLARIS +#endif + + +/* Just in case LDAP_NO_LIMIT is not defined by some of these libraries. */ + +#ifndef LDAP_NO_LIMIT +#define LDAP_NO_LIMIT 0 +#endif + + +/* Just in case LDAP_DEREF_NEVER is not defined */ + +#ifndef LDAP_DEREF_NEVER +#define LDAP_DEREF_NEVER 0 +#endif + + +/* For libraries without TCP connect timeouts */ + +#ifndef LDAP_X_IO_TIMEOUT_NO_TIMEOUT +#define LDAP_X_IO_TIMEOUT_NO_TIMEOUT (-1) +#endif + + +/* Four types of LDAP search are implemented */ + +#define SEARCH_LDAP_MULTIPLE 0 /* Get attributes from multiple entries */ +#define SEARCH_LDAP_SINGLE 1 /* Get attributes from one entry only */ +#define SEARCH_LDAP_DN 2 /* Get just the DN from one entry */ +#define SEARCH_LDAP_AUTH 3 /* Just checking for authentication */ + +/* In all 4 cases, the DN is left in $ldap_dn (which post-dates the +SEARCH_LDAP_DN lookup). */ + + +/* Structure and anchor for caching connections. */ + +typedef struct ldap_connection { + struct ldap_connection *next; + uschar *host; + uschar *user; + uschar *password; + BOOL bound; + int port; + LDAP *ld; +} LDAP_CONNECTION; + +static LDAP_CONNECTION *ldap_connections = NULL; + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* This is the function that actually does the work. It is called (indirectly +via control_ldap_search) from eldap_find(), eldapauth_find(), eldapdn_find(), +and eldapm_find(), with a difference in the "search_type" argument. + +The case of eldapauth_find() is special in that all it does is do +authentication, returning OK or FAIL as appropriate. This isn't used as a +lookup. Instead, it is called from expand.c as an expansion condition test. + +The DN from a successful lookup is placed in $ldap_dn. This feature postdates +the provision of the SEARCH_LDAP_DN facility for returning just the DN as the +data. + +Arguments: + ldap_url the URL to be looked up + server server host name, when URL contains none + s_port server port, used when URL contains no name + search_type SEARCH_LDAP_MULTIPLE allows values from multiple entries + SEARCH_LDAP_SINGLE allows values from one entry only + SEARCH_LDAP_DN gets the DN from one entry + res set to point at the result (not used for ldapauth) + errmsg set to point a message if result is not OK + defer_break set TRUE if no more servers to be tried after a DEFER + user user name for authentication, or NULL + password password for authentication, or NULL + sizelimit max number of entries returned, or 0 for no limit + timelimit max time to wait, or 0 for no limit + tcplimit max time to connect, or NULL for OS default + deference the dereference option, which is one of + LDAP_DEREF_{NEVER,SEARCHING,FINDING,ALWAYS} + +Returns: OK or FAIL or DEFER + FAIL is given only if a lookup was performed successfully, but + returned no data. +*/ + +static int +perform_ldap_search(uschar *ldap_url, uschar *server, int s_port, int search_type, + uschar **res, uschar **errmsg, BOOL *defer_break, uschar *user, uschar *password, + int sizelimit, int timelimit, int tcplimit, int dereference) +{ +LDAPURLDesc *ludp = NULL; +LDAPMessage *result = NULL; +BerElement *ber; +LDAP_CONNECTION *lcp; + +struct timeval timeout; +struct timeval *timeoutptr = NULL; + +uschar *attr; +uschar **attrp; +uschar *data = NULL; +uschar *dn = NULL; +uschar *host; +uschar **values; +uschar **firstval; +uschar porttext[16]; + +uschar *error1 = NULL; /* string representation of errcode (static) */ +uschar *error2 = NULL; /* error message from the server */ +uschar *matched = NULL; /* partially matched DN */ + +int attr_count = 0; +int error_yield = DEFER; +int msgid; +int rc; +int port; +int ptr = 0; +int rescount = 0; +int size = 0; +BOOL attribute_found = FALSE; +BOOL ldapi = FALSE; + +DEBUG(D_lookup) + debug_printf("perform_ldap_search: ldap%s URL = \"%s\" server=%s port=%d " + "sizelimit=%d timelimit=%d tcplimit=%d\n", + (search_type == SEARCH_LDAP_MULTIPLE)? "m" : + (search_type == SEARCH_LDAP_DN)? "dn" : + (search_type == SEARCH_LDAP_AUTH)? "auth" : "", + ldap_url, server, s_port, sizelimit, timelimit, tcplimit); + +/* Check if LDAP thinks the URL is a valid LDAP URL. We assume that if the LDAP +library that is in use doesn't recognize, say, "ldapi", it will barf here. */ + +if (!ldap_is_ldap_url(CS ldap_url)) + { + *errmsg = string_sprintf("ldap_is_ldap_url: not an LDAP url \"%s\"\n", + ldap_url); + goto RETURN_ERROR_BREAK; + } + +/* Parse the URL */ + +if ((rc = ldap_url_parse(CS ldap_url, &ludp)) != 0) + { + *errmsg = string_sprintf("ldap_url_parse: (error %d) parsing \"%s\"\n", rc, + ldap_url); + goto RETURN_ERROR_BREAK; + } + +/* If the host name is empty, take it from the separate argument, if one is +given. OpenLDAP 2.0.6 sets an unset hostname to "" rather than empty, but +expects NULL later in ldap_init() to mean "default", annoyingly. In OpenLDAP +2.0.11 this has changed (it uses NULL). */ + +if ((ludp->lud_host == NULL || ludp->lud_host[0] == 0) && server != NULL) + { + host = server; + port = s_port; + } +else + { + host = US ludp->lud_host; + if (host != NULL && host[0] == 0) host = NULL; + port = ludp->lud_port; + } + +DEBUG(D_lookup) debug_printf("after ldap_url_parse: host=%s port=%d\n", + host, port); + +if (port == 0) port = LDAP_PORT; /* Default if none given */ +sprintf(CS porttext, ":%d", port); /* For messages */ + +/* If the "host name" is actually a path, we are going to connect using a Unix +socket, regardless of whether "ldapi" was actually specified or not. This means +that a Unix socket can be declared in eldap_default_servers, and "traditional" +LDAP queries using just "ldap" can be used ("ldaps" is similarly overridden). +The path may start with "/" or it may already be escaped as "%2F" if it was +actually declared that way in eldap_default_servers. (I did it that way the +first time.) If the host name is not a path, the use of "ldapi" causes an +error, except in the default case. (But lud_scheme doesn't seem to exist in +older libraries.) */ + +if (host != NULL) + { + if ((host[0] == '/' || Ustrncmp(host, "%2F", 3) == 0)) + { + ldapi = TRUE; + porttext[0] = 0; /* Remove port from messages */ + } + + #if defined LDAP_LIB_OPENLDAP2 + else if (strncmp(ludp->lud_scheme, "ldapi", 5) == 0) + { + *errmsg = string_sprintf("ldapi requires an absolute path (\"%s\" given)", + host); + goto RETURN_ERROR; + } + #endif + } + +/* Count the attributes; we need this later to tell us how to format results */ + +for (attrp = USS ludp->lud_attrs; attrp != NULL && *attrp != NULL; attrp++) + attr_count++; + +/* See if we can find a cached connection to this host. The port is not +relevant for ldapi. The host name pointer is set to NULL if no host was given +(implying the library default), rather than to the empty string. Note that in +this case, there is no difference between ldap and ldapi. */ + +for (lcp = ldap_connections; lcp != NULL; lcp = lcp->next) + { + if ((host == NULL) != (lcp->host == NULL) || + (host != NULL && strcmpic(lcp->host, host) != 0)) + continue; + if (ldapi || port == lcp->port) break; + } + +/* If no cached connection found, we must open a connection to the server. If +the server name is actually an absolute path, we set ldapi=TRUE above. This +requests connection via a Unix socket. However, as far as I know, only OpenLDAP +supports the use of sockets, and the use of ldap_initialize(). */ + +if (lcp == NULL) + { + LDAP *ld; + + + /* --------------------------- OpenLDAP ------------------------ */ + + /* There seems to be a preference under OpenLDAP for ldap_initialize() + instead of ldap_init(), though I have as yet been unable to find + documentation that says this. (OpenLDAP documentation is sparse to + non-existent). So we handle OpenLDAP differently here. Also, support for + ldapi seems to be OpenLDAP-only at present. */ + + #ifdef LDAP_LIB_OPENLDAP2 + + /* We now need an empty string for the default host. Get some store in which + to build a URL for ldap_initialize(). In the ldapi case, it can't be bigger + than (9 + 3*Ustrlen(shost)), whereas in the other cases it can't be bigger + than the host name + "ldaps:///" plus : and a port number, say 20 + the + length of the host name. What we get should accommodate both, easily. */ + + uschar *shost = (host == NULL)? US"" : host; + uschar *init_url = store_get(20 + 3 * Ustrlen(shost)); + uschar *init_ptr; + + /* Handle connection via Unix socket ("ldapi"). We build a basic LDAP URI to + contain the path name, with slashes escaped as %2F. */ + + if (ldapi) + { + int ch; + init_ptr = init_url + 8; + Ustrcpy(init_url, "ldapi://"); + while ((ch = *shost++) != 0) + { + if (ch == '/') + { + Ustrncpy(init_ptr, "%2F", 3); + init_ptr += 3; + } + else *init_ptr++ = ch; + } + *init_ptr = 0; + } + + /* This is not an ldapi call. Just build a URI with the protocol type, host + name, and port. */ + + else + { + init_ptr = Ustrchr(ldap_url, '/'); + Ustrncpy(init_url, ldap_url, init_ptr - ldap_url); + init_ptr = init_url + (init_ptr - ldap_url); + sprintf(CS init_ptr, "//%s:%d/", shost, port); + } + + /* Call ldap_initialize() and check the result */ + + DEBUG(D_lookup) debug_printf("ldap_initialize with URL %s\n", init_url); + rc = ldap_initialize(&ld, CS init_url); + if (rc != LDAP_SUCCESS) + { + *errmsg = string_sprintf("ldap_initialize: (error %d) URL \"%s\"\n", + rc, init_url); + goto RETURN_ERROR; + } + store_reset(init_url); /* Might as well save memory when we can */ + + + /* ------------------------- Not OpenLDAP ---------------------- */ + + /* For libraries other than OpenLDAP, use ldap_init(). */ + + #else /* LDAP_LIB_OPENLDAP2 */ + ld = ldap_init(CS host, port); + #endif /* LDAP_LIB_OPENLDAP2 */ + + /* -------------------------------------------------------------- */ + + + /* Handle failure to initialize */ + + if (ld == NULL) + { + *errmsg = string_sprintf("failed to initialize for LDAP server %s%s - %s", + host, porttext, strerror(errno)); + goto RETURN_ERROR; + } + + /* Set the TCP connect time limit if available. This is something that is + in Netscape SDK v4.1; I don't know about other libraries. */ + + #ifdef LDAP_X_OPT_CONNECT_TIMEOUT + ldap_set_option(ld, LDAP_X_OPT_CONNECT_TIMEOUT, (void *)&tcplimit); + #endif + + /* I could not get TLS to work until I set the version to 3. That version + seems to be the default nowadays. The RFC is dated 1997, so I would hope + that all the LDAP libraries support it. Therefore, if eldap_version hasn't + been set, go for v3 if we can. */ + + if (eldap_version < 0) + { + #ifdef LDAP_VERSION3 + eldap_version = LDAP_VERSION3; + #else + eldap_version = 2; + #endif + } + + #ifdef LDAP_OPT_PROTOCOL_VERSION + ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, (void *)&eldap_version); + #endif + + DEBUG(D_lookup) debug_printf("initialized for LDAP (v%d) server %s%s\n", + eldap_version, host, porttext); + + /* If not using ldapi and TLS is available, set appropriate TLS options: hard + for "ldaps" and soft otherwise. */ + + #ifdef LDAP_OPT_X_TLS + if (!ldapi) + { + int tls_option; + if (strncmp(ludp->lud_scheme, "ldaps", 5) == 0) + { + tls_option = LDAP_OPT_X_TLS_HARD; + DEBUG(D_lookup) debug_printf("LDAP_OPT_X_TLS_HARD set\n"); + } + else + { + tls_option = LDAP_OPT_X_TLS_TRY; + DEBUG(D_lookup) debug_printf("LDAP_OPT_X_TLS_TRY set\n"); + } + ldap_set_option(ld, LDAP_OPT_X_TLS, (void *)&tls_option); + } + #endif /* LDAP_OPT_X_TLS */ + + /* Now add this connection to the chain of cached connections */ + + lcp = store_get(sizeof(LDAP_CONNECTION)); + lcp->host = (host == NULL)? NULL : string_copy(host); + lcp->bound = FALSE; + lcp->user = NULL; + lcp->password = NULL; + lcp->port = port; + lcp->ld = ld; + lcp->next = ldap_connections; + ldap_connections = lcp; + } + +/* Found cached connection */ + +else + { + DEBUG(D_lookup) + debug_printf("re-using cached connection to LDAP server %s%s\n", + host, porttext); + } + +/* Bind with the user/password supplied, or an anonymous bind if these values +are NULL, unless a cached connection is already bound with the same values. */ + +if (!lcp->bound || + (lcp->user == NULL && user != NULL) || + (lcp->user != NULL && user == NULL) || + (lcp->user != NULL && user != NULL && Ustrcmp(lcp->user, user) != 0) || + (lcp->password == NULL && password != NULL) || + (lcp->password != NULL && password == NULL) || + (lcp->password != NULL && password != NULL && + Ustrcmp(lcp->password, password) != 0)) + { + DEBUG(D_lookup) debug_printf("%sbinding with user=%s password=%s\n", + (lcp->bound)? "re-" : "", user, password); + if ((rc = ldap_bind_s(lcp->ld, CS user, CS password, LDAP_AUTH_SIMPLE)) + != LDAP_SUCCESS) + { + /* Invalid credentials when just checking credentials returns FAIL. This + stops any further servers being tried. */ + + if (search_type == SEARCH_LDAP_AUTH && rc == LDAP_INVALID_CREDENTIALS) + { + DEBUG(D_lookup) + debug_printf("Invalid credentials: ldapauth returns FAIL\n"); + error_yield = FAIL; + goto RETURN_ERROR_NOMSG; + } + + /* Otherwise we have a problem that doesn't stop further servers from being + tried. */ + + *errmsg = string_sprintf("failed to bind the LDAP connection to server " + "%s%s - LDAP error %d: %s", host, porttext, rc, ldap_err2string(rc)); + goto RETURN_ERROR; + } + + /* Successful bind */ + + lcp->bound = TRUE; + lcp->user = (user == NULL)? NULL : string_copy(user); + lcp->password = (password == NULL)? NULL : string_copy(password); + } + +/* If we are just checking credentials, return OK. */ + +if (search_type == SEARCH_LDAP_AUTH) + { + DEBUG(D_lookup) debug_printf("Bind succeeded: ldapauth returns OK\n"); + goto RETURN_OK; + } + +/* Before doing the search, set the time and size limits (if given). Here again +the different implementations of LDAP have chosen to do things differently. */ + +#if defined(LDAP_OPT_SIZELIMIT) +ldap_set_option(lcp->ld, LDAP_OPT_SIZELIMIT, (void *)&sizelimit); +ldap_set_option(lcp->ld, LDAP_OPT_TIMELIMIT, (void *)&timelimit); +#else +lcp->ld->ld_sizelimit = sizelimit; +lcp->ld->ld_timelimit = timelimit; +#endif + +/* Similarly for dereferencing aliases. Don't know if this is possible on +an LDAP library without LDAP_OPT_DEREF. */ + +#if defined(LDAP_OPT_DEREF) +ldap_set_option(lcp->ld, LDAP_OPT_DEREF, (void *)&dereference); +#endif + +/* Start the search on the server. */ + +DEBUG(D_lookup) debug_printf("Start search\n"); + +msgid = ldap_search(lcp->ld, ludp->lud_dn, ludp->lud_scope, ludp->lud_filter, + ludp->lud_attrs, 0); + +if (msgid == -1) + { + *errmsg = string_sprintf("ldap search initiation failed"); + goto RETURN_ERROR; + } + +/* Loop to pick up results as they come in, setting a timeout if one was +given. */ + +if (timelimit > 0) + { + timeout.tv_sec = timelimit; + timeout.tv_usec = 0; + timeoutptr = &timeout; + } + +while ((rc = ldap_result(lcp->ld, msgid, 0, timeoutptr, &result)) == + LDAP_RES_SEARCH_ENTRY) + { + LDAPMessage *e; + + DEBUG(D_lookup) debug_printf("ldap_result loop\n"); + + for(e = ldap_first_entry(lcp->ld, result); + e != NULL; + e = ldap_next_entry(lcp->ld, e)) + { + uschar *new_dn; + BOOL insert_space = FALSE; + + DEBUG(D_lookup) debug_printf("LDAP entry loop\n"); + + rescount++; /* Count results */ + + /* Results for multiple entries values are separated by newlines. */ + + if (data != NULL) data = string_cat(data, &size, &ptr, US"\n", 1); + + /* Get the DN from the last result. */ + + new_dn = US ldap_get_dn(lcp->ld, e); + if (new_dn != NULL) + { + if (dn != NULL) + { + #if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2 + ldap_memfree(dn); + #else /* OPENLDAP 1, UMich, Solaris */ + free(dn); + #endif + } + /* Save for later */ + dn = new_dn; + } + + /* If the data we want is actually the DN rather than any attribute values, + (an "ldapdn" search) add it to the data string. If there are multiple + entries, the DNs will be concatenated, but we test for this case below, as + for SEARCH_LDAP_SINGLE, and give an error. */ + + if (search_type == SEARCH_LDAP_DN) /* Do not amalgamate these into one */ + { /* condition, because of the else */ + if (new_dn != NULL) /* below, that's for the first only */ + { + data = string_cat(data, &size, &ptr, new_dn, Ustrlen(new_dn)); + data[ptr] = 0; + attribute_found = TRUE; + } + } + + /* Otherwise, loop through the entry, grabbing attribute values. If there's + only one attribute being retrieved, no attribute name is given, and the + result is not quoted. Multiple values are separated by (comma, space). + If more than one attribute is being retrieved, the data is given as a + sequence of name=value pairs, with the value always in quotes. If there are + multiple values, they are given within the quotes, comma separated. */ + + else for (attr = US ldap_first_attribute(lcp->ld, e, &ber); + attr != NULL; + attr = US ldap_next_attribute(lcp->ld, e, ber)) + { + if (attr[0] != 0) + { + /* Get array of values for this attribute. */ + + if ((firstval = values = USS ldap_get_values(lcp->ld, e, CS attr)) + != NULL) + { + if (attr_count != 1) + { + if (insert_space) + data = string_cat(data, &size, &ptr, US" ", 1); + else + insert_space = TRUE; + data = string_cat(data, &size, &ptr, attr, Ustrlen(attr)); + data = string_cat(data, &size, &ptr, US"=\"", 2); + } + + while (*values != NULL) + { + uschar *value = *values; + int len = Ustrlen(value); + + DEBUG(D_lookup) debug_printf("LDAP attr loop %s:%s\n", attr, value); + + if (values != firstval) + data = string_cat(data, &size, &ptr, US", ", 2); + + /* For multiple attributes, the data is in quotes. We must escape + internal quotes, backslashes, newlines. */ + + if (attr_count != 1) + { + int j; + for (j = 0; j < len; j++) + { + if (value[j] == '\n') + data = string_cat(data, &size, &ptr, US"\\n", 2); + else + { + if (value[j] == '\"' || value[j] == '\\') + data = string_cat(data, &size, &ptr, US"\\", 1); + data = string_cat(data, &size, &ptr, value+j, 1); + } + } + } + + /* For single attributes, copy the value verbatim */ + + else data = string_cat(data, &size, &ptr, value, len); + + /* Move on to the next value */ + + values++; + attribute_found = TRUE; + } + + /* Closing quote at the end of the data for a named attribute. */ + + if (attr_count != 1) + data = string_cat(data, &size, &ptr, US"\"", 1); + + /* Free the values */ + + ldap_value_free(CSS firstval); + } + } + + #if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2 + + /* Netscape and OpenLDAP2 LDAP's attrs are dynamically allocated and need + to be freed. UMich LDAP stores them in static storage and does not require + this. */ + + ldap_memfree(attr); + #endif + } /* End "for" loop for extracting attributes from an entry */ + } /* End "for" loop for extracting entries from a result */ + + /* Free the result */ + + ldap_msgfree(result); + result = NULL; + } /* End "while" loop for multiple results */ + +/* Terminate the dynamic string that we have built and reclaim unused store */ + +if (data != NULL) + { + data[ptr] = 0; + store_reset(data + ptr + 1); + } + +/* Copy the last dn into eldap_dn */ + +if (dn != NULL) + { + eldap_dn = string_copy(dn); + #if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2 + ldap_memfree(dn); + #else /* OPENLDAP 1, UMich, Solaris */ + free(dn); + #endif + } + +DEBUG(D_lookup) debug_printf("search ended by ldap_result yielding %d\n",rc); + +if (rc == 0) + { + *errmsg = US"ldap_result timed out"; + goto RETURN_ERROR; + } + +/* A return code of -1 seems to mean "ldap_result failed internally or couldn't +provide you with a message". Other error states seem to exist where +ldap_result() didn't give us any message from the server at all, leaving result +set to NULL. Apparently, "the error parameters of the LDAP session handle will +be set accordingly". That's the best we can do to retrieve an error status; we +can't use functions like ldap_result2error because they parse a message from +the server, which we didn't get. + +Annoyingly, the different implementations of LDAP have gone for different +methods of handling error codes and generating error messages. */ + +if (rc == -1 || result == NULL) + { + int err; + DEBUG(D_lookup) debug_printf("ldap_result failed\n"); + + #if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2 + ldap_get_option(lcp->ld, LDAP_OPT_ERROR_NUMBER, &err); + *errmsg = string_sprintf("ldap_result failed: %d, %s", + err, ldap_err2string(err)); + + #elif defined LDAP_LIB_NETSCAPE + /* Dubious (surely 'matched' is spurious here?) */ + (void)ldap_get_lderrno(lcp->ld, &matched, &error1); + *errmsg = string_sprintf("ldap_result failed: %s (%s)", error1, matched); + + #else /* UMich LDAP aka OpenLDAP 1.x */ + *errmsg = string_sprintf("ldap_result failed: %d, %s", + lcp->ld->ld_errno, ldap_err2string(lcp->ld->ld_errno)); + #endif + + goto RETURN_ERROR; + } + +/* A return code that isn't -1 doesn't necessarily mean there were no problems +with the search. The message must be an LDAP_RES_SEARCH_RESULT or else it's +something we can't handle. */ + +if (rc != LDAP_RES_SEARCH_RESULT) + { + *errmsg = string_sprintf("ldap_result returned unexpected code %d", rc); + goto RETURN_ERROR; + } + +/* We have a result message from the server. This doesn't yet mean all is well. +We need to parse the message to find out exactly what's happened. */ + + #if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2 + if (ldap_parse_result(lcp->ld, result, &rc, CSS &matched, CSS &error2, NULL, + NULL, 0) < 0) + { + *errmsg = US"ldap_parse_result failed"; + goto RETURN_ERROR; + } + error1 = US ldap_err2string(rc); + +#elif defined LDAP_LIB_NETSCAPE + /* Dubious (it doesn't reference 'result' at all!) */ + rc = ldap_get_lderrno(lcp->ld, &matched, &error1); + +#else /* UMich LDAP aka OpenLDAP 1.x */ + rc = ldap_result2error(lcp->ld, result, 0); + error1 = ldap_err2string(rc); + error2 = lcp->ld->ld_error; + matched = lcp->ld->ld_matched; +#endif + +/* Process the status as follows: + + (1) If we get LDAP_SIZELIMIT_EXCEEDED, just carry on, to return the + truncated result list. + + (2) The range of errors defined by LDAP_NAME_ERROR generally mean "that + object does not, or cannot, exist in the database". For those cases we + fail the lookup. + + (3) All other non-successes here are treated as some kind of problem with + the lookup, so return DEFER (which is the default in error_yield). +*/ + +DEBUG(D_lookup) debug_printf("ldap_parse_result yielded %d: %s\n", + rc, ldap_err2string(rc)); + +if (rc != LDAP_SUCCESS && rc != LDAP_SIZELIMIT_EXCEEDED) + { + *errmsg = string_sprintf("LDAP search failed - error %d: %s%s%s%s%s", + rc, + (error1 != NULL)? error1 : US"", + (error2 != NULL && error2[0] != 0)? US"/" : US"", + (error2 != NULL)? error2 : US"", + (matched != NULL && matched[0] != 0)? US"/" : US"", + (matched != NULL)? matched : US""); + + #if defined LDAP_NAME_ERROR + if (LDAP_NAME_ERROR(rc)) + #elif defined NAME_ERROR /* OPENLDAP1 calls it this */ + if (NAME_ERROR(rc)) + #else + if (rc == LDAP_NO_SUCH_OBJECT) + #endif + + { + DEBUG(D_lookup) debug_printf("lookup failure forced\n"); + error_yield = FAIL; + } + goto RETURN_ERROR; + } + +/* The search succeeded. Check if we have too many results */ + +if (search_type != SEARCH_LDAP_MULTIPLE && rescount > 1) + { + *errmsg = string_sprintf("LDAP search: more than one entry (%d) was returned " + "(filter not specific enough?)", rescount); + goto RETURN_ERROR_BREAK; + } + +/* Check if we have too few (zero) entries */ + +if (rescount < 1) + { + *errmsg = string_sprintf("LDAP search: no results"); + error_yield = FAIL; + goto RETURN_ERROR_BREAK; + } + +/* If an entry was found, but it had no attributes, we behave as if no entries +were found, that is, the lookup failed. */ + +if (!attribute_found) + { + *errmsg = US"LDAP search: found no attributes"; + error_yield = FAIL; + goto RETURN_ERROR; + } + +/* Otherwise, it's all worked */ + +DEBUG(D_lookup) debug_printf("LDAP search: returning: %s\n", data); +*res = data; + +RETURN_OK: +if (result != NULL) ldap_msgfree(result); +ldap_free_urldesc(ludp); +return OK; + +/* Error returns */ + +RETURN_ERROR_BREAK: +*defer_break = TRUE; + +RETURN_ERROR: +DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + +RETURN_ERROR_NOMSG: +if (result != NULL) ldap_msgfree(result); +if (ludp != NULL) ldap_free_urldesc(ludp); + +#if defined LDAP_LIB_OPENLDAP2 + if (error2 != NULL) ldap_memfree(error2); + if (matched != NULL) ldap_memfree(matched); +#endif + +return error_yield; +} + + + +/************************************************* +* Internal search control function * +*************************************************/ + +/* This function is called from eldap_find(), eldapauth_find(), eldapdn_find(), +and eldapm_find() with a difference in the "search_type" argument. It controls +calls to perform_ldap_search() which actually does the work. We call that +repeatedly for certain types of defer in the case when the URL contains no host +name and eldap_default_servers is set to a list of servers to try. This gives +more control than just passing over a list of hosts to ldap_open() because it +handles other kinds of defer as well as just a failure to open. Note that the +URL is defined to contain either zero or one "hostport" only. + +Parameter data in addition to the URL can be passed as preceding text in the +string, as items of the form XXX=yyy. The URL itself can be detected because it +must begin "ldapx://", where x is empty, s, or i. + +Arguments: + ldap_url the URL to be looked up, optionally preceded by other parameter + settings + search_type SEARCH_LDAP_MULTIPLE allows values from multiple entries + SEARCH_LDAP_SINGLE allows values from one entry only + SEARCH_LDAP_DN gets the DN from one entry + res set to point at the result + errmsg set to point a message if result is not OK + +Returns: OK or FAIL or DEFER +*/ + +static int +control_ldap_search(uschar *ldap_url, int search_type, uschar **res, + uschar **errmsg) +{ +BOOL defer_break = FALSE; +int timelimit = LDAP_NO_LIMIT; +int sizelimit = LDAP_NO_LIMIT; +int tcplimit = LDAP_X_IO_TIMEOUT_NO_TIMEOUT; +int dereference = LDAP_DEREF_NEVER; +int sep = 0; +uschar *url = ldap_url; +uschar *p; +uschar *user = NULL; +uschar *password = NULL; +uschar *server, *list; +uschar buffer[512]; + +while (isspace(*url)) url++; + +/* Until the string begins "ldap", search for the other parameter settings that +are recognized. They are of the form NAME=VALUE, with the value being +optionally double-quoted. There must still be a space after it, however. No +NAME has the value "ldap". */ + +while (strncmpic(url, US"ldap", 4) != 0) + { + uschar *name = url; + while (*url != 0 && *url != '=') url++; + if (*url == '=') + { + int namelen; + uschar *value; + namelen = ++url - name; + value = string_dequote(&url); + if (isspace(*url)) + { + if (strncmpic(name, US"USER=", namelen) == 0) user = value; + else if (strncmpic(name, US"PASS=", namelen) == 0) password = value; + else if (strncmpic(name, US"SIZE=", namelen) == 0) sizelimit = Uatoi(value); + else if (strncmpic(name, US"TIME=", namelen) == 0) timelimit = Uatoi(value); + else if (strncmpic(name, US"CONNECT=", namelen) == 0) tcplimit = Uatoi(value) * 1000; + + /* Don't know if all LDAP libraries have LDAP_OPT_DEREF */ + + #ifdef LDAP_OPT_DEREF + else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0) + { + if (strcmpic(value, US"never") == 0) dereference = LDAP_DEREF_NEVER; + else if (strcmpic(value, US"searching") == 0) + dereference = LDAP_DEREF_SEARCHING; + else if (strcmpic(value, US"finding") == 0) + dereference = LDAP_DEREF_FINDING; + if (strcmpic(value, US"always") == 0) dereference = LDAP_DEREF_ALWAYS; + } + #else + else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0) + { + *errmsg = string_sprintf("LDAP_OP_DEREF not defined in this LDAP " + "library - cannot use \"dereference\""); + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + + #endif + + else + { + *errmsg = + string_sprintf("unknown parameter \"%.*s\" precedes LDAP URL", + namelen, name); + DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); + return DEFER; + } + while (isspace(*url)) url++; + continue; + } + } + *errmsg = US"malformed parameter setting precedes LDAP URL"; + DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); + return DEFER; + } + +/* If user is set, de-URL-quote it. Some LDAP libraries do this for themselves, +but it seems that not all behave like this. The DN for the user is often the +result of ${quote_ldap_dn:...} quoting, which does apply URL quoting, because +that is needed when the DN is used as a base DN in a query. Sigh. This is all +far too complicated. */ + +if (user != NULL) + { + uschar *s; + uschar *t = user; + for (s = user; *s != 0; s++) + { + int c, d; + if (*s == '%' && isxdigit(c=s[1]) && isxdigit(d=s[2])) + { + c = tolower(c); + d = tolower(d); + *t++ = + (((c >= 'a')? (10 + c - 'a') : c - '0') << 4) | + ((d >= 'a')? (10 + d - 'a') : d - '0'); + s += 2; + } + else *t++ = *s; + } + *t = 0; + } + +DEBUG(D_lookup) + debug_printf("LDAP parameters: user=%s pass=%s size=%d time=%d connect=%d " + "dereference=%d\n", user, password, sizelimit, timelimit, tcplimit, + dereference); + +/* If the request is just to check authentication, some credentials must +be given. The password must not be empty because LDAP binds with an empty +password are considered anonymous, and will succeed on most installations. */ + +if (search_type == SEARCH_LDAP_AUTH) + { + if (user == NULL || password == NULL) + { + *errmsg = US"ldapauth lookups must specify the username and password"; + return DEFER; + } + if (password[0] == 0) + { + DEBUG(D_lookup) debug_printf("Empty password: ldapauth returns FAIL\n"); + return FAIL; + } + } + +/* Check for valid ldap url starters */ + +p = url + 4; +if (tolower(*p) == 's' || tolower(*p) == 'i') p++; +if (Ustrncmp(p, "://", 3) != 0) + { + *errmsg = string_sprintf("LDAP URL does not start with \"ldap://\", " + "\"ldaps://\", or \"ldapi://\" (it starts with \"%.16s...\")", url); + DEBUG(D_lookup) debug_printf("LDAP query error: %s\n", *errmsg); + return DEFER; + } + +/* No default servers, or URL contains a server name: just one attempt */ + +if (eldap_default_servers == NULL || p[3] != '/') + { + return perform_ldap_search(url, NULL, 0, search_type, res, errmsg, + &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference); + } + +/* Loop through the default servers until OK or FAIL */ + +list = eldap_default_servers; +while ((server = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) + { + int rc; + int port = 0; + uschar *colon = Ustrchr(server, ':'); + if (colon != NULL) + { + *colon = 0; + port = Uatoi(colon+1); + } + rc = perform_ldap_search(url, server, port, search_type, res, errmsg, + &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference); + if (rc != DEFER || defer_break) return rc; + } + +return DEFER; +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The different kinds of search +are handled by a common function, with a flag to differentiate between them. +The handle and filename arguments are not used. */ + +int +eldap_find(void *handle, uschar *filename, uschar *ldap_url, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_SINGLE, result, errmsg)); +} + +int +eldapm_find(void *handle, uschar *filename, uschar *ldap_url, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_MULTIPLE, result, errmsg)); +} + +int +eldapdn_find(void *handle, uschar *filename, uschar *ldap_url, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_DN, result, errmsg)); +} + +int +eldapauth_find(void *handle, uschar *filename, uschar *ldap_url, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +/* Keep picky compilers happy */ +do_cache = do_cache; +return(control_ldap_search(ldap_url, SEARCH_LDAP_AUTH, result, errmsg)); +} + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +eldap_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. +Make sure that eldap_dn does not refer to reclaimed or worse, freed store */ + +void +eldap_tidy(void) +{ +LDAP_CONNECTION *lcp = NULL; +eldap_dn = NULL; + +while ((lcp = ldap_connections) != NULL) + { + DEBUG(D_lookup) debug_printf("unbind LDAP connection to %s:%d\n", lcp->host, + lcp->port); + ldap_unbind(lcp->ld); + ldap_connections = lcp->next; + } +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* LDAP quoting is unbelievably messy. For a start, two different levels of +quoting have to be done: LDAP quoting, and URL quoting. The current +specification is the result of a suggestion by Brian Candler. It recognizes +two separate cases: + +(1) For text that appears in a search filter, the following escapes are + required (see RFC 2254): + + * -> \2A + ( -> \28 + ) -> \29 + \ -> \5C + NULL -> \00 + + Then the entire filter text must be URL-escaped. This kind of quoting is + implemented by ${quote_ldap:....}. Note that we can never have a NULL + in the input string, because that's a terminator. + +(2) For a DN that is part of a URL (i.e. the base DN), the characters + + , + " \ < > ; + + must be quoted by backslashing. See RFC 2253. Leading and trailing spaces + must be escaped, as must a leading #. Then the string must be URL-quoted. + This type of quoting is implemented by ${quote_ldap_dn:....}. + +For URL quoting, the only characters that need not be quoted are the +alphamerics and + + ! $ ' ( ) * + - . _ + +All the others must be hexified and preceded by %. This includes the +backslashes used for LDAP quoting. + +For a DN that is given in the USER parameter for authentication, we need the +same initial quoting as (2) but in this case, the result must NOT be +URL-escaped, because it isn't a URL. The way this is handled is by +de-URL-quoting the text when processing the USER parameter in +control_ldap_search() above. That means that the same quote operator can be +used. This has the additional advantage that spaces in the DN won't cause +parsing problems. For example: + + USER=cn=${quote_ldap_dn:$1},%20dc=example,%20dc=com + +should be safe if there are spaces in $1. + + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + only "dn" is recognized + +Returns: the processed string or NULL for a bad option +*/ + + + +/* The characters in this string, together with alphanumerics, never need +quoting in any way. */ + +#define ALWAYS_LITERAL "!$'-._" + +/* The special characters in this string do not need to be URL-quoted. The set +is a bit larger than the general literals. */ + +#define URL_NONQUOTE ALWAYS_LITERAL "()*+" + +/* The following macros define the characters that are quoted by quote_ldap and +quote_ldap_dn, respectively. */ + +#define LDAP_QUOTE "*()\\" +#define LDAP_DN_QUOTE ",+\"\\<>;" + + + +uschar * +eldap_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +int len = 0; +BOOL dn = FALSE; +uschar *t = s; +uschar *quoted; + +/* Test for a DN quotation. */ + +if (opt != NULL) + { + if (Ustrcmp(opt, "dn") != 0) return NULL; /* No others recognized */ + dn = TRUE; + } + +/* Compute how much extra store we need for the string. This doesn't have to be +exact as long as it isn't an underestimate. The worst case is the addition of 5 +extra bytes for a single character. This occurs for certain characters in DNs, +where, for example, < turns into %5C%3C. For simplicity, we just add 5 for each +possibly escaped character. The really fast way would be just to test for +non-alphanumerics, but it is probably better to spot a few others that are +never escaped, because if there are no specials at all, we can avoid copying +the string. */ + +while ((c = *t++) != 0) + { + len++; + if (!isalnum(c) && Ustrchr(ALWAYS_LITERAL, c) == NULL) count += 5; + } +if (count == 0) return s; + +/* Get sufficient store to hold the quoted string */ + +t = quoted = store_get(len + count + 1); + +/* Handle plain quote_ldap */ + +if (!dn) + { + while ((c = *s++) != 0) + { + if (!isalnum(c)) + { + if (Ustrchr(LDAP_QUOTE, c) != NULL) + { + sprintf(CS t, "%%5C%02X", c); /* e.g. * => %5C2A */ + t += 5; + continue; + } + if (Ustrchr(URL_NONQUOTE, c) == NULL) /* e.g. ] => %5D */ + { + sprintf(CS t, "%%%02X", c); + t += 3; + continue; + } + } + *t++ = c; /* unquoted character */ + } + } + +/* Handle quote_ldap_dn */ + +else + { + uschar *ss = s + len; + + /* Find the last char before any trailing spaces */ + + while (ss > s && ss[-1] == ' ') ss--; + + /* Quote leading spaces and sharps */ + + for (; s < ss; s++) + { + if (*s != ' ' && *s != '#') break; + sprintf(CS t, "%%5C%%%02X", *s); + t += 6; + } + + /* Handle the rest of the string, up to the trailing spaces */ + + while (s < ss) + { + c = *s++; + if (!isalnum(c)) + { + if (Ustrchr(LDAP_DN_QUOTE, c) != NULL) + { + Ustrncpy(t, "%5C", 3); /* insert \ where needed */ + t += 3; /* fall through to check URL */ + } + if (Ustrchr(URL_NONQUOTE, c) == NULL) /* e.g. ] => %5D */ + { + sprintf(CS t, "%%%02X", c); + t += 3; + continue; + } + } + *t++ = c; /* unquoted character, or non-URL quoted after %5C */ + } + + /* Handle the trailing spaces */ + + while (*ss++ != 0) + { + Ustrncpy(t, "%5C%20", 6); + t += 6; + } + } + +/* Terminate the new string and return */ + +*t = 0; +return quoted; +} + +#endif /* LOOKUP_LDAP */ + +/* End of lookups/ldap.c */ diff --git a/src/src/lookups/ldap.h b/src/src/lookups/ldap.h new file mode 100644 index 000000000..894a263b7 --- /dev/null +++ b/src/src/lookups/ldap.h @@ -0,0 +1,24 @@ +/* $Cambridge: exim/src/src/lookups/ldap.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the ldap lookups */ + +extern void *eldap_open(uschar *, uschar **); +extern int eldap_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern int eldapauth_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern int eldapdn_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern int eldapm_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern void eldap_tidy(void); +extern uschar *eldap_quote(uschar *, uschar *); + +/* End of lookups/ldap.h */ diff --git a/src/src/lookups/lf_check_file.c b/src/src/lookups/lf_check_file.c new file mode 100644 index 000000000..7ccb0c591 --- /dev/null +++ b/src/src/lookups/lf_check_file.c @@ -0,0 +1,115 @@ +/* $Cambridge: exim/src/src/lookups/lf_check_file.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "lf_functions.h" + + + +/************************************************* +* Check a file's credentials * +*************************************************/ + +/* fstat can normally be expected to work on an open file, but there are some +NFS states where it may not. + +Arguments: + fd an open file descriptor or -1 + filename a file name if fd is -1 + s_type type of file (S_IFREG or S_IFDIR) + modemask a mask specifying mode bits that must *not* be set + owners NULL or a list of of allowable uids, count in the first item + owngroups NULL or a list of allowable gids, count in the first item + type name of lookup type for putting in error message + errmsg where to put an error message + +Returns: -1 stat() or fstat() failed + 0 OK + +1 something didn't match + +Side effect: sets errno to ERRNO_BADUGID, ERRNO_NOTREGULAR or ERRNO_BADMODE for + bad uid/gid, not a regular file, or bad mode; otherwise leaves it + to what fstat set it to. +*/ + +int +lf_check_file(int fd, uschar *filename, int s_type, int modemask, uid_t *owners, + gid_t *owngroups, char *type, uschar **errmsg) +{ +int i; +struct stat statbuf; + +if ((fd >= 0 && fstat(fd, &statbuf) != 0) || + (fd < 0 && Ustat(filename, &statbuf) != 0)) + { + int save_errno = errno; + *errmsg = string_sprintf("%s: stat failed", filename); + errno = save_errno; + return -1; + } + +if ((statbuf.st_mode & S_IFMT) != s_type) + { + if (s_type == S_IFREG) + { + *errmsg = string_sprintf("%s is not a regular file (%s lookup)", + filename, type); + errno = ERRNO_NOTREGULAR; + } + else + { + *errmsg = string_sprintf("%s is not a directory (%s lookup)", + filename, type); + errno = ERRNO_NOTDIRECTORY; + } + return +1; + } + +if ((statbuf.st_mode & modemask) != 0) + { + *errmsg = string_sprintf("%s (%s lookup): file mode %.4o should not contain " + "%.4o", filename, type, statbuf.st_mode & 07777, + statbuf.st_mode & modemask); + errno = ERRNO_BADMODE; + return +1; + } + +if (owners != NULL) + { + BOOL uid_ok = FALSE; + for (i = 1; i <= (int)owners[0]; i++) + if (owners[i] == statbuf.st_uid) { uid_ok = TRUE; break; } + if (!uid_ok) + { + *errmsg = string_sprintf("%s (%s lookup): file has wrong owner", filename, + type); + errno = ERRNO_BADUGID; + return +1; + } + } + +if (owngroups != NULL) + { + BOOL gid_ok = FALSE; + for (i = 1; i <= (int)owngroups[0]; i++) + if (owngroups[i] == statbuf.st_gid) { gid_ok = TRUE; break; } + if (!gid_ok) + { + *errmsg = string_sprintf("%s (%s lookup): file has wrong group", filename, + type); + errno = ERRNO_BADUGID; + return +1; + } + } + +return 0; +} + +/* End of lf_check_file.c */ diff --git a/src/src/lookups/lf_functions.h b/src/src/lookups/lf_functions.h new file mode 100644 index 000000000..1f896a204 --- /dev/null +++ b/src/src/lookups/lf_functions.h @@ -0,0 +1,16 @@ +/* $Cambridge: exim/src/src/lookups/lf_functions.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the functions that are shared by the lookups */ + +extern int lf_check_file(int, uschar *, int, int, uid_t *, gid_t *, char *, + uschar **); +extern uschar *lf_quote(uschar *, uschar *, int, uschar *, int *, int *); + +/* End of lf_functions.h */ diff --git a/src/src/lookups/lf_quote.c b/src/src/lookups/lf_quote.c new file mode 100644 index 000000000..9cc291723 --- /dev/null +++ b/src/src/lookups/lf_quote.c @@ -0,0 +1,67 @@ +/* $Cambridge: exim/src/src/lookups/lf_quote.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "lf_functions.h" + + +/************************************************* +* Add string to result, quoting if necessary * +*************************************************/ + +/* This function is called by some lookups that create name=value result +strings, to handle the quoting of the data. It adds "name=" to the result, +followed by appropriately quoted data, followed by a single space. + +Arguments: + name the field name + value the data value + vlength the data length + result the result pointer + asize points to the size variable + aoffset points to the offset variable + +Returns: the result pointer (possibly updated) +*/ + +uschar * +lf_quote(uschar *name, uschar *value, int vlength, uschar *result, int *asize, + int *aoffset) +{ +result = string_append(result, asize, aoffset, 2, name, US"="); + +/* NULL is handled as an empty string */ + +if (value == NULL) value = US""; + +/* Quote the value if it is empty, contains white space, or starts with a quote +character. */ + +if (value[0] == 0 || Ustrpbrk(value, " \t\n\r") != NULL || value[0] == '\"') + { + int j; + result = string_cat(result, asize, aoffset, US"\"", 1); + for (j = 0; j < vlength; j++) + { + if (value[j] == '\"' || value[j] == '\\') + result = string_cat(result, asize, aoffset, US"\\", 1); + result = string_cat(result, asize, aoffset, US value+j, 1); + } + result = string_cat(result, asize, aoffset, US"\"", 1); + } +else + { + result = string_cat(result, asize, aoffset, US value, vlength); + } + +return string_cat(result, asize, aoffset, US" ", 1); +} + +/* End of lf_quote.c */ diff --git a/src/src/lookups/lsearch.c b/src/src/lookups/lsearch.c new file mode 100644 index 000000000..56b12783e --- /dev/null +++ b/src/src/lookups/lsearch.c @@ -0,0 +1,401 @@ +/* $Cambridge: exim/src/src/lookups/lsearch.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "lsearch.h" + +/* Codes for the different kinds of lsearch that are supported */ + +enum { + LSEARCH_PLAIN, /* Literal keys */ + LSEARCH_WILD, /* Wild card keys, expanded */ + LSEARCH_NWILD, /* Wild card keys, not expanded */ + LSEARCH_IP /* IP addresses and networks */ +}; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ + +void * +lsearch_open(uschar *filename, uschar **errmsg) +{ +FILE *f = Ufopen(filename, "rb"); +if (f == NULL) + { + int save_errno = errno; + *errmsg = string_open_failed(errno, "%s for linear search", filename); + errno = save_errno; + return NULL; + } +return f; +} + + + +/************************************************* +* Check entry point * +*************************************************/ + +BOOL +lsearch_check(void *handle, uschar *filename, int modemask, uid_t *owners, + gid_t *owngroups, uschar **errmsg) +{ +return lf_check_file(fileno((FILE *)handle), filename, S_IFREG, modemask, + owners, owngroups, "lsearch", errmsg) == 0; +} + + + +/************************************************* +* Internal function for the various lsearches * +*************************************************/ + +/* See local README for interface description, plus: + +Extra argument: + + type one of the values LSEARCH_PLAIN, LSEARCH_WILD, LSEARCH_NWILD, or + LSEARCH_IP + +There is some messy logic in here to cope with very long data lines that do not +fit into the fixed sized buffer. Most of the time this will never be exercised, +but people do occasionally do weird things. */ + +static int +internal_lsearch_find(void *handle, uschar *filename, uschar *keystring, + int length, uschar **result, uschar **errmsg, int type) +{ +FILE *f = (FILE *)handle; +BOOL last_was_eol = TRUE; +BOOL this_is_eol = TRUE; +int old_pool = store_pool; +void *reset_point = NULL; +uschar buffer[4096]; + +/* Wildcard searches may use up some store, because of expansions. We don't +want them to fill up our search store. What we do is set the pool to the main +pool and get a point to reset to later. Wildcard searches could also issue +lookups, but internal_search_find will take care of that, and the cache will be +safely stored in the search pool again. */ + +if(type == LSEARCH_WILD || type == LSEARCH_NWILD) + { + store_pool = POOL_MAIN; + reset_point = store_get(0); + } + +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; + +rewind(f); +for (last_was_eol = TRUE; + Ufgets(buffer, sizeof(buffer), f) != NULL; + last_was_eol = this_is_eol) + { + int ptr, size; + int p = Ustrlen(buffer); + int linekeylength; + uschar *yield; + uschar *s = buffer; + + /* Check whether this the final segment of a line. If it follows an + incomplete part-line, skip it. */ + + this_is_eol = p > 0 && buffer[p-1] == '\n'; + if (!last_was_eol) continue; + + /* We now have the start of a physical line. If this is a final line segment, + remove trailing white space. */ + + if (this_is_eol) + { + while (p > 0 && isspace((uschar)buffer[p-1])) p--; + buffer[p] = 0; + } + + /* If the buffer is empty it might be (a) a complete empty line, or (b) the + start of a line that begins with so much white space that it doesn't all fit + in the buffer. In both cases we want to skip the entire physical line. + + If the buffer begins with # it is a comment line; if it begins with white + space it is a logical continuation; again, we want to skip the entire + physical line. */ + + if (buffer[0] == 0 || buffer[0] == '#' || isspace(buffer[0])) continue; + + /* We assume that they key will fit in the buffer. If the key starts with ", + read it as a quoted string. We don't use string_dequote() because that uses + new store for the result, and we may be doing this many times in a long file. + We know that the dequoted string must be shorter than the original, because + we are removing the quotes, and also any escape sequences always turn two or + more characters into one character. Therefore, we can store the new string in + the same buffer. */ + + if (*s == '\"') + { + uschar *t = s++; + while (*s != 0 && *s != '\"') + { + if (*s == '\\') *t++ = string_interpret_escape(&s); + else *t++ = *s; + s++; + } + if (*s != 0) s++; /* Past terminating " */ + linekeylength = t - buffer; + } + + /* Otherwise it is terminated by a colon or white space */ + + else + { + while (*s != 0 && *s != ':' && !isspace(*s)) s++; + linekeylength = s - buffer; + } + + /* The matching test depends on which kind of lsearch we are doing */ + + switch(type) + { + /* A plain lsearch treats each key as a literal */ + + case LSEARCH_PLAIN: + if (linekeylength != length || strncmpic(buffer, keystring, length) != 0) + continue; + break; /* Key matched */ + + /* A wild lsearch treats each key as a possible wildcarded string; no + expansion is done for nwildlsearch. */ + + case LSEARCH_WILD: + case LSEARCH_NWILD: + { + int rc; + int save = buffer[linekeylength]; + uschar *list = buffer; + buffer[linekeylength] = 0; + rc = match_isinlist(keystring, + &list, + UCHAR_MAX+1, /* Single-item list */ + NULL, /* No anchor */ + NULL, /* No caching */ + MCL_STRING + ((type == LSEARCH_WILD)? 0:MCL_NOEXPAND), + TRUE, /* Caseless */ + NULL); + buffer[linekeylength] = save; + if (rc == FAIL) continue; + if (rc == DEFER) return DEFER; + } + break; /* Key matched */ + + /* Compare an ip address against a list of network/ip addresses. We have to + allow for the "*" case specially. */ + + case LSEARCH_IP: + if (linekeylength == 1 && buffer[0] == '*') + { + if (length != 1 || keystring[0] != '*') continue; + } + else if (length == 1 && keystring[0] == '*') continue; + else + { + int maskoffset; + int save = buffer[linekeylength]; + buffer[linekeylength] = 0; + if (!string_is_ip_address(buffer, &maskoffset) || + !host_is_in_net(keystring, buffer, maskoffset)) continue; + buffer[linekeylength] = save; + } + break; /* Key matched */ + } + + /* The key has matched. Skip spaces after the key, and allow an optional + colon after the spaces. This is an odd specification, but it's for + compatibility. */ + + while (isspace((uschar)*s)) s++; + if (*s == ':') + { + s++; + while (isspace((uschar)*s)) s++; + } + + /* Reset dynamic store, if we need to, and revert to the search pool */ + + if (reset_point != NULL) + { + store_reset(reset_point); + store_pool = old_pool; + } + + /* Now we want to build the result string to contain the data. There can be + two kinds of continuation: (a) the physical line may not all have fitted into + the buffer, and (b) there may be logical continuation lines, for which we + must convert all leading white space into a single blank. + + Initialize, and copy the first segment of data. */ + + size = 100; + ptr = 0; + yield = store_get(size); + if (*s != 0) + yield = string_cat(yield, &size, &ptr, s, Ustrlen(s)); + + /* Now handle continuations */ + + for (last_was_eol = this_is_eol; + Ufgets(buffer, sizeof(buffer), f) != NULL; + last_was_eol = this_is_eol) + { + s = buffer; + p = Ustrlen(buffer); + this_is_eol = p > 0 && buffer[p-1] == '\n'; + + /* Remove trailing white space from a physical line end */ + + if (this_is_eol) + { + while (p > 0 && isspace((uschar)buffer[p-1])) p--; + buffer[p] = 0; + } + + /* If this is not a physical line continuation, skip it entirely if it's + empty or starts with #. Otherwise, break the loop if it doesn't start with + white space. Otherwise, replace leading white space with a single blank. */ + + if (last_was_eol) + { + if (buffer[0] == 0 || buffer[0] == '#') continue; + if (!isspace((uschar)buffer[0])) break; + while (isspace((uschar)*s)) s++; + *(--s) = ' '; + } + + /* Join a physical or logical line continuation onto the result string. */ + + yield = string_cat(yield, &size, &ptr, s, Ustrlen(s)); + } + + yield[ptr] = 0; + store_reset(yield + ptr + 1); + *result = yield; + return OK; + } + +/* Reset dynamic store, if we need to */ + +if (reset_point != NULL) + { + store_reset(reset_point); + store_pool = old_pool; + } + +return FAIL; +} + + +/************************************************* +* Find entry point for lsearch * +*************************************************/ + +/* See local README for interface description */ + +int +lsearch_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_PLAIN); +} + + + +/************************************************* +* Find entry point for wildlsearch * +*************************************************/ + +/* See local README for interface description */ + +int +wildlsearch_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_WILD); +} + + + +/************************************************* +* Find entry point for nwildlsearch * +*************************************************/ + +/* See local README for interface description */ + +int +nwildlsearch_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_NWILD); +} + + + + +/************************************************* +* Find entry point for iplsearch * +*************************************************/ + +/* See local README for interface description */ + +int +iplsearch_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +do_cache = do_cache; /* Keep picky compilers happy */ +if ((length == 1 && keystring[0] == '*') || + string_is_ip_address(keystring, NULL)) + { + return internal_lsearch_find(handle, filename, keystring, length, result, + errmsg, LSEARCH_IP); + } +else + { + *errmsg = string_sprintf("\"%s\" is not a valid iplsearch key (an IP " + "address, with optional CIDR mask, is wanted): " + "in a host list, use net-iplsearch as the search type", keystring); + return DEFER; + } +} + + + + +/************************************************* +* Close entry point * +*************************************************/ + +/* See local README for interface description */ + +void +lsearch_close(void *handle) +{ +fclose((FILE *)handle); +} + +/* End of lookups/lsearch.c */ diff --git a/src/src/lookups/lsearch.h b/src/src/lookups/lsearch.h new file mode 100644 index 000000000..7877c0814 --- /dev/null +++ b/src/src/lookups/lsearch.h @@ -0,0 +1,25 @@ +/* $Cambridge: exim/src/src/lookups/lsearch.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the lsearch and wildlsearch lookups */ + +extern void *lsearch_open(uschar *, uschar **); +extern BOOL lsearch_check(void *, uschar *, int, uid_t *, gid_t *, uschar **); +extern int lsearch_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern void lsearch_close(void *); + +extern int wildlsearch_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern int nwildlsearch_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern int iplsearch_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); + +/* End of lookups/lsearch.h */ diff --git a/src/src/lookups/mysql.c b/src/src/lookups/mysql.c new file mode 100644 index 000000000..fbd8127a2 --- /dev/null +++ b/src/src/lookups/mysql.c @@ -0,0 +1,436 @@ +/* $Cambridge: exim/src/src/lookups/mysql.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Thanks to Paul Kelly for contributing the original code for these +functions. */ + + +#include "../exim.h" +#include "lf_functions.h" +#include "mysql.h" /* The local header */ + + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the MYSQL header +available for compiling. Therefore, compile these functions only if +LOOKUP_MYSQL is defined. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef LOOKUP_MYSQL +static void dummy(int x) { dummy(x-1); } +#else + + +#include /* The system header */ + + +/* Structure and anchor for caching connections. */ + +typedef struct mysql_connection { + struct mysql_connection *next; + uschar *server; + MYSQL *handle; +} mysql_connection; + +static mysql_connection *mysql_connections = NULL; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +mysql_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +void +mysql_tidy(void) +{ +mysql_connection *cn; +while ((cn = mysql_connections) != NULL) + { + mysql_connections = cn->next; + DEBUG(D_lookup) debug_printf("close MYSQL connection: %s\n", cn->server); + mysql_close(cn->handle); + } +} + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* This function is called from the find entry point to do the search for a +single server. + +Arguments: + query the query string + server the server string + resultptr where to store the result + errmsg where to point an error message + defer_break TRUE if no more servers are to be tried after DEFER + do_cache set false if data is changed + +The server string is of the form "host/dbname/user/password". The host can be +host:port. This string is in a nextinlist temporary buffer, so can be +overwritten. + +Returns: OK, FAIL, or DEFER +*/ + +static int +perform_mysql_search(uschar *query, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break, BOOL *do_cache) +{ +MYSQL *mysql_handle = NULL; /* Keep compilers happy */ +MYSQL_RES *mysql_result = NULL; +MYSQL_ROW mysql_row_data; +MYSQL_FIELD *fields; + +int i; +int ssize = 0; +int offset = 0; +int yield = DEFER; +unsigned int num_fields; +uschar *result = NULL; +mysql_connection *cn; +uschar *server_copy = NULL; +uschar *sdata[4]; + +/* Disaggregate the parameters from the server argument. The order is host, +database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + +for (i = 3; i > 0; i--) + { + uschar *pp = Ustrrchr(server, '/'); + if (pp == NULL) + { + *errmsg = string_sprintf("incomplete MySQL server data: %s", + (i == 3)? server : server_copy); + *defer_break = TRUE; + return DEFER; + } + *pp++ = 0; + sdata[i] = pp; + if (i == 3) server_copy = string_copy(server); /* sans password */ + } +sdata[0] = server; /* What's left at the start */ + +/* See if we have a cached connection to the server */ + +for (cn = mysql_connections; cn != NULL; cn = cn->next) + { + if (Ustrcmp(cn->server, server_copy) == 0) + { + mysql_handle = cn->handle; + break; + } + } + +/* If no cached connection, we must set one up. Mysql allows for a host name +and port to be specified. It also allows the name of a Unix socket to be used. +Unfortunately, this contains slashes, but its use is expected to be rare, so +the rather cumbersome syntax shouldn't inconvenience too many people. We use +this: host:port(socket) where all the parts are optional. */ + +if (cn == NULL) + { + uschar *p; + uschar *socket = NULL; + int port = 0; + + if ((p = Ustrchr(sdata[0], '(')) != NULL) + { + *p++ = 0; + socket = p; + while (*p != 0 && *p != ')') p++; + *p = 0; + } + + if ((p = Ustrchr(sdata[0], ':')) != NULL) + { + *p++ = 0; + port = Uatoi(p); + } + + if (Ustrchr(sdata[0], '/') != NULL) + { + *errmsg = string_sprintf("unexpected slash in MySQL server hostname: %s", + sdata[0]); + *defer_break = TRUE; + return DEFER; + } + + /* If the database is the empty string, set it NULL - the query must then + define it. */ + + if (sdata[1][0] == 0) sdata[1] = NULL; + + DEBUG(D_lookup) + debug_printf("MYSQL new connection: host=%s port=%d socket=%s " + "database=%s user=%s\n", sdata[0], port, socket, sdata[1], sdata[2]); + + /* Get store for a new handle, initialize it, and connect to the server */ + + mysql_handle = store_get(sizeof(MYSQL)); + mysql_init(mysql_handle); + if (mysql_real_connect(mysql_handle, + /* host user passwd database */ + CS sdata[0], CS sdata[2], CS sdata[3], CS sdata[1], + port, CS socket, 0) == NULL) + { + *errmsg = string_sprintf("MYSQL connection failed: %s", + mysql_error(mysql_handle)); + *defer_break = FALSE; + goto MYSQL_EXIT; + } + + /* Add the connection to the cache */ + + cn = store_get(sizeof(mysql_connection)); + cn->server = server_copy; + cn->handle = mysql_handle; + cn->next = mysql_connections; + mysql_connections = cn; + } + +/* Else use a previously cached connection */ + +else + { + DEBUG(D_lookup) + debug_printf("MYSQL using cached connection for %s\n", server_copy); + } + +/* Run the query */ + +if (mysql_query(mysql_handle, CS query) != 0) + { + *errmsg = string_sprintf("MYSQL: query failed: %s\n", + mysql_error(mysql_handle)); + *defer_break = FALSE; + goto MYSQL_EXIT; + } + +/* Pick up the result. If the query was not of the type that returns data, +namely INSERT, UPDATE, or DELETE, an error occurs here. However, this situation +can be detected by calling mysql_field_count(). If its result is zero, no data +was expected (this is all explained clearly in the MySQL manual). In this case, +we return the number of rows affected by the command. In this event, we do NOT +want to cache the result; also the whole cache for the handle must be cleaned +up. Setting do_cache FALSE requests this. */ + +if ((mysql_result = mysql_use_result(mysql_handle)) == NULL) + { + if ( mysql_field_count(mysql_handle) == 0 ) + { + DEBUG(D_lookup) debug_printf("MYSQL: query was not one that returns data\n"); + result = string_sprintf("%d", mysql_affected_rows(mysql_handle)); + *do_cache = FALSE; + goto MYSQL_EXIT; + } + *errmsg = string_sprintf("MYSQL: lookup result failed: %s\n", + mysql_error(mysql_handle)); + *defer_break = FALSE; + goto MYSQL_EXIT; + } + +/* Find the number of fields returned. If this is one, we don't add field +names to the data. Otherwise we do. */ + +num_fields = mysql_num_fields(mysql_result); + +/* Get the fields and construct the result string. If there is more than one +row, we insert '\n' between them. */ + +fields = mysql_fetch_fields(mysql_result); + +while ((mysql_row_data = mysql_fetch_row(mysql_result)) != NULL) + { + unsigned long *lengths = mysql_fetch_lengths(mysql_result); + + if (result != NULL) + result = string_cat(result, &ssize, &offset, US"\n", 1); + + if (num_fields == 1) + { + if (mysql_row_data[0] != NULL) /* NULL value yields nothing */ + result = string_cat(result, &ssize, &offset, US mysql_row_data[0], + lengths[0]); + } + + else for (i = 0; i < num_fields; i++) + { + result = lf_quote(US fields[i].name, US mysql_row_data[i], lengths[i], + result, &ssize, &offset); + } + } + +/* If result is NULL then no data has been found and so we return FAIL. +Otherwise, we must terminate the string which has been built; string_cat() +always leaves enough room for a terminating zero. */ + +if (result == NULL) + { + yield = FAIL; + *errmsg = US"MYSQL: no data found"; + } +else + { + result[offset] = 0; + store_reset(result + offset + 1); + } + +/* Get here by goto from various error checks and from the case where no data +was read (e.g. an update query). */ + +MYSQL_EXIT: + +/* Free mysal store for any result that was got; don't close the connection, as +it is cached. */ + +if (mysql_result != NULL) mysql_free_result(mysql_result); + +/* Non-NULL result indicates a sucessful result */ + +if (result != NULL) + { + *resultptr = result; + return OK; + } +else + { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return yield; /* FAIL or DEFER */ + } +} + + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The handle and filename +arguments are not used. Loop through a list of servers while the query is +deferred with a retryable error. */ + +int +mysql_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +int sep = 0; +uschar *server; +uschar *list = mysql_servers; +uschar buffer[512]; + +DEBUG(D_lookup) debug_printf("MYSQL query: %s\n", query); + +while ((server = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) + { + BOOL defer_break = FALSE; + int rc = perform_mysql_search(query, server, result, errmsg, &defer_break, + do_cache); + if (rc != DEFER || defer_break) return rc; + } + +if (mysql_servers == NULL) + *errmsg = US"no MYSQL servers defined (mysql_servers option)"; + +return DEFER; +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only characters that need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. +Percent, and underscore and not escaped. They are only special in contexts +where they can be wild cards, and this isn't usually the case for data inserted +from messages, since that isn't likely to be treated as a pattern of any kind. +Sadly, MySQL doesn't seem to behave like other programs. If you use something +like "where id="ab\%cd" it does not treat the string as "ab%cd". So you really +can't quote "on spec". + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +uschar * +mysql_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt != NULL) return NULL; /* No options recognized */ + +while ((c = *t++) != 0) + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +if (count == 0) return s; +t = quoted = store_get(Ustrlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) + { + *t++ = '\\'; + switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + } + } + else *t++ = c; + } + +*t = 0; +return quoted; +} + + +#endif /* MYSQL_LOOKUP */ + +/* End of lookups/mysql.c */ diff --git a/src/src/lookups/mysql.h b/src/src/lookups/mysql.h new file mode 100644 index 000000000..c13a482b6 --- /dev/null +++ b/src/src/lookups/mysql.h @@ -0,0 +1,18 @@ +/* $Cambridge: exim/src/src/lookups/mysql.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the mysql lookup functions */ + +extern void *mysql_open(uschar *, uschar **); +extern int mysql_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern void mysql_tidy(void); +extern uschar *mysql_quote(uschar *, uschar *); + +/* End of lookups/mysql.h */ diff --git a/src/src/lookups/nis.c b/src/src/lookups/nis.c new file mode 100644 index 000000000..bbb85b5bd --- /dev/null +++ b/src/src/lookups/nis.c @@ -0,0 +1,104 @@ +/* $Cambridge: exim/src/src/lookups/nis.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "nis.h" + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the NIS header +available for compiling. Therefore, compile these functions only if LOOKUP_NIS +is defined. However, some compilers don't like compiling empty modules, so keep +them happy with a dummy when skipping the rest. Make it reference itself to +stop picky compilers complaining that it is unused, and put in a dummy argument +to stop even pickier compilers complaining about infinite loops. */ + +#ifndef LOOKUP_NIS +static void dummy(int x) { dummy(x-1); } +#else + +#include + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. This serves for both +the "nis" and "nis0" lookup types. */ + +void * +nis_open(uschar *filename, uschar **errmsg) +{ +char *nis_domain; +if (yp_get_default_domain(&nis_domain) != 0) + { + *errmsg = string_sprintf("failed to get default NIS domain"); + return NULL; + } +return nis_domain; +} + + + +/************************************************* +* Find entry point for nis * +*************************************************/ + +/* See local README for interface description. A separate function is used +for nis0 because they are so short it isn't worth trying to use any common +code. */ + +int +nis_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +int rc; +uschar *nis_data; +int nis_data_length; +do_cache = do_cache; /* Placate picky compilers */ +if ((rc = yp_match(CS handle, CS filename, CS keystring, length, + CSS &nis_data, &nis_data_length)) == 0) + { + *result = string_copy(nis_data); + (*result)[nis_data_length] = 0; /* remove final '\n' */ + return OK; + } +return (rc == YPERR_KEY || rc == YPERR_MAP)? FAIL : DEFER; +} + + + +/************************************************* +* Find entry point for nis0 * +*************************************************/ + +/* See local README for interface description. */ + +int +nis0_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +int rc; +uschar *nis_data; +int nis_data_length; +do_cache = do_cache; /* Placate picky compilers */ +if ((rc = yp_match(CS handle, CS filename, CS keystring, length + 1, + CSS &nis_data, &nis_data_length)) == 0) + { + *result = string_copy(nis_data); + (*result)[nis_data_length] = 0; /* remove final '\n' */ + return OK; + } +return (rc == YPERR_KEY || rc == YPERR_MAP)? FAIL : DEFER; +} + +#endif /* LOOKUP_NIS */ + +/* End of lookups/nis.c */ diff --git a/src/src/lookups/nis.h b/src/src/lookups/nis.h new file mode 100644 index 000000000..660a1e380 --- /dev/null +++ b/src/src/lookups/nis.h @@ -0,0 +1,18 @@ +/* $Cambridge: exim/src/src/lookups/nis.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the nis and nis0 lookups */ + +extern void *nis_open(uschar *, uschar **); +extern int nis_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern int nis0_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); + +/* End of lookups/nis.h */ diff --git a/src/src/lookups/nisplus.c b/src/src/lookups/nisplus.c new file mode 100644 index 000000000..865924498 --- /dev/null +++ b/src/src/lookups/nisplus.c @@ -0,0 +1,279 @@ +/* $Cambridge: exim/src/src/lookups/nisplus.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "nisplus.h" + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the NIS+ header +available for compiling. Therefore, compile these functions only if +LOOKUP_NISPLUS is defined. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef LOOKUP_NISPLUS +static void dummy(int x) { dummy(x-1); } +#else + + +#include + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +nisplus_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The format of queries for a +NIS+ search is + + [field=value,...],table-name +or + [field=value,...],table-name:result-field-name + +in other words, a normal NIS+ "indexed name", with an optional result field +name tagged on the end after a colon. If there is no result-field name, the +yield is the concatenation of all the fields, preceded by their names and an +equals sign. */ + +int +nisplus_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +int i; +int ssize = 0; +int offset = 0; +int error_error = FAIL; +uschar *field_name = NULL; +nis_result *nrt = NULL; +nis_result *nre = NULL; +nis_object *tno, *eno; +struct entry_obj *eo; +struct table_obj *ta; +uschar *p = query + length; +uschar *yield = NULL; + +do_cache = do_cache; /* Placate picky compilers */ + +/* Search backwards for a colon to see if a result field name +has been given. */ + +while (p > query && p[-1] != ':') p--; + +if (p > query) + { + field_name = p; + p[-1] = 0; + } +else p = query + length; + +/* Now search backwards to find the comma that starts the +table name. */ + +while (p > query && p[-1] != ',') p--; +if (p <= query) + { + *errmsg = US"NIS+ query malformed"; + error_error = DEFER; + goto NISPLUS_EXIT; + } + +/* Look up the data for the table, in order to get the field names, +check that we got back a table, and set up pointers so the field +names can be scanned. */ + +nrt = nis_lookup(CS p, EXPAND_NAME | NO_CACHE); +if (nrt->status != NIS_SUCCESS) + { + *errmsg = string_sprintf("NIS+ error accessing %s table: %s", p, + nis_sperrno(nrt->status)); + if (nrt->status != NIS_NOTFOUND && nrt->status != NIS_NOSUCHTABLE) + error_error = DEFER; + goto NISPLUS_EXIT; + } +tno = nrt->objects.objects_val; +if (tno->zo_data.zo_type != TABLE_OBJ) + { + *errmsg = string_sprintf("NIS+ error: %s is not a table", p); + goto NISPLUS_EXIT; + } +ta = &(tno->zo_data.objdata_u.ta_data); + +/* Now look up the entry in the table, check that we got precisely one +object and that it is a table entry. */ + +nre = nis_list(CS query, EXPAND_NAME, NULL, NULL); +if (nre->status != NIS_SUCCESS) + { + *errmsg = string_sprintf("NIS+ error accessing entry %s: %s", + query, nis_sperrno(nre->status)); + goto NISPLUS_EXIT; + } +if (nre->objects.objects_len > 1) + { + *errmsg = string_sprintf("NIS+ returned more than one object for %s", + query); + goto NISPLUS_EXIT; + } +else if (nre->objects.objects_len < 1) + { + *errmsg = string_sprintf("NIS+ returned no data for %s", query); + goto NISPLUS_EXIT; + } +eno = nre->objects.objects_val; +if (eno->zo_data.zo_type != ENTRY_OBJ) + { + *errmsg = string_sprintf("NIS+ error: %s is not an entry", query); + goto NISPLUS_EXIT; + } + +/* Scan the columns in the entry and in the table. If a result field +was given, look for that field; otherwise concatenate all the fields +with their names. */ + +eo = &(eno->zo_data.objdata_u.en_data); +for (i = 0; i < eo->en_cols.en_cols_len; i++) + { + table_col *tc = ta->ta_cols.ta_cols_val + i; + entry_col *ec = eo->en_cols.en_cols_val + i; + int len = ec->ec_value.ec_value_len; + uschar *value = US ec->ec_value.ec_value_val; + + /* The value may be NULL for a zero-length field. Turn this into an + empty string for consistency. Remove trailing whitespace and zero + bytes. */ + + if (value == NULL) value = US""; else + while (len > 0 && (value[len-1] == 0 || isspace(value[len-1]))) + len--; + + /* Concatenate all fields if no specific one selected */ + + if (field_name == NULL) + { + yield = string_cat(yield, &ssize, &offset,US tc->tc_name, + Ustrlen(tc->tc_name)); + yield = string_cat(yield, &ssize, &offset, US"=", 1); + + /* Quote the value if it contains spaces or is empty */ + + if (value[0] == 0 || Ustrchr(value, ' ') != NULL) + { + int j; + yield = string_cat(yield, &ssize, &offset, US"\"", 1); + for (j = 0; j < len; j++) + { + if (value[j] == '\"' || value[j] == '\\') + yield = string_cat(yield, &ssize, &offset, US"\\", 1); + yield = string_cat(yield, &ssize, &offset, value+j, 1); + } + yield = string_cat(yield, &ssize, &offset, US"\"", 1); + } + else yield = string_cat(yield, &ssize, &offset, value, len); + + yield = string_cat(yield, &ssize, &offset, US" ", 1); + } + + /* When the specified field is found, grab its data and finish */ + + else if (Ustrcmp(field_name, tc->tc_name) == 0) + { + yield = string_copyn(value, len); + goto NISPLUS_EXIT; + } + } + +/* Error if a field name was specified and we didn't find it; if no +field name, ensure the concatenated data is zero-terminated. */ + +if (field_name != NULL) + *errmsg = string_sprintf("NIS+ field %s not found for %s", field_name, + query); +else + { + yield[offset] = 0; + store_reset(yield + offset + 1); + } + +/* Restore the colon in the query, and free result store before +finishing. */ + +NISPLUS_EXIT: +if (field_name != NULL) field_name[-1] = ':'; +if (nrt != NULL) nis_freeresult(nrt); +if (nre != NULL) nis_freeresult(nre); + +if (yield != NULL) + { + *result = yield; + return OK; + } + +return error_error; /* FAIL or DEFER */ +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only quoting that is necessary for NIS+ is to double any doublequote +characters. No options are recognized. + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +uschar * +nisplus_quote(uschar *s, uschar *opt) +{ +int count = 0; +uschar *quoted; +uschar *t = s; + +if (opt != NULL) return NULL; /* No options recognized */ + +while (*t != 0) if (*t++ == '\"') count++; +if (count == 0) return s; + +t = quoted = store_get(Ustrlen(s) + count + 1); + +while (*s != 0) + { + *t++ = *s; + if (*s++ == '\"') *t++ = '\"'; + } + +*t = 0; +return quoted; +} + +#endif /* LOOKUP_NISPLUS */ + +/* End of lookups/nisplus.c */ diff --git a/src/src/lookups/nisplus.h b/src/src/lookups/nisplus.h new file mode 100644 index 000000000..6f1e60b3e --- /dev/null +++ b/src/src/lookups/nisplus.h @@ -0,0 +1,17 @@ +/* $Cambridge: exim/src/src/lookups/nisplus.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the nisplus lookup */ + +extern void *nisplus_open(uschar *, uschar **); +extern int nisplus_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern uschar *nisplus_quote(uschar *, uschar *); + +/* End of lookups/nisplus.h */ diff --git a/src/src/lookups/oracle.c b/src/src/lookups/oracle.c new file mode 100644 index 000000000..b438b3bb1 --- /dev/null +++ b/src/src/lookups/oracle.c @@ -0,0 +1,619 @@ +/* $Cambridge: exim/src/src/lookups/oracle.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Interface to an Oracle database. This code was originally supplied by +Paul Kelly, but I have hacked it around for various reasons, and tried to add +some comments from my position of Oracle ignorance. */ + + +#include "../exim.h" + + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the ORACLE headers +available for compiling. Therefore, compile these functions only if +LOOKUP_ORACLE is defined. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef LOOKUP_ORACLE +static void dummy(int x) { dummy(x-1); } +#else + +/* The Oracle system headers */ + +#include +#include +#include + +#include "oracle.h" /* The local header */ + +#define PARSE_NO_DEFER 0 /* parse straight away */ +#define PARSE_V7_LNG 2 +#define MAX_ITEM_BUFFER_SIZE 1024 /* largest size of a cell of data */ +#define MAX_SELECT_LIST_SIZE 32 /* maximum number of columns (not rows!) */ + +/* Paul's comment on this was "change this to 512 for 64bit cpu", but I don't +understand why. The Oracle manual just asks for 256 bytes. */ + +#define HDA_SIZE 256 + +/* Internal/external datatype codes */ + +#define NUMBER_TYPE 2 +#define INT_TYPE 3 +#define FLOAT_TYPE 4 +#define STRING_TYPE 5 +#define ROWID_TYPE 11 +#define DATE_TYPE 12 + +/* ORACLE error codes used in demonstration programs */ + +#define VAR_NOT_IN_LIST 1007 +#define NO_DATA_FOUND 1403 + +typedef struct Ora_Describe { + sb4 dbsize; + sb2 dbtype; + sb1 buf[MAX_ITEM_BUFFER_SIZE]; + sb4 buflen; + sb4 dsize; + sb2 precision; + sb2 scale; + sb2 nullok; +} Ora_Describe; + +typedef struct Ora_Define { + ub1 buf[MAX_ITEM_BUFFER_SIZE]; + float flt_buf; + sword int_buf; + sb2 indp; + ub2 col_retlen, col_retcode; +} Ora_Define; + +/* Structure and anchor for caching connections. */ + +typedef struct oracle_connection { + struct oracle_connection *next; + uschar *server; + struct cda_def *handle; + void *hda_mem; +} oracle_connection; + +static oracle_connection *oracle_connections = NULL; + + + + + +/************************************************* +* Set up message after error * +*************************************************/ + +/* Sets up a message from a local string plus whatever Oracle gives. + +Arguments: + oracle_handle the handle of the connection + rc the return code + msg local text message +*/ + +static uschar * +oracle_error(struct cda_def *oracle_handle, int rc, uschar *msg) +{ +uschar tmp[1024]; +oerhms(oracle_handle, rc, tmp, sizeof(tmp)); +return string_sprintf("ORACLE %s: %s", msg, tmp); +} + + + +/************************************************* +* Describe and define the select list items * +*************************************************/ + +/* Figures out sizes, types, and numbers. + +Arguments: + cda the connection + def + desc descriptions put here + +Returns: number of fields +*/ + +static sword +describe_define(Cda_Def *cda, Ora_Define *def, Ora_Describe *desc) +{ +sword col, deflen, deftyp; +static ub1 *defptr; +static sword numwidth = 8; + +/* Describe the select-list items. */ + +for (col = 0; col < MAX_SELECT_LIST_SIZE; col++) + { + desc[col].buflen = MAX_ITEM_BUFFER_SIZE; + + if (odescr(cda, col + 1, &desc[col].dbsize, + &desc[col].dbtype, &desc[col].buf[0], + &desc[col].buflen, &desc[col].dsize, + &desc[col].precision, &desc[col].scale, + &desc[col].nullok) != 0) + { + /* Break on end of select list. */ + if (cda->rc == VAR_NOT_IN_LIST) break; else return -1; + } + + /* Adjust sizes and types for display, handling NUMBER with scale as float. */ + + if (desc[col].dbtype == NUMBER_TYPE) + { + desc[col].dbsize = numwidth; + if (desc[col].scale != 0) + { + defptr = (ub1 *)&def[col].flt_buf; + deflen = (sword) sizeof(float); + deftyp = FLOAT_TYPE; + desc[col].dbtype = FLOAT_TYPE; + } + else + { + defptr = (ub1 *)&def[col].int_buf; + deflen = (sword) sizeof(sword); + deftyp = INT_TYPE; + desc[col].dbtype = INT_TYPE; + } + } + else + { + if (desc[col].dbtype == DATE_TYPE) + desc[col].dbsize = 9; + if (desc[col].dbtype == ROWID_TYPE) + desc[col].dbsize = 18; + defptr = def[col].buf; + deflen = desc[col].dbsize > MAX_ITEM_BUFFER_SIZE ? + MAX_ITEM_BUFFER_SIZE : desc[col].dbsize + 1; + deftyp = STRING_TYPE; + desc[col].dbtype = STRING_TYPE; + } + + /* Define an output variable */ + + if (odefin(cda, col + 1, + defptr, deflen, deftyp, + -1, &def[col].indp, (text *) 0, -1, -1, + &def[col].col_retlen, + &def[col].col_retcode) != 0) + return -1; + } /* Loop for each column */ + +return col; +} + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +oracle_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +void +oracle_tidy(void) +{ +oracle_connection *cn; +while ((cn = oracle_connections) != NULL) + { + oracle_connections = cn->next; + DEBUG(D_lookup) debug_printf("close ORACLE connection: %s\n", cn->server); + ologof(cn->handle); + } +} + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* This function is called from the find entry point to do the search for a +single server. + +Arguments: + query the query string + server the server string + resultptr where to store the result + errmsg where to point an error message + defer_break TRUE if no more servers are to be tried after DEFER + +The server string is of the form "host/dbname/user/password", for compatibility +with MySQL and pgsql, but at present, the dbname is not used. This string is in +a nextinlist temporary buffer, so can be overwritten. + +Returns: OK, FAIL, or DEFER +*/ + +static int +perform_oracle_search(uschar *query, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break) +{ +Cda_Def *cda = NULL; +struct cda_def *oracle_handle = NULL; +Ora_Describe *desc = NULL; +Ora_Define *def = NULL; +void *hda = NULL; + +int i; +int ssize = 0; +int offset = 0; +int yield = DEFER; +unsigned int num_fields = 0; +uschar *result = NULL; +oracle_connection *cn = NULL; +uschar *server_copy = NULL; +uschar *sdata[4]; +uschar tmp[1024]; + +/* Disaggregate the parameters from the server argument. The order is host, +database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + +for (i = 3; i > 0; i--) + { + uschar *pp = Ustrrchr(server, '/'); + if (pp == NULL) + { + *errmsg = string_sprintf("incomplete ORACLE server data: %s", server); + *defer_break = TRUE; + return DEFER; + } + *pp++ = 0; + sdata[i] = pp; + if (i == 3) server_copy = string_copy(server); /* sans password */ + } +sdata[0] = server; /* What's left at the start */ + +/* If the database is the empty string, set it NULL - the query must then +define it. */ + +if (sdata[1][0] == 0) sdata[1] = NULL; + +/* See if we have a cached connection to the server */ + +for (cn = oracle_connections; cn != NULL; cn = cn->next) + { + if (strcmp(cn->server, server_copy) == 0) + { + oracle_handle = cn->handle; + hda = cn->hda_mem; + break; + } + } + +/* If no cached connection, we must set one up */ + +if (cn == NULL) + { + DEBUG(D_lookup) debug_printf("ORACLE new connection: host=%s database=%s " + "user=%s\n", sdata[0], sdata[1], sdata[2]); + + /* Get store for a new connection, initialize it, and connect to the server */ + + oracle_handle = store_get(sizeof(struct cda_def)); + hda = store_get(HDA_SIZE); + memset(hda,'\0',HDA_SIZE); + + /* + * Perform a default (blocking) login + * + * sdata[0] = tnsname (service name - typically host name) + * sdata[1] = dbname - not used at present + * sdata[2] = username + * sdata[3] = passwd + */ + + if(olog(oracle_handle, hda, sdata[2], -1, sdata[3], -1, sdata[0], -1, + (ub4)OCI_LM_DEF) != 0) + { + *errmsg = oracle_error(oracle_handle, oracle_handle->rc, + US"connection failed"); + *defer_break = FALSE; + goto ORACLE_EXIT_NO_VALS; + } + + /* Add the connection to the cache */ + + cn = store_get(sizeof(oracle_connection)); + cn->server = server_copy; + cn->handle = oracle_handle; + cn->next = oracle_connections; + cn->hda_mem = hda; + oracle_connections = cn; + } + +/* Else use a previously cached connection - we can write to the server string +to obliterate the password because it is in a nextinlist temporary buffer. */ + +else + { + DEBUG(D_lookup) + debug_printf("ORACLE using cached connection for %s\n", server_copy); + } + +/* We have a connection. Open a cursor and run the query */ + +cda = store_get(sizeof(Cda_Def)); + +if (oopen(cda, oracle_handle, (text *)0, -1, -1, (text *)0, -1) != 0) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "failed to open cursor"); + *defer_break = FALSE; + goto ORACLE_EXIT_NO_VALS; + } + +if (oparse(cda, (text *)query, (sb4) -1, + (sword)PARSE_NO_DEFER, (ub4)PARSE_V7_LNG) != 0) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "query failed"); + *defer_break = FALSE; + oclose(cda); + goto ORACLE_EXIT_NO_VALS; + } + +/* Find the number of fields returned and sort out their types. If the number +is one, we don't add field names to the data. Otherwise we do. */ + +def = store_get(sizeof(Ora_Define)*MAX_SELECT_LIST_SIZE); +desc = store_get(sizeof(Ora_Describe)*MAX_SELECT_LIST_SIZE); + +if ((num_fields = describe_define(cda,def,desc)) == -1) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "describe_define failed"); + *defer_break = FALSE; + goto ORACLE_EXIT; + } + +if (oexec(cda)!=0) + { + *errmsg = oracle_error(oracle_handle, cda->rc, "oexec failed"); + *defer_break = FALSE; + goto ORACLE_EXIT; + } + +/* Get the fields and construct the result string. If there is more than one +row, we insert '\n' between them. */ + +while (cda->rc != NO_DATA_FOUND) /* Loop for each row */ + { + ofetch(cda); + if(cda->rc == NO_DATA_FOUND) break; + + if (result != NULL) result = string_cat(result, &ssize, &offset, "\n", 1); + + /* Single field - just add on the data */ + + if (num_fields == 1) + result = string_cat(result, &ssize, &offset, def[0].buf, def[0].col_retlen); + + /* Multiple fields - precede by file name, removing {lead,trail}ing WS */ + + else for (i = 0; i < num_fields; i++) + { + int slen; + uschar *s = US desc[i].buf; + + while (*s != 0 && isspace(*s)) s++; + slen = Ustrlen(s); + while (slen > 0 && isspace(s[slen-1])) slen--; + result = string_cat(result, &ssize, &offset, s, slen); + result = string_cat(result, &ssize, &offset, US"=", 1); + + /* int and float type wont ever need escaping. Otherwise, quote the value + if it contains spaces or is empty. */ + + if (desc[i].dbtype != INT_TYPE && desc[i].dbtype != FLOAT_TYPE && + (def[i].buf[0] == 0 || strchr(def[i].buf, ' ') != NULL)) + { + int j; + result = string_cat(result, &ssize, &offset, "\"", 1); + for (j = 0; j < def[i].col_retlen; j++) + { + if (def[i].buf[j] == '\"' || def[i].buf[j] == '\\') + result = string_cat(result, &ssize, &offset, "\\", 1); + result = string_cat(result, &ssize, &offset, def[i].buf+j, 1); + } + result = string_cat(result, &ssize, &offset, "\"", 1); + } + + else switch(desc[i].dbtype) + { + case INT_TYPE: + sprintf(CS tmp, "%d", def[i].int_buf); + result = string_cat(result, &ssize, &offset, tmp, Ustrlen(tmp)); + break; + + case FLOAT_TYPE: + sprintf(CS tmp, "%f", def[i].flt_buf); + result = string_cat(result, &ssize, &offset, tmp, Ustrlen(tmp)); + break; + + case STRING_TYPE: + result = string_cat(result, &ssize, &offset, def[i].buf, + def[i].col_retlen); + break; + + default: + *errmsg = string_sprintf("ORACLE: unknown field type %d", desc[i].dbtype); + *defer_break = FALSE; + result = NULL; + goto ORACLE_EXIT; + } + + result = string_cat(result, &ssize, &offset, " ", 1); + } + } + +/* If result is NULL then no data has been found and so we return FAIL. +Otherwise, we must terminate the string which has been built; string_cat() +always leaves enough room for a terminating zero. */ + +if (result == NULL) + { + yield = FAIL; + *errmsg = "ORACLE: no data found"; + } +else + { + result[offset] = 0; + store_reset(result + offset + 1); + } + +/* Get here by goto from various error checks. */ + +ORACLE_EXIT: + +/* Close the cursor; don't close the connection, as it is cached. */ + +oclose(cda); + +ORACLE_EXIT_NO_VALS: + +/* Non-NULL result indicates a sucessful result */ + +if (result != NULL) + { + *resultptr = result; + return OK; + } +else + { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return yield; /* FAIL or DEFER */ + } +} + + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The handle and filename +arguments are not used. Loop through a list of servers while the query is +deferred with a retryable error. */ + +int +oracle_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +int sep = 0; +uschar *server; +uschar *list = oracle_servers; +uschar buffer[512]; + +do_cache = do_cache; /* Placate picky compilers */ + +DEBUG(D_lookup) debug_printf("ORACLE query: %s\n", query); + +while ((server = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) + { + BOOL defer_break; + int rc = perform_oracle_search(query, server, result, errmsg, &defer_break); + if (rc != DEFER || defer_break) return rc; + } + +if (oracle_servers == NULL) + *errmsg = "no ORACLE servers defined (oracle_servers option)"; + +return DEFER; +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The only characters that need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. +Percent and underscore are not escaped. They are only special in contexts where +they can be wild cards, and this isn't usually the case for data inserted from +messages, since that isn't likely to be treated as a pattern of any kind. + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +uschar * +oracle_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt != NULL) return NULL; /* No options are recognized */ + +while ((c = *t++) != 0) + if (strchr("\n\t\r\b\'\"\\", c) != NULL) count++; + +if (count == 0) return s; +t = quoted = store_get((int)strlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (strchr("\n\t\r\b\'\"\\", c) != NULL) + { + *t++ = '\\'; + switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + } + } + else *t++ = c; + } + +*t = 0; +return quoted; +} + +#endif /* LOOKUP_ORACLE */ + +/* End of lookups/oracle.c */ diff --git a/src/src/lookups/oracle.h b/src/src/lookups/oracle.h new file mode 100644 index 000000000..98a28010d --- /dev/null +++ b/src/src/lookups/oracle.h @@ -0,0 +1,18 @@ +/* $Cambridge: exim/src/src/lookups/oracle.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the Oracle lookup functions */ + +extern void *oracle_open(uschar *, uschar **); +extern int oracle_find(void *, uschar *, uschar *, int, uschar **, + uschar **, BOOL *); +extern void oracle_tidy(void); +extern uschar *oracle_quote(uschar *, uschar *); + +/* End of lookups/oracle.h */ diff --git a/src/src/lookups/passwd.c b/src/src/lookups/passwd.c new file mode 100644 index 000000000..1b1ceadff --- /dev/null +++ b/src/src/lookups/passwd.c @@ -0,0 +1,56 @@ +/* $Cambridge: exim/src/src/lookups/passwd.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "passwd.h" + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description */ + +void * +passwd_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +return (void *)(-1); /* Just return something non-null */ +} + + + + +/************************************************* +* Find entry point for passwd * +*************************************************/ + +/* See local README for interface description */ + +int +passwd_find(void *handle, uschar *filename, uschar *keystring, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +struct passwd *pw; + +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; +errmsg = errmsg; +do_cache = do_cache; + +if (!route_finduser(keystring, &pw, NULL)) return FAIL; +*result = string_sprintf("*:%d:%d:%s:%s:%s", (int)pw->pw_uid, (int)pw->pw_gid, + pw->pw_gecos, pw->pw_dir, pw->pw_shell); +return OK; +} + +/* End of lookups/passwd.c */ diff --git a/src/src/lookups/passwd.h b/src/src/lookups/passwd.h new file mode 100644 index 000000000..3bb011a97 --- /dev/null +++ b/src/src/lookups/passwd.h @@ -0,0 +1,16 @@ +/* $Cambridge: exim/src/src/lookups/passwd.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the passwd lookup */ + +extern void *passwd_open(uschar *, uschar **); +extern int passwd_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); + +/* End of lookups/passwd.h */ diff --git a/src/src/lookups/pgsql.c b/src/src/lookups/pgsql.c new file mode 100644 index 000000000..2f65d8363 --- /dev/null +++ b/src/src/lookups/pgsql.c @@ -0,0 +1,469 @@ +/* $Cambridge: exim/src/src/lookups/pgsql.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Thanks to Petr Cech for contributing the original code for these +functions. Thanks to Joachim Wieland for the initial patch for the Unix domain +socket extension. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "pgsql.h" /* The local header */ + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the PGSQL header +available for compiling. Therefore, compile these functions only if +LOOKUP_PGSQL is defined. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef LOOKUP_PGSQL +static void dummy(int x) { dummy(x-1); } +#else + + +#include /* The system header */ + +/* Structure and anchor for caching connections. */ + +typedef struct pgsql_connection { + struct pgsql_connection *next; + uschar *server; + PGconn *handle; +} pgsql_connection; + +static pgsql_connection *pgsql_connections = NULL; + + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +pgsql_open(uschar *filename, uschar **errmsg) +{ +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Tidy entry point * +*************************************************/ + +/* See local README for interface description. */ + +void +pgsql_tidy(void) +{ +pgsql_connection *cn; +while ((cn = pgsql_connections) != NULL) + { + pgsql_connections = cn->next; + DEBUG(D_lookup) debug_printf("close PGSQL connection: %s\n", cn->server); + PQfinish(cn->handle); + } +} + + + +/************************************************* +* Internal search function * +*************************************************/ + +/* This function is called from the find entry point to do the search for a +single server. The server string is of the form "server/dbname/user/password". + +PostgreSQL supports connections through Unix domain sockets. This is usually +faster and costs less cpu time than a TCP/IP connection. However it can only be +used if the mail server runs on the same machine as the database server. A +configuration line for PostgreSQL via Unix domain sockets looks like this: + +hide pgsql_servers = (/tmp/.s.PGSQL.5432)/db/user/password[:] + +We enclose the path name in parentheses so that its slashes aren't visually +confused with the delimeters for the other pgsql_server settings. + +For TCP/IP connections, the server is a host name and optional port (with a +colon separator). + +NOTE: + 1) All three '/' must be present. + 2) If host is omitted the local unix socket is used. + +Arguments: + query the query string + server the server string; this is in dynamic memory and can be updated + resultptr where to store the result + errmsg where to point an error message + defer_break TRUE if no more servers are to be tried after DEFER + do_cache set FALSE if data is changed + +Returns: OK, FAIL, or DEFER +*/ + +static int +perform_pgsql_search(uschar *query, uschar *server, uschar **resultptr, + uschar **errmsg, BOOL *defer_break, BOOL *do_cache) +{ +PGconn *pg_conn = NULL; +PGresult *pg_result = NULL; + +int i; +int ssize = 0; +int offset = 0; +int yield = DEFER; +unsigned int num_fields, num_tuples; +uschar *result = NULL; +pgsql_connection *cn; +uschar *server_copy = NULL; +uschar *sdata[3]; + +/* Disaggregate the parameters from the server argument. The order is host or +path, database, user, password. We can write to the string, since it is in a +nextinlist temporary buffer. The copy of the string that is used for caching +has the password removed. This copy is also used for debugging output. */ + +for (i = 2; i >= 0; i--) + { + uschar *pp = Ustrrchr(server, '/'); + if (pp == NULL) + { + *errmsg = string_sprintf("incomplete pgSQL server data: %s", + (i == 2)? server : server_copy); + *defer_break = TRUE; + return DEFER; + } + *pp++ = 0; + sdata[i] = pp; + if (i == 2) server_copy = string_copy(server); /* sans password */ + } + +/* The total server string has now been truncated so that what is left at the +start is the identification of the server (host or path). See if we have a +cached connection to the server. */ + +for (cn = pgsql_connections; cn != NULL; cn = cn->next) + { + if (Ustrcmp(cn->server, server_copy) == 0) + { + pg_conn = cn->handle; + break; + } + } + +/* If there is no cached connection, we must set one up. */ + +if (cn == NULL) + { + uschar *port = US""; + + /* For a Unix domain socket connection, the path is in parentheses */ + + if (*server == '(') + { + uschar *last_slash, *last_dot, *p; + + p = ++server; + while (*p != 0 && *p != ')') p++; + *p = 0; + + last_slash = Ustrrchr(server, '/'); + last_dot = Ustrrchr(server, '.'); + + DEBUG(D_lookup) debug_printf("PGSQL new connection: socket=%s " + "database=%s user=%s\n", server, sdata[0], sdata[1]); + + /* A valid socket name looks like this: /var/run/postgresql/.s.PGSQL.5432 + We have to call PQsetdbLogin with '/var/run/postgresql' as the hostname + argument and put '5432' into the port variable. */ + + if (last_slash == NULL || last_dot == NULL) + { + *errmsg = string_sprintf("PGSQL invalid filename for socket: %s", + server); + *defer_break = TRUE; + return DEFER; + } + + /* Terminate the path name and set up the port: we'll have something like + server = "/var/run/postgresql" and port = "5432". */ + + *last_slash = 0; + port = last_dot + 1; + } + + /* Host connection; sort out the port */ + + else + { + uschar *p; + if ((p = Ustrchr(server, ':')) != NULL) + { + *p++ = 0; + port = p; + } + + if (Ustrchr(server, '/') != NULL) + { + *errmsg = string_sprintf("unexpected slash in pgSQL server hostname: %s", + server); + *defer_break = TRUE; + return DEFER; + } + + DEBUG(D_lookup) debug_printf("PGSQL new connection: host=%s port=%s " + "database=%s user=%s\n", server, port, sdata[0], sdata[1]); + } + + /* If the database is the empty string, set it NULL - the query must then + define it. */ + + if (sdata[0][0] == 0) sdata[0] = NULL; + + /* Get store for a new handle, initialize it, and connect to the server */ + + pg_conn=PQsetdbLogin( + /* host port options tty database user passwd */ + CS server, CS port, NULL, NULL, CS sdata[0], CS sdata[1], CS sdata[2]); + + if(PQstatus(pg_conn) == CONNECTION_BAD) + { + store_reset(server_copy); + *errmsg = string_sprintf("PGSQL connection failed: %s", + PQerrorMessage(pg_conn)); + PQfinish(pg_conn); + *defer_break = FALSE; + goto PGSQL_EXIT; + } + + /* Add the connection to the cache */ + + cn = store_get(sizeof(pgsql_connection)); + cn->server = server_copy; + cn->handle = pg_conn; + cn->next = pgsql_connections; + pgsql_connections = cn; + } + +/* Else use a previously cached connection */ + +else + { + DEBUG(D_lookup) debug_printf("PGSQL using cached connection for %s\n", + server_copy); + } + +/* Run the query */ + + pg_result = PQexec(pg_conn, CS query); + switch(PQresultStatus(pg_result)) + { + case PGRES_EMPTY_QUERY: + case PGRES_COMMAND_OK: + /* The command was successful but did not return any data since it was + * not SELECT but either an INSERT, UPDATE or DELETE statement. Tell the + * high level code to not cache this query, and clean the current cache for + * this handle by setting *do_cache FALSE. */ + result = string_copy(US PQcmdTuples(pg_result)); + offset = Ustrlen(result); + *do_cache = FALSE; + DEBUG(D_lookup) debug_printf("PGSQL: command does not return any data " + "but was successful. Rows affected: %s\n", result); + + case PGRES_TUPLES_OK: + break; + + default: + *errmsg = string_sprintf("PGSQL: query failed: %s\n", + PQresultErrorMessage(pg_result)); + *defer_break = FALSE; + goto PGSQL_EXIT; + } + +/* Result is in pg_result. Find the number of fields returned. If this is one, +we don't add field names to the data. Otherwise we do. If the query did not +return anything we skip the for loop; this also applies to the case +PGRES_COMMAND_OK. */ + +num_fields = PQnfields(pg_result); +num_tuples = PQntuples(pg_result); + +/* Get the fields and construct the result string. If there is more than one +row, we insert '\n' between them. */ + +for (i = 0; i < num_tuples; i++) + { + if (result != NULL) + result = string_cat(result, &ssize, &offset, US"\n", 1); + + if (num_fields == 1) + { + result = string_cat(result, &ssize, &offset, + US PQgetvalue(pg_result, i, 0), PQgetlength(pg_result, i, 0)); + } + + else + { + int j; + for (j = 0; j < num_fields; j++) + { + uschar *tmp = US PQgetvalue(pg_result, i, j); + result = lf_quote(US PQfname(pg_result, j), tmp, Ustrlen(tmp), result, + &ssize, &offset); + } + } + } + +/* If result is NULL then no data has been found and so we return FAIL. +Otherwise, we must terminate the string which has been built; string_cat() +always leaves enough room for a terminating zero. */ + +if (result == NULL) + { + yield = FAIL; + *errmsg = US"PGSQL: no data found"; + } +else + { + result[offset] = 0; + store_reset(result + offset + 1); + } + +/* Get here by goto from various error checks. */ + +PGSQL_EXIT: + +/* Free store for any result that was got; don't close the connection, as +it is cached. */ + +if (pg_result != NULL) PQclear(pg_result); + +/* Non-NULL result indicates a sucessful result */ + +if (result != NULL) + { + *resultptr = result; + return OK; + } +else + { + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return yield; /* FAIL or DEFER */ + } +} + + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. The handle and filename +arguments are not used. Loop through a list of servers while the query is +deferred with a retryable error. */ + +int +pgsql_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +int sep = 0; +uschar *server; +uschar *list = pgsql_servers; +uschar buffer[512]; + +DEBUG(D_lookup) debug_printf("PGSQL query: %s\n", query); + +while ((server = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) + != NULL) + { + BOOL defer_break; + int rc = perform_pgsql_search(query, server, result, errmsg, &defer_break, + do_cache); + if (rc != DEFER || defer_break) return rc; + } + +if (pgsql_servers == NULL) + *errmsg = US"no PGSQL servers defined (pgsql_servers option)"; + +return DEFER; +} + + + +/************************************************* +* Quote entry point * +*************************************************/ + +/* The characters that always need to be quoted (with backslash) are newline, +tab, carriage return, backspace, backslash itself, and the quote characters. +Percent and underscore are only special in contexts where they can be wild +cards, and this isn't usually the case for data inserted from messages, since +that isn't likely to be treated as a pattern of any kind. However, pgsql seems +to allow escaping "on spec". If you use something like "where id="ab\%cd" it +does treat the string as "ab%cd". So we can safely quote percent and +underscore. [This is different to MySQL, where you can't do this.] + +Arguments: + s the string to be quoted + opt additional option text or NULL if none + +Returns: the processed string or NULL for a bad option +*/ + +uschar * +pgsql_quote(uschar *s, uschar *opt) +{ +register int c; +int count = 0; +uschar *t = s; +uschar *quoted; + +if (opt != NULL) return NULL; /* No options recognized */ + +while ((c = *t++) != 0) + if (Ustrchr("\n\t\r\b\'\"\\%_", c) != NULL) count++; + +if (count == 0) return s; +t = quoted = store_get(Ustrlen(s) + count + 1); + +while ((c = *s++) != 0) + { + if (Ustrchr("\n\t\r\b\'\"\\%_", c) != NULL) + { + *t++ = '\\'; + switch(c) + { + case '\n': *t++ = 'n'; + break; + case '\t': *t++ = 't'; + break; + case '\r': *t++ = 'r'; + break; + case '\b': *t++ = 'b'; + break; + default: *t++ = c; + break; + } + } + else *t++ = c; + } + +*t = 0; +return quoted; +} + +#endif /* PGSQL_LOOKUP */ + +/* End of lookups/pgsql.c */ diff --git a/src/src/lookups/pgsql.h b/src/src/lookups/pgsql.h new file mode 100644 index 000000000..6ab5f3700 --- /dev/null +++ b/src/src/lookups/pgsql.h @@ -0,0 +1,18 @@ +/* $Cambridge: exim/src/src/lookups/pgsql.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the pgsql lookup functions */ + +extern void *pgsql_open(uschar *, uschar **); +extern int pgsql_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); +extern void pgsql_tidy(void); +extern uschar *pgsql_quote(uschar *, uschar *); + +/* End of lookups/pgsql.h */ diff --git a/src/src/lookups/testdb.c b/src/src/lookups/testdb.c new file mode 100644 index 000000000..30b465110 --- /dev/null +++ b/src/src/lookups/testdb.c @@ -0,0 +1,69 @@ +/* $Cambridge: exim/src/src/lookups/testdb.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "lf_functions.h" +#include "testdb.h" + + +/* These are not real lookup functions; they are just a way of testing the +rest of Exim by providing an easy way of specifying particular yields from +the find function. */ + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +testdb_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +return (void *)(1); /* Just return something non-null */ +} + + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. */ + +int +testdb_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; + +if (Ustrcmp(query, "fail") == 0) + { + *errmsg = US"testdb lookup forced FAIL"; + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return FAIL; + } +if (Ustrcmp(query, "defer") == 0) + { + *errmsg = US"testdb lookup forced DEFER"; + DEBUG(D_lookup) debug_printf("%s\n", *errmsg); + return DEFER; + } + +if (Ustrcmp(query, "nocache") == 0) *do_cache = FALSE; + +*result = string_copy(query); +return OK; +} + +/* End of lookups/testdb.c */ diff --git a/src/src/lookups/testdb.h b/src/src/lookups/testdb.h new file mode 100644 index 000000000..d8e0207a5 --- /dev/null +++ b/src/src/lookups/testdb.h @@ -0,0 +1,16 @@ +/* $Cambridge: exim/src/src/lookups/testdb.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the testdb lookup */ + +extern void *testdb_open(uschar *, uschar **); +extern int testdb_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); + +/* End of lookups/testdb.h */ diff --git a/src/src/lookups/whoson.c b/src/src/lookups/whoson.c new file mode 100644 index 000000000..a3212e6ba --- /dev/null +++ b/src/src/lookups/whoson.c @@ -0,0 +1,82 @@ +/* $Cambridge: exim/src/src/lookups/whoson.c,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This code originally came from Robert Wal. */ + +#include "../exim.h" + + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the WHOSON headers +available for compiling. Therefore, compile these functions only if +LOOKUP_WHOSON is defined. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef LOOKUP_WHOSON +static void dummy(int x) { dummy(x-1); } +#else + + +#include /* Public header */ +#include "whoson.h" /* Local header */ + + +/************************************************* +* Open entry point * +*************************************************/ + +/* See local README for interface description. */ + +void * +whoson_open(uschar *filename, uschar **errmsg) +{ +filename = filename; /* Keep picky compilers happy */ +errmsg = errmsg; +return (void *)(1); /* Just return something non-null */ +} + + +/************************************************* +* Find entry point * +*************************************************/ + +/* See local README for interface description. */ + +int +whoson_find(void *handle, uschar *filename, uschar *query, int length, + uschar **result, uschar **errmsg, BOOL *do_cache) +{ +uschar buffer[80]; +handle = handle; /* Keep picky compilers happy */ +filename = filename; +length = length; +errmsg = errmsg; +do_cache = do_cache; + +switch (wso_query(query, CS buffer, sizeof(buffer))) + { + case 0: + *result = string_copy(buffer); /* IP in database; return name of user */ + return OK; + + case +1: + return FAIL; /* IP not in database */ + + default: + *errmsg = string_sprintf("WHOSON: failed to complete: %s", buffer); + return DEFER; + } +} + +#endif /* LOOKUP_WHOSON */ + +/* End of lookups/whoson.c */ diff --git a/src/src/lookups/whoson.h b/src/src/lookups/whoson.h new file mode 100644 index 000000000..f49daf535 --- /dev/null +++ b/src/src/lookups/whoson.h @@ -0,0 +1,16 @@ +/* $Cambridge: exim/src/src/lookups/whoson.h,v 1.1 2004/10/07 13:10:01 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the whoson lookup */ + +extern void *whoson_open(uschar *, uschar **); +extern int whoson_find(void *, uschar *, uschar *, int, uschar **, uschar **, + BOOL *); + +/* End of lookups/whoson.h */ diff --git a/src/src/routers/Makefile b/src/src/routers/Makefile new file mode 100644 index 000000000..5e8ddeebc --- /dev/null +++ b/src/src/routers/Makefile @@ -0,0 +1,44 @@ +# $Cambridge: exim/src/src/routers/Makefile,v 1.1 2004/10/07 13:10:02 ph10 Exp $ + +# Make file for building a library containing all the available routers and +# calling it routers.a. This is called from the main make file, after cd'ing +# to the directors subdirectory. The library also contains functions that +# are called only from within the individual routers. + +OBJ = accept.o dnslookup.o ipliteral.o iplookup.o manualroute.o \ + queryprogram.o redirect.o \ + rf_change_domain.o rf_expand_data.o rf_get_errors_address.o \ + rf_get_munge_headers.o rf_get_transport.o rf_get_ugid.o \ + rf_lookup_hostlist.o \ + rf_queue_add.o rf_self_action.o \ + rf_set_ugid.o + +routers.a: $(OBJ) + /bin/rm -f routers.a + $(AR) routers.a $(OBJ) + $(RANLIB) $@ + /bin/rm -rf ../drtables.o + +.SUFFIXES: .o .c +.c.o:; $(CC) -c $(CFLAGS) $(INCLUDE) $*.c + +rf_change_domain.o: $(HDRS) rf_change_domain.c rf_functions.h +rf_expand_data.o: $(HDRS) rf_expand_data.c rf_functions.h +rf_get_errors_address.o: $(HDRS) rf_get_errors_address.c rf_functions.h +rf_get_munge_headers.o: $(HDRS) rf_get_munge_headers.c rf_functions.h +rf_get_transport.o: $(HDRS) rf_get_transport.c rf_functions.h +rf_get_ugid.o: $(HDRS) rf_get_ugid.c rf_functions.h +rf_lookup_hostlist.o: $(HDRS) rf_lookup_hostlist.c rf_functions.h +rf_queue_add.o: $(HDRS) rf_queue_add.c rf_functions.h +rf_self_action.o: $(HDRS) rf_self_action.c rf_functions.h +rf_set_ugid.o: $(HDRS) rf_set_ugid.c rf_functions.h + +accept.o: $(HDRS) accept.c rf_functions.h accept.h +dnslookup.o: $(HDRS) dnslookup.c rf_functions.h dnslookup.h +ipliteral.o: $(HDRS) ipliteral.c rf_functions.h ipliteral.h +iplookup.o: $(HDRS) iplookup.c rf_functions.h iplookup.h +manualroute.o: $(HDRS) manualroute.c rf_functions.h manualroute.h +queryprogram.o: $(HDRS) queryprogram.c rf_functions.h queryprogram.h +redirect.o: $(HDRS) redirect.c rf_functions.h redirect.h + +# End diff --git a/src/src/routers/README b/src/src/routers/README new file mode 100644 index 000000000..830ba3059 --- /dev/null +++ b/src/src/routers/README @@ -0,0 +1,59 @@ +$Cambridge: exim/src/src/routers/README,v 1.1 2004/10/07 13:10:02 ph10 Exp $ + +ROUTERS: + +The yield of a router is one of: + + OK the address was routed and either added to one of the + addr_local or addr_remote chains, or one or more new + addresses were added to addr_new. The original may be added + to addr_succeed. + + REROUTED this is used when a child address is created and added to + addr_new as a consequence of a domain widening or because + "self = reroute" was encountered. The only time it is handled + differently from OK is when verifying, to force it to + continue with the child address. + + DECLINE the address was not routed; pass to next router unless + no_more is set. It is permitted for additional addresses to + have been added to addr_new (or indeed for addresses to have + been put on the other chains). + + PASS the address was not routed, but might have been modified; + pass to the next router unconditionally. + + DISCARD the address was discarded (:blackhole: or "seen finish") + + FAIL the address was not routed; do not pass to any subseqent + routers, i.e. cause routing to fail. + + DEFER retry this address later. + +Both ERROR and DEFER cause the message to be kept on the queue; either may +request freezing, but nowadays we try not to request freezing from routers +because it may hold up other addresses in the message. + + +When routing succeeds, the following field in the address can be set: + + transport points to the transport instance control block + + uid, gid are the uid and gid under which a local transport is to be + run if the transport does not itself specify them. + + initgroups is set true if initgroups() is to be called when using the + uid and gid set up by the router. + + fallback_hosts fallback host list - relevant only if the router sets up + a remote transport for the address. + + errors_address where to send error messages for this address. + + extra_headers additional headers to be added to the message for this + address. + + remove_headers the names of headers to be removed from the message for this + address + +**** diff --git a/src/src/routers/accept.c b/src/src/routers/accept.c new file mode 100644 index 000000000..6e5fa721e --- /dev/null +++ b/src/src/routers/accept.c @@ -0,0 +1,130 @@ +/* $Cambridge: exim/src/src/routers/accept.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" +#include "accept.h" + + +/* Options specific to the accept router. Because some compilers do not like +empty declarations ("undefined" in the Standard) we put in a dummy value. */ + +optionlist accept_router_options[] = { + { "", opt_hidden, NULL } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int accept_router_options_count = + sizeof(accept_router_options)/sizeof(optionlist); + +/* Default private options block for the accept router. Again, a dummy +value is used. */ + +accept_router_options_block accept_router_option_defaults = { + NULL /* dummy */ +}; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to enable +consistency checks to be done, or anything else that needs to be set up. */ + +void accept_router_init(router_instance *rblock) +{ +/* +accept_router_options_block *ob = + (accept_router_options_block *)(rblock->options_block); +*/ + +/* By default, log deliveries via this router as local deliveries. We can't +just leave it as TRUE_UNSET, because the global default is FALSE. */ + +if (rblock->log_as_local == TRUE_UNSET) rblock->log_as_local = TRUE; +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface description. This router returns: + +DEFER + . verifying the errors address caused a deferment or a big disaster such + as an expansion failure (rf_get_errors_address) + . expanding a headers_{add,remove} string caused a deferment or another + expansion error (rf_get_munge_headers) + . a problem in rf_get_transport: no transport when one is needed; + failed to expand dynamic transport; failed to find dynamic transport + . failure to expand or find a uid/gid (rf_get_ugid via rf_queue_add) + +OK + added address to addr_local or addr_remote, as appropriate for the + type of transport +*/ + +int accept_router_entry( + router_instance *rblock, /* data for this instantiation */ + address_item *addr, /* address we are working on */ + struct passwd *pw, /* passwd entry after check_local_user */ + BOOL verify, /* TRUE when verifying */ + address_item **addr_local, /* add it to this if it's local */ + address_item **addr_remote, /* add it to this if it's remote */ + address_item **addr_new, /* put new addresses on here */ + address_item **addr_succeed) /* put old address here on success */ +{ +/* +accept_router_options_block *ob = + (accept_router_options_block *)(rblock->options_block); +*/ +int rc; +uschar *errors_to; +uschar *remove_headers; +header_line *extra_headers; + +addr_new = addr_new; /* Keep picky compilers happy */ +addr_succeed = addr_succeed; + +DEBUG(D_route) debug_printf("%s router called for %s\n domain = %s\n", + rblock->name, addr->address, addr->domain); + +/* Set up the errors address, if any. */ + +rc = rf_get_errors_address(addr, rblock, verify, &errors_to); +if (rc != OK) return rc; + +/* Set up the additional and removeable headers for the address. */ + +rc = rf_get_munge_headers(addr, rblock, &extra_headers, &remove_headers); +if (rc != OK) return rc; + +/* Set the transport and accept the address; update its errors address and +header munging. Initialization ensures that there is a transport except when +verifying. */ + +if (!rf_get_transport(rblock->transport_name, &(rblock->transport), + addr, rblock->name, NULL)) return DEFER; + +addr->transport = rblock->transport; +addr->p.errors_address = errors_to; +addr->p.extra_headers = extra_headers; +addr->p.remove_headers = remove_headers; + +return rf_queue_add(addr, addr_local, addr_remote, rblock, pw)? OK : DEFER; +} + +/* End of routers/accept.c */ diff --git a/src/src/routers/accept.h b/src/src/routers/accept.h new file mode 100644 index 000000000..ff78a32e4 --- /dev/null +++ b/src/src/routers/accept.h @@ -0,0 +1,33 @@ +/* $Cambridge: exim/src/src/routers/accept.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options (there aren't any). */ + +typedef struct { + uschar *dummy; +} accept_router_options_block; + +/* Data for reading the private options. */ + +extern optionlist accept_router_options[]; +extern int accept_router_options_count; + +/* Block containing default values. */ + +extern accept_router_options_block accept_router_option_defaults; + +/* The main and initialization entry points for the router */ + +extern int accept_router_entry(router_instance *, address_item *, + struct passwd *, BOOL, address_item **, address_item **, + address_item **, address_item **); + +extern void accept_router_init(router_instance *); + +/* End of routers/accept.h */ diff --git a/src/src/routers/dnslookup.c b/src/src/routers/dnslookup.c new file mode 100644 index 000000000..7789d4636 --- /dev/null +++ b/src/src/routers/dnslookup.c @@ -0,0 +1,408 @@ +/* $Cambridge: exim/src/src/routers/dnslookup.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" +#include "dnslookup.h" + + + +/* Options specific to the dnslookup router. */ + +optionlist dnslookup_router_options[] = { + { "check_secondary_mx", opt_bool, + (void *)(offsetof(dnslookup_router_options_block, check_secondary_mx)) }, + { "check_srv", opt_stringptr, + (void *)(offsetof(dnslookup_router_options_block, check_srv)) }, + { "mx_domains", opt_stringptr, + (void *)(offsetof(dnslookup_router_options_block, mx_domains)) }, + { "mx_fail_domains", opt_stringptr, + (void *)(offsetof(dnslookup_router_options_block, mx_fail_domains)) }, + { "qualify_single", opt_bool, + (void *)(offsetof(dnslookup_router_options_block, qualify_single)) }, + { "rewrite_headers", opt_bool, + (void *)(offsetof(dnslookup_router_options_block, rewrite_headers)) }, + { "same_domain_copy_routing", opt_bool|opt_public, + (void *)(offsetof(router_instance, same_domain_copy_routing)) }, + { "search_parents", opt_bool, + (void *)(offsetof(dnslookup_router_options_block, search_parents)) }, + { "srv_fail_domains", opt_stringptr, + (void *)(offsetof(dnslookup_router_options_block, srv_fail_domains)) }, + { "widen_domains", opt_stringptr, + (void *)(offsetof(dnslookup_router_options_block, widen_domains)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int dnslookup_router_options_count = + sizeof(dnslookup_router_options)/sizeof(optionlist); + +/* Default private options block for the dnslookup router. */ + +dnslookup_router_options_block dnslookup_router_option_defaults = { + FALSE, /* check_secondary_mx */ + TRUE, /* qualify_single */ + FALSE, /* search_parents */ + TRUE, /* rewrite_headers */ + NULL, /* widen_domains */ + NULL, /* mx_domains */ + NULL, /* mx_fail_domains */ + NULL, /* srv_fail_domains */ + NULL /* check_srv */ +}; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to enable +consistency checks to be done, or anything else that needs to be set up. */ + +void +dnslookup_router_init(router_instance *rblock) +{ +/* +dnslookup_router_options_block *ob = + (dnslookup_router_options_block *)(rblock->options_block); +*/ +rblock = rblock; +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. This router returns: + +DECLINE + . the domain does not exist in the DNS + . MX records point to non-existent hosts (including RHS = IP address) + . a single SRV record has a host name of "." (=> no service) + . syntactically invalid mail domain + . check_secondary_mx set, and local host not in host list + +DEFER + . lookup defer for mx_domains + . timeout etc on DNS lookup + . verifying the errors address caused a deferment or a big disaster such + as an expansion failure (rf_get_errors_address) + . expanding a headers_{add,remove} string caused a deferment or another + expansion error (rf_get_munge_headers) + . a problem in rf_get_transport: no transport when one is needed; + failed to expand dynamic transport; failed to find dynamic transport + . failure to expand or find a uid/gid (rf_get_ugid via rf_queue_add) + . self = "freeze", self = "defer" + +PASS + . timeout etc on DNS lookup and pass_on_timeout set + . self = "pass" + +REROUTED + . routed to local host, but name was expanded by DNS lookup, so a + re-routing should take place + . self = "reroute" + + In both cases the new address will have been set up as a child + +FAIL + . self = "fail" + +OK + added address to addr_local or addr_remote, as appropriate for the + type of transport; this includes the self="send" case. +*/ + +int +dnslookup_router_entry( + router_instance *rblock, /* data for this instantiation */ + address_item *addr, /* address we are working on */ + struct passwd *pw, /* passwd entry after check_local_user */ + BOOL verify, /* TRUE when verifying */ + address_item **addr_local, /* add it to this if it's local */ + address_item **addr_remote, /* add it to this if it's remote */ + address_item **addr_new, /* put new addresses on here */ + address_item **addr_succeed) /* put old address here on success */ +{ +host_item h; +int rc; +int widen_sep = 0; +int whichrrs = HOST_FIND_BY_MX | HOST_FIND_BY_A; +dnslookup_router_options_block *ob = + (dnslookup_router_options_block *)(rblock->options_block); +uschar *srv_service = NULL; +uschar *widen = NULL; +uschar *pre_widen = addr->domain; +uschar *post_widen = NULL; +uschar *fully_qualified_name; +uschar *listptr; +uschar widen_buffer[256]; + +addr_new = addr_new; /* Keep picky compilers happy */ +addr_succeed = addr_succeed; + +DEBUG(D_route) + debug_printf("%s router called for %s\n domain = %s\n", + rblock->name, addr->address, addr->domain); + +/* If an SRV check is required, expand the service name */ + +if (ob->check_srv != NULL) + { + srv_service = expand_string(ob->check_srv); + if (srv_service == NULL && !expand_string_forcedfail) + { + addr->message = string_sprintf("%s router: failed to expand \"%s\": %s", + rblock->name, ob->check_srv, expand_string_message); + return DEFER; + } + else whichrrs |= HOST_FIND_BY_SRV; + } + +/* Set up the first of any widening domains. The code further down copes with +either pre- or post-widening, but at present there is no way to turn on +pre-widening, as actually doing so seems like a rather bad idea, and nobody has +requested it. Pre-widening would cause local abbreviated names to take +precedence over global names. For example, if the domain is "xxx.ch" it might +be something in the "ch" toplevel domain, but it also might be xxx.ch.xyz.com. +The choice of pre- or post-widening affects which takes precedence. If ever +somebody comes up with some kind of requirement for pre-widening, presumably +with some conditions under which it is done, it can be selected here. */ + +if (ob->widen_domains != NULL) + { + listptr = ob->widen_domains; + widen = string_nextinlist(&listptr, &widen_sep, widen_buffer, + sizeof(widen_buffer)); + +/**** + if (some condition requiring pre-widening) + { + post_widen = pre_widen; + pre_widen = NULL; + } +****/ + } + +/* Loop to cope with explicit widening of domains as configured. This code +copes with widening that may happen before or after the original name. The +decision as to which is taken above. */ + +for (;;) + { + int flags = whichrrs; + BOOL removed = FALSE; + + if (pre_widen != NULL) + { + h.name = pre_widen; + pre_widen = NULL; + } + else if (widen != NULL) + { + h.name = string_sprintf("%s.%s", addr->domain, widen); + widen = string_nextinlist(&listptr, &widen_sep, widen_buffer, + sizeof(widen_buffer)); + DEBUG(D_route) debug_printf("%s router widened %s to %s\n", rblock->name, + addr->domain, h.name); + } + else if (post_widen != NULL) + { + h.name = post_widen; + post_widen = NULL; + DEBUG(D_route) debug_printf("%s router trying %s after widening failed\n", + rblock->name, h.name); + } + else return DECLINE; + + /* Set up the rest of the initial host item. Others may get chained on if + there is more than one IP address. We set it up here instead of outside the + loop so as to re-initialize if a previous try succeeded but was rejected + because of not having an MX record. */ + + h.next = NULL; + h.address = NULL; + h.port = PORT_NONE; + h.mx = MX_NONE; + h.status = hstatus_unknown; + h.why = hwhy_unknown; + h.last_try = 0; + + /* Unfortunately, we cannot set the mx_only option in advance, because the + DNS lookup may extend an unqualified name. Therefore, we must do the test + subsequently. */ + + if (ob->qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE; + if (ob->search_parents) flags |= HOST_FIND_SEARCH_PARENTS; + + rc = host_find_bydns(&h, rblock->ignore_target_hosts, flags, srv_service, + ob->srv_fail_domains, ob->mx_fail_domains, &fully_qualified_name, &removed); + if (removed) setflag(addr, af_local_host_removed); + + /* If host found with only address records, test for the domain's being in + the mx_domains list. Note that this applies also to SRV records; the name of + the option is historical. */ + + if ((rc == HOST_FOUND || rc == HOST_FOUND_LOCAL) && h.mx < 0 && + ob->mx_domains != NULL) + { + switch(match_isinlist(fully_qualified_name, &(ob->mx_domains), 0, + &domainlist_anchor, addr->domain_cache, MCL_DOMAIN, TRUE, NULL)) + { + case DEFER: + addr->message = US"lookup defer for mx_domains"; + return DEFER; + + case OK: + DEBUG(D_route) debug_printf("%s router rejected %s: no MX record(s)\n", + rblock->name, fully_qualified_name); + continue; + } + } + + /* Deferral returns forthwith, and anything other than failure breaks the + loop. */ + + if (rc == HOST_FIND_AGAIN) + { + if (rblock->pass_on_timeout) + { + DEBUG(D_route) debug_printf("%s router timed out, and pass_on_timeout is set\n", + rblock->name); + return PASS; + } + addr->message = US"host lookup did not complete"; + return DEFER; + } + + if (rc != HOST_FIND_FAILED) break; + + /* Check to see if the failure is the result of MX records pointing + to non-existent domains, and if so, set an appropriate error message; the + case of an SRV record pointing to "." is another special case that we can + detect. Otherwise "unknown mail domain" is used, which is confusing. Also, in + this case don't do the widening. We need check only the first host to see if + its MX has been filled in, but there is no address, because if there were any + usable addresses returned, we would not have had HOST_FIND_FAILED. + + As a common cause of this problem is MX records with IP addresses on the + RHS, give a special message in this case. */ + + if (h.mx >= 0 && h.address == NULL) + { + if (h.name[0] == 0) + addr->message = US"an SRV record indicated no SMTP service"; + else + { + addr->message = US"all relevant MX records point to non-existent hosts"; + if (!allow_mx_to_ip && string_is_ip_address(h.name, NULL)) + { + addr->user_message = + string_sprintf("It appears that the DNS operator for %s\n" + "has installed an invalid MX record with an IP address\n" + "instead of a domain name on the right hand side.", addr->domain); + addr->message = string_sprintf("%s or (invalidly) to IP addresses", + addr->message); + } + } + return DECLINE; + } + + /* If there's a syntax error, do not continue with any widening, and note + the error. */ + + if (host_find_failed_syntax) + { + addr->message = string_sprintf("mail domain \"%s\" is syntactically " + "invalid", h.name); + return DECLINE; + } + } + +/* If the original domain name has been changed as a result of the host lookup, +set up a child address for rerouting and request header rewrites if so +configured. Then yield REROUTED. However, if the only change is a change of +case in the domain name, which some resolvers yield (others don't), just change +the domain name in the original address so that the official version is used in +RCPT commands. */ + +if (Ustrcmp(addr->domain, fully_qualified_name) != 0) + { + if (strcmpic(addr->domain, fully_qualified_name) == 0) + { + uschar *at = Ustrrchr(addr->address, '@'); + memcpy(at+1, fully_qualified_name, Ustrlen(at+1)); + } + else + { + rf_change_domain(addr, fully_qualified_name, ob->rewrite_headers, addr_new); + return REROUTED; + } + } + +/* If the yield is HOST_FOUND_LOCAL, the remote domain name either found MX +records with the lowest numbered one pointing to a host with an IP address that +is set on one of the interfaces of this machine, or found A records or got +addresses from gethostbyname() that contain one for this machine. This can +happen quite legitimately if the original name was a shortened form of a +domain, but we will have picked that up already via the name change test above. + +Otherwise, the action to be taken can be configured by the self option, the +handling of which is in a separate function, as it is also required for other +routers. */ + +if (rc == HOST_FOUND_LOCAL) + { + rc = rf_self_action(addr, &h, rblock->self_code, rblock->self_rewrite, + rblock->self, addr_new); + if (rc != OK) return rc; + } + +/* Otherwise, insist on being a secondary MX if so configured */ + +else if (ob->check_secondary_mx && !testflag(addr, af_local_host_removed)) + { + DEBUG(D_route) debug_printf("check_secondary_mx set and local host not secondary\n"); + return DECLINE; + } + +/* Set up the errors address, if any. */ + +rc = rf_get_errors_address(addr, rblock, verify, &(addr->p.errors_address)); +if (rc != OK) return rc; + +/* Set up the additional and removeable headers for this address. */ + +rc = rf_get_munge_headers(addr, rblock, &(addr->p.extra_headers), + &(addr->p.remove_headers)); +if (rc != OK) return rc; + +/* Get store in which to preserve the original host item, chained on +to the address. */ + +addr->host_list = store_get(sizeof(host_item)); +addr->host_list[0] = h; + +/* Fill in the transport and queue the address for delivery. */ + +if (!rf_get_transport(rblock->transport_name, &(rblock->transport), + addr, rblock->name, NULL)) + return DEFER; + +addr->transport = rblock->transport; + +return rf_queue_add(addr, addr_local, addr_remote, rblock, pw)? + OK : DEFER; +} + +/* End of routers/dnslookup.c */ diff --git a/src/src/routers/dnslookup.h b/src/src/routers/dnslookup.h new file mode 100644 index 000000000..64867831f --- /dev/null +++ b/src/src/routers/dnslookup.h @@ -0,0 +1,41 @@ +/* $Cambridge: exim/src/src/routers/dnslookup.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + BOOL check_secondary_mx; + BOOL qualify_single; + BOOL search_parents; + BOOL rewrite_headers; + uschar *widen_domains; + uschar *mx_domains; + uschar *mx_fail_domains; + uschar *srv_fail_domains; + uschar *check_srv; +} dnslookup_router_options_block; + +/* Data for reading the private options. */ + +extern optionlist dnslookup_router_options[]; +extern int dnslookup_router_options_count; + +/* Block containing default values. */ + +extern dnslookup_router_options_block dnslookup_router_option_defaults; + +/* The main and initialization entry points for the router */ + +extern int dnslookup_router_entry(router_instance *, address_item *, + struct passwd *, BOOL, address_item **, address_item **, + address_item **, address_item **); + +extern void dnslookup_router_init(router_instance *); + +/* End of routers/dnslookup.h */ diff --git a/src/src/routers/ipliteral.c b/src/src/routers/ipliteral.c new file mode 100644 index 000000000..fa41cc411 --- /dev/null +++ b/src/src/routers/ipliteral.c @@ -0,0 +1,195 @@ +/* $Cambridge: exim/src/src/routers/ipliteral.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" +#include "ipliteral.h" + + +/* Options specific to the ipliteral router. Because some compilers do not like +empty declarations ("undefined" in the Standard) we put in a dummy value. */ + +optionlist ipliteral_router_options[] = { + { "", opt_hidden, NULL } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int ipliteral_router_options_count = + sizeof(ipliteral_router_options)/sizeof(optionlist); + +/* Default private options block for the ipliteral router. Again, a dummy +value is present to keep some compilers happy. */ + +ipliteral_router_options_block ipliteral_router_option_defaults = { 0 }; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to enable +consistency checks to be done, or anything else that needs to be set up. */ + +void +ipliteral_router_init(router_instance *rblock) +{ +/* +ipliteral_router_options_block *ob = + (ipliteral_router_options_block *)(rblock->options_block); +*/ +rblock = rblock; +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. This router returns: + +DECLINE + . the domain is not in the form of an IP literal + +DEFER + . verifying the errors address caused a deferment or a big disaster such + as an expansion failure (rf_get_errors_address) + . expanding a headers_{add,remove} string caused a deferment or another + expansion error (rf_get_munge_headers) + . a problem in rf_get_transport: no transport when one is needed; + failed to expand dynamic transport; failed to find dynamic transport + . failure to expand or find a uid/gid (rf_get_ugid via rf_queue_add) + . self = "freeze", self = "defer" + +PASS + . self = "pass" + +REROUTED + . self = "reroute" + +FAIL + . self = "fail" + +OK + added address to addr_local or addr_remote, as appropriate for the + type of transport; this includes the self="send" case. +*/ + +int +ipliteral_router_entry( + router_instance *rblock, /* data for this instantiation */ + address_item *addr, /* address we are working on */ + struct passwd *pw, /* passwd entry after check_local_user */ + BOOL verify, /* TRUE when verifying */ + address_item **addr_local, /* add it to this if it's local */ + address_item **addr_remote, /* add it to this if it's remote */ + address_item **addr_new, /* put new addresses on here */ + address_item **addr_succeed) /* put old address here on success */ +{ +/* +ipliteral_router_options_block *ob = + (ipliteral_router_options_block *)(rblock->options_block); +*/ +host_item *h; +uschar *domain = addr->domain; +int len = Ustrlen(domain); +int rc; + +addr_new = addr_new; /* Keep picky compilers happy */ +addr_succeed = addr_succeed; + +DEBUG(D_route) debug_printf("%s router called for %s: domain = %s\n", + rblock->name, addr->address, addr->domain); + +/* Check that the domain is an IP address enclosed in square brackets. If +not, the router declines. Otherwise route to the single IP address, setting the +host name to "(unnamed)". */ + +if (domain[0] != '[' || domain[len-1] != ']') return DECLINE; +domain[len-1] = 0; /* temporarily */ + +if (!string_is_ip_address(domain+1, NULL)) + { + domain[len-1] = ']'; + return DECLINE; + } + +/* It seems unlikely that ignore_target_hosts will be used with this router, +but if it is set, it should probably work. */ + +if (verify_check_this_host(&(rblock->ignore_target_hosts), NULL, NULL, + domain + 1, NULL) == OK) + { + DEBUG(D_route) + debug_printf("%s is in ignore_target_hosts\n", domain+1); + addr->message = US"IP literal host explicitly ignored"; + domain[len-1] = ']'; + return DECLINE; + } + +/* Set up a host item */ + +h = store_get(sizeof(host_item)); + +h->next = NULL; +h->address = string_copy(domain+1); +h->port = PORT_NONE; +domain[len-1] = ']'; /* restore */ +h->name = string_copy(domain); +h->mx = MX_NONE; +h->status = hstatus_unknown; +h->why = hwhy_unknown; +h->last_try = 0; + +/* Determine whether the host is the local host, and if so, take action +according to the configuration. */ + +if (host_scan_for_local_hosts(h, &h, NULL) == HOST_FOUND_LOCAL) + { + int rc = rf_self_action(addr, h, rblock->self_code, rblock->self_rewrite, + rblock->self, addr_new); + if (rc != OK) return rc; + } + +/* Address is routed to this host */ + +addr->host_list = h; + +/* Set up the errors address, if any. */ + +rc = rf_get_errors_address(addr, rblock, verify, &(addr->p.errors_address)); +if (rc != OK) return rc; + +/* Set up the additional and removeable headers for this address. */ + +rc = rf_get_munge_headers(addr, rblock, &(addr->p.extra_headers), + &(addr->p.remove_headers)); +if (rc != OK) return rc; + +/* Fill in the transport, queue the address for local or remote delivery, and +yield success. For local delivery, of course, the IP address won't be used. If +just verifying, there need not be a transport, in which case it doesn't matter +which queue we put the address on. This is all now handled by the route_queue() +function. */ + +if (!rf_get_transport(rblock->transport_name, &(rblock->transport), + addr, rblock->name, NULL)) + return DEFER; + +addr->transport = rblock->transport; + +return rf_queue_add(addr, addr_local, addr_remote, rblock, pw)? + OK : DEFER; +} + +/* End of routers/ipliteral.c */ diff --git a/src/src/routers/ipliteral.h b/src/src/routers/ipliteral.h new file mode 100644 index 000000000..cedf82979 --- /dev/null +++ b/src/src/routers/ipliteral.h @@ -0,0 +1,36 @@ +/* $Cambridge: exim/src/src/routers/ipliteral.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +/* Private structure for the private options. Some compilers do not like empty +structures - the Standard, alas, says "undefined behaviour" for an empty +structure - so we have to put in a dummy value. */ + +typedef struct { + int dummy; +} ipliteral_router_options_block; + +/* Data for reading the private options. */ + +extern optionlist ipliteral_router_options[]; +extern int ipliteral_router_options_count; + +/* Block containing default values. */ + +extern ipliteral_router_options_block ipliteral_router_option_defaults; + +/* The main and initialization entry points for the router */ + +extern int ipliteral_router_entry(router_instance *, address_item *, + struct passwd *, BOOL, address_item **, address_item **, + address_item **, address_item **); + +extern void ipliteral_router_init(router_instance *); + +/* End of routers/ipliteral.h */ diff --git a/src/src/routers/iplookup.c b/src/src/routers/iplookup.c new file mode 100644 index 000000000..7d419efad --- /dev/null +++ b/src/src/routers/iplookup.c @@ -0,0 +1,397 @@ +/* $Cambridge: exim/src/src/routers/iplookup.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" +#include "iplookup.h" + + +/* IP connection types */ + +#define ip_udp 0 +#define ip_tcp 1 + + +/* Options specific to the iplookup router. */ + +optionlist iplookup_router_options[] = { + { "hosts", opt_stringptr, + (void *)(offsetof(iplookup_router_options_block, hosts)) }, + { "optional", opt_bool, + (void *)(offsetof(iplookup_router_options_block, optional)) }, + { "port", opt_int, + (void *)(offsetof(iplookup_router_options_block, port)) }, + { "protocol", opt_stringptr, + (void *)(offsetof(iplookup_router_options_block, protocol_name)) }, + { "query", opt_stringptr, + (void *)(offsetof(iplookup_router_options_block, query)) }, + { "reroute", opt_stringptr, + (void *)(offsetof(iplookup_router_options_block, reroute)) }, + { "response_pattern", opt_stringptr, + (void *)(offsetof(iplookup_router_options_block, response_pattern)) }, + { "timeout", opt_time, + (void *)(offsetof(iplookup_router_options_block, timeout)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int iplookup_router_options_count = + sizeof(iplookup_router_options)/sizeof(optionlist); + +/* Default private options block for the iplookup router. */ + +iplookup_router_options_block iplookup_router_option_defaults = { + -1, /* port */ + ip_udp, /* protocol */ + 5, /* timeout */ + NULL, /* protocol_name */ + NULL, /* hosts */ + NULL, /* query; NULL => local_part@domain */ + NULL, /* response_pattern; NULL => don't apply regex */ + NULL, /* reroute; NULL => just used returned data */ + NULL, /* re_response_pattern; compiled pattern */ + FALSE /* optional */ +}; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to enable +consistency checks to be done, or anything else that needs to be set up. */ + +void +iplookup_router_init(router_instance *rblock) +{ +iplookup_router_options_block *ob = + (iplookup_router_options_block *)(rblock->options_block); + +/* A port and a host list must be given */ + +if (ob->port < 0) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "a port must be specified", rblock->name); + +if (ob->hosts == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "a host list must be specified", rblock->name); + +/* Translate protocol name into value */ + +if (ob->protocol_name != NULL) + { + if (Ustrcmp(ob->protocol_name, "udp") == 0) ob->protocol = ip_udp; + else if (Ustrcmp(ob->protocol_name, "tcp") == 0) ob->protocol = ip_tcp; + else log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "protocol not specified as udp or tcp", rblock->name); + } + +/* If a response pattern is given, compile it now to get the error early. */ + +if (ob->response_pattern != NULL) + ob->re_response_pattern = + regex_must_compile(ob->response_pattern, FALSE, TRUE); +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. This router returns: + +DECLINE + . pattern or identification match on returned data failed + +DEFER + . failed to expand the query or rerouting string + . failed to create socket ("optional" not set) + . failed to find a host, failed to connect, timed out ("optional" not set) + . rerouting string is not in the form localpart@domain + . verifying the errors address caused a deferment or a big disaster such + as an expansion failure (rf_get_errors_address) + . expanding a headers_{add,remove} string caused a deferment or another + expansion error (rf_get_munge_headers) + +PASS + . failed to create socket ("optional" set) + . failed to find a host, failed to connect, timed out ("optional" set) + +OK + . new address added to addr_new +*/ + +int +iplookup_router_entry( + router_instance *rblock, /* data for this instantiation */ + address_item *addr, /* address we are working on */ + struct passwd *pw, /* passwd entry after check_local_user */ + BOOL verify, /* TRUE when verifying */ + address_item **addr_local, /* add it to this if it's local */ + address_item **addr_remote, /* add it to this if it's remote */ + address_item **addr_new, /* put new addresses on here */ + address_item **addr_succeed) /* put old address here on success */ +{ +uschar *query = NULL; +uschar reply[256]; +uschar *hostname, *reroute, *domain, *listptr; +uschar host_buffer[256]; +host_item *host = store_get(sizeof(host_item)); +address_item *new_addr; +iplookup_router_options_block *ob = + (iplookup_router_options_block *)(rblock->options_block); +const pcre *re = ob->re_response_pattern; +int count, query_len, rc; +int sep = 0; + +addr_local = addr_local; /* Keep picky compilers happy */ +addr_remote = addr_remote; +addr_succeed = addr_succeed; +pw = pw; + +DEBUG(D_route) debug_printf("%s router called for %s: domain = %s\n", + rblock->name, addr->address, addr->domain); + +/* Build the query string to send. If not explicitly given, a default of +"user@domain user@domain" is used. */ + +if (ob->query == NULL) + query = string_sprintf("%s@%s %s@%s", addr->local_part, addr->domain, + addr->local_part, addr->domain); +else + { + query = expand_string(ob->query); + if (query == NULL) + { + addr->message = string_sprintf("%s router: failed to expand %s: %s", + rblock->name, ob->query, expand_string_message); + return DEFER; + } + } + +query_len = Ustrlen(query); +DEBUG(D_route) debug_printf("%s router query is \"%s\"\n", rblock->name, + string_printing(query)); + +/* Now connect to the required port for each of the hosts in turn, until a +response it received. Initialization insists on the port being set and there +being a host list. */ + +listptr = ob->hosts; +while ((hostname = string_nextinlist(&listptr, &sep, host_buffer, + sizeof(host_buffer))) != NULL) + { + host_item *h; + + DEBUG(D_route) debug_printf("calling host %s\n", hostname); + + host->name = hostname; + host->address = NULL; + host->port = PORT_NONE; + host->mx = MX_NONE; + host->next = NULL; + + if (string_is_ip_address(host->name, NULL)) + host->address = host->name; + else + { + int rc = host_find_byname(host, NULL, NULL, TRUE); + if (rc == HOST_FIND_FAILED || rc == HOST_FIND_AGAIN) continue; + } + + /* Loop for possible multiple IP addresses for the given name. */ + + for (h = host; h != NULL; h = h->next) + { + int host_af, query_socket; + + /* Skip any hosts for which we have no address */ + + if (h->address == NULL) continue; + + /* Create a socket, for UDP or TCP, as configured. IPv6 addresses are + detected by checking for a colon in the address. */ + + host_af = (Ustrchr(h->address, ':') != NULL)? AF_INET6 : AF_INET; + query_socket = ip_socket((ob->protocol == ip_udp)? SOCK_DGRAM:SOCK_STREAM, + host_af); + if (query_socket < 0) + { + if (ob->optional) return PASS; + addr->message = string_sprintf("failed to create socket in %s router", + rblock->name); + return DEFER; + } + + /* Connect to the remote host, under a timeout. In fact, timeouts can occur + here only for TCP calls; for a UDP socket, "connect" always works (the + router will timeout later on the read call). */ + + if (ip_connect(query_socket, host_af, h->address,ob->port, ob->timeout) < 0) + { + DEBUG(D_route) + debug_printf("connection to %s failed: %s\n", h->address, + strerror(errno)); + continue; + } + + /* Send the query. If it fails, just continue with the next address. */ + + if (send(query_socket, query, query_len, 0) < 0) + { + DEBUG(D_route) debug_printf("send to %s failed\n", h->address); + close(query_socket); + continue; + } + + /* Read the response and close the socket. If the read fails, try the + next IP address. */ + + count = ip_recv(query_socket, reply, sizeof(reply) - 1, ob->timeout); + close(query_socket); + if (count <= 0) + { + DEBUG(D_route) debug_printf("%s from %s\n", (errno == ETIMEDOUT)? + "timed out" : "recv failed", h->address); + *reply = 0; + continue; + } + + /* Success; break the loop */ + + reply[count] = 0; + DEBUG(D_route) debug_printf("%s router received \"%s\" from %s\n", + rblock->name, string_printing(reply), h->address); + break; + } + + /* If h == NULL we have tried all the IP addresses and failed on all of them, + so we must continue to try more host names. Otherwise we have succeeded. */ + + if (h != NULL) break; + } + + +/* If hostname is NULL, we have failed to find any host, or failed to +connect to any of the IP addresses, or timed out while reading or writing to +those we have connected to. In all cases, we must pass if optional and +defer otherwise. */ + +if (hostname == NULL) + { + DEBUG(D_route) debug_printf("%s router failed to get anything\n", rblock->name); + if (ob->optional) return PASS; + addr->message = string_sprintf("%s router: failed to communicate with any " + "host", rblock->name); + return DEFER; + } + + +/* If a response pattern was supplied, match the returned string against it. A +failure to match causes the router to decline. After a successful match, the +numerical variables for expanding the rerouted address are set up. */ + +if (re != NULL) + { + if (!regex_match_and_setup(re, reply, 0, -1)) + { + DEBUG(D_route) debug_printf("%s router: %s failed to match response %s\n", + rblock->name, ob->response_pattern, reply); + return DECLINE; + } + } + + +/* If no response pattern was supplied, set up $0 as the response up to the +first white space (if any). Also, if no query was specified, check that what +follows the white space matches user@domain. */ + +else + { + int n = 0; + while (reply[n] != 0 && !isspace(reply[n])) n++; + expand_nmax = 0; + expand_nstring[0] = reply; + expand_nlength[0] = n; + + if (ob->query == NULL) + { + int nn = n; + while (isspace(reply[nn])) nn++; + if (Ustrcmp(query + query_len/2 + 1, reply+nn) != 0) + { + DEBUG(D_route) debug_printf("%s router: failed to match identification " + "in response %s\n", rblock->name, reply); + return DECLINE; + } + } + + reply[n] = 0; /* Terminate for the default case */ + } + +/* If an explicit rerouting string is specified, expand it. Otherwise, use +what was sent back verbatim. */ + +if (ob->reroute != NULL) + { + reroute = expand_string(ob->reroute); + expand_nmax = -1; + if (reroute == NULL) + { + addr->message = string_sprintf("%s router: failed to expand %s: %s", + rblock->name, ob->reroute, expand_string_message); + return DEFER; + } + } +else reroute = reply; + +/* We should now have a new address in the form user@domain. */ + +domain = Ustrchr(reroute, '@'); +if (domain == NULL) + { + log_write(0, LOG_MAIN, "%s router: reroute string %s is not of the form " + "user@domain", rblock->name, reroute); + addr->message = string_sprintf("%s router: reroute string %s is not of the " + "form user@domain", rblock->name, reroute); + return DEFER; + } + +/* Create a child address with the old one as parent. Put the new address on +the chain of new addressess. */ + +new_addr = deliver_make_addr(reroute, TRUE); +new_addr->parent = addr; + +copyflag(new_addr, addr, af_propagate); +new_addr->p = addr->p; + +addr->child_count++; +new_addr->next = *addr_new; +*addr_new = new_addr; + +/* Set up the errors address, if any, and the additional and removeable headers +for this new address. */ + +rc = rf_get_errors_address(addr, rblock, verify, &(new_addr->p.errors_address)); +if (rc != OK) return rc; + +rc = rf_get_munge_headers(addr, rblock, &(new_addr->p.extra_headers), + &(new_addr->p.remove_headers)); +if (rc != OK) return rc; + +return OK; +} + +/* End of routers/iplookup.c */ diff --git a/src/src/routers/iplookup.h b/src/src/routers/iplookup.h new file mode 100644 index 000000000..d24d38396 --- /dev/null +++ b/src/src/routers/iplookup.h @@ -0,0 +1,43 @@ +/* $Cambridge: exim/src/src/routers/iplookup.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +/* Private structure for the private options. */ + +typedef struct { + int port; + int protocol; + int timeout; + uschar *protocol_name; + uschar *hosts; + uschar *query; + uschar *response_pattern; + uschar *reroute; + const pcre *re_response_pattern; + BOOL optional; +} iplookup_router_options_block; + +/* Data for reading the private options. */ + +extern optionlist iplookup_router_options[]; +extern int iplookup_router_options_count; + +/* Block containing default values. */ + +extern iplookup_router_options_block iplookup_router_option_defaults; + +/* The main and initialization entry points for the router */ + +extern int iplookup_router_entry(router_instance *, address_item *, + struct passwd *, BOOL, address_item **, address_item **, + address_item **, address_item **); + +extern void iplookup_router_init(router_instance *); + +/* End of routers/iplookup.h */ diff --git a/src/src/routers/manualroute.c b/src/src/routers/manualroute.c new file mode 100644 index 000000000..b4e033e02 --- /dev/null +++ b/src/src/routers/manualroute.c @@ -0,0 +1,426 @@ +/* $Cambridge: exim/src/src/routers/manualroute.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" +#include "manualroute.h" + + +/* Options specific to the manualroute router. */ + +optionlist manualroute_router_options[] = { + { "host_find_failed", opt_stringptr, + (void *)(offsetof(manualroute_router_options_block, host_find_failed)) }, + { "hosts_randomize", opt_bool, + (void *)(offsetof(manualroute_router_options_block, hosts_randomize)) }, + { "route_data", opt_stringptr, + (void *)(offsetof(manualroute_router_options_block, route_data)) }, + { "route_list", opt_stringptr, + (void *)(offsetof(manualroute_router_options_block, route_list)) }, + { "same_domain_copy_routing", opt_bool|opt_public, + (void *)(offsetof(router_instance, same_domain_copy_routing)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int manualroute_router_options_count = + sizeof(manualroute_router_options)/sizeof(optionlist); + +/* Default private options block for the manualroute router. */ + +manualroute_router_options_block manualroute_router_option_defaults = { + hff_freeze, /* host_find_failed code */ + FALSE, /* hosts_randomize */ + US"freeze", /* host_find_failed */ + NULL, /* route_data */ + NULL /* route_list */ +}; + + + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to enable +consistency checks to be done, or anything else that needs to be set up. */ + +void +manualroute_router_init(router_instance *rblock) +{ +manualroute_router_options_block *ob = + (manualroute_router_options_block *)(rblock->options_block); + +/* Host_find_failed must be a recognized word */ + +if (Ustrcmp(ob->host_find_failed, "freeze") == 0) + ob->hff_code = hff_freeze; +else if (Ustrcmp(ob->host_find_failed, "decline") == 0) + ob->hff_code = hff_decline; +else if (Ustrcmp(ob->host_find_failed, "defer") == 0) + ob->hff_code = hff_defer; +else if (Ustrcmp(ob->host_find_failed, "pass") == 0) + ob->hff_code = hff_pass; +else if (Ustrcmp(ob->host_find_failed, "fail") == 0) + ob->hff_code = hff_fail; +else + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "unrecognized setting for host_find_failed option", rblock->name); + +/* One of route_list or route_data must be specified */ + +if ((ob->route_list == NULL && ob->route_data == NULL) || + (ob->route_list != NULL && ob->route_data != NULL)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "route_list or route_data (but not both) must be specified", + rblock->name); +} + + + + +/************************************************* +* Parse a route item * +*************************************************/ + +/* The format of a route list item is: + + [ []] + +if obtained from route_list. The domain is absent if the string came from +route_data, in which case domain==NULL. The domain and the host list may be +enclosed in quotes. + +Arguments: + s pointer to route list item + domain if not NULL, where to put the domain pointer + hostlist where to put the host[list] pointer + options where to put the options pointer + +Returns: FALSE if domain expected and string is empty; + TRUE otherwise +*/ + +static BOOL +parse_route_item(uschar *s, uschar **domain, uschar **hostlist, + uschar **options) +{ +while (*s != 0 && isspace(*s)) s++; + +if (domain != NULL) + { + if (*s == 0) return FALSE; /* missing data */ + *domain = string_dequote(&s); + while (*s != 0 && isspace(*s)) s++; + } + +*hostlist = string_dequote(&s); +while (*s != 0 && isspace(*s)) s++; +*options = s; +return TRUE; +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* The manualroute router provides a manual routing facility (surprise, +surprise). The data that defines the routing can either be set in route_data +(which means it can be found by, for example, looking up the domain in a file), +or a list of domain patterns and their corresponding data can be provided in +route_list. */ + +/* See local README for interface details. This router returns: + +DECLINE + . no pattern in route_list matched (route_data not set) + . route_data was an empty string (route_list not set) + . forced expansion failure in route_data (rf_expand_data) + . forced expansion of host list + . host_find_failed = decline + +DEFER + . transport not defined when needed + . lookup defer in route_list when matching domain pattern + . non-forced expansion failure in route_data + . non-forced expansion failure in host list + . unknown routing option + . host list missing for remote transport (not verifying) + . timeout etc on host lookup (pass_on_timeout not set) + . verifying the errors address caused a deferment or a big disaster such + as an expansion failure (rf_get_errors_address) + . expanding a headers_{add,remove} string caused a deferment or another + expansion error (rf_get_munge_headers) + . a problem in rf_get_transport: no transport when one is needed; + failed to expand dynamic transport; failed to find dynamic transport + . failure to expand or find a uid/gid (rf_get_ugid via rf_queue_add) + . host_find_failed = freeze or defer + . self = freeze or defer + +PASS + . timeout etc on host lookup (pass_on_timeout set) + . host_find_failed = pass + . self = pass + +REROUTED + . self = reroute + +FAIL + . host_find_failed = fail + . self = fail + +OK + . added address to addr_local or addr_remote, as appropriate for the + type of transport; this includes the self="send" case. +*/ + +int +manualroute_router_entry( + router_instance *rblock, /* data for this instantiation */ + address_item *addr, /* address we are working on */ + struct passwd *pw, /* passwd entry after check_local_user */ + BOOL verify, /* TRUE when verifying */ + address_item **addr_local, /* add it to this if it's local */ + address_item **addr_remote, /* add it to this if it's remote */ + address_item **addr_new, /* put new addresses on here */ + address_item **addr_succeed) /* put old address here on success */ +{ +int rc, lookup_type; +uschar *route_item = NULL; +uschar *options = NULL; +uschar *hostlist = NULL; +uschar *domain, *newhostlist, *listptr; +manualroute_router_options_block *ob = + (manualroute_router_options_block *)(rblock->options_block); +transport_instance *transport = NULL; +BOOL individual_transport_set = FALSE; +BOOL randomize = ob->hosts_randomize; + +addr_new = addr_new; /* Keep picky compilers happy */ +addr_succeed = addr_succeed; + +DEBUG(D_route) debug_printf("%s router called for %s\n domain = %s\n", + rblock->name, addr->address, addr->domain); + +/* The initialization check ensures that either route_list or route_data is +set. */ + +if (ob->route_list != NULL) + { + int sep = -(';'); /* Default is semicolon */ + listptr = ob->route_list; + + while ((route_item = string_nextinlist(&listptr, &sep, NULL, 0)) != NULL) + { + int rc; + + DEBUG(D_route) debug_printf("route_item = %s\n", route_item); + if (!parse_route_item(route_item, &domain, &hostlist, &options)) + continue; /* Ignore blank items */ + + /* Check the current domain; if it matches, break the loop */ + + if ((rc = match_isinlist(addr->domain, &domain, UCHAR_MAX+1, + &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, &lookup_value)) == OK) + break; + + /* If there was a problem doing the check, defer */ + + if (rc == DEFER) + { + addr->message = US"lookup defer in route_list"; + return DEFER; + } + } + + if (route_item == NULL) return DECLINE; /* No pattern in the list matched */ + } + +/* Handle a single routing item in route_data. If it expands to an empty +string, decline. */ + +else + { + route_item = rf_expand_data(addr, ob->route_data, &rc); + if (route_item == NULL) return rc; + (void) parse_route_item(route_item, NULL, &hostlist, &options); + if (hostlist[0] == 0) return DECLINE; + } + +/* Expand the hostlist item. It may then pointing to an empty string, or to a +single host or a list of hosts; options is pointing to the rest of the +routelist item, which is either empty or contains various option words. */ + +DEBUG(D_route) debug_printf("original list of hosts = \"%s\" options = %s\n", + hostlist, options); + +newhostlist = expand_string_copy(hostlist); +lookup_value = NULL; /* Finished with */ +expand_nmax = -1; + +/* If the expansion was forced to fail, just decline. Otherwise there is a +configuration problem. */ + +if (newhostlist == NULL) + { + if (expand_string_forcedfail) return DECLINE; + addr->message = string_sprintf("%s router: failed to expand \"%s\": %s", + rblock->name, hostlist, expand_string_message); + return DEFER; + } +else hostlist = newhostlist; + +DEBUG(D_route) debug_printf("expanded list of hosts = \"%s\" options = %s\n", + hostlist, options); + +/* Set default lookup type and scan the options */ + +lookup_type = lk_default; + +while (*options != 0) + { + int term; + uschar *s = options; + while (*options != 0 && !isspace(*options)) options++; + term = *options; + *options = 0; + + if (Ustrcmp(s, "randomize") == 0) randomize = TRUE; + else if (Ustrcmp(s, "no_randomize") == 0) randomize = FALSE; + else if (Ustrcmp(s, "byname") == 0) lookup_type = lk_byname; + else if (Ustrcmp(s, "bydns") == 0) lookup_type = lk_bydns; + else + { + transport_instance *t; + for (t = transports; t != NULL; t = t->next) + { + if (Ustrcmp(t->name, s) == 0) + { + transport = t; + individual_transport_set = TRUE; + break; + } + } + if (t == NULL) + { + s = string_sprintf("unknown routing option or transport name \"%s\"", s); + log_write(0, LOG_MAIN, "Error in %s router: %s", rblock->name, s); + addr->message = string_sprintf("error in router: %s", s); + return DEFER; + } + } + + if (term != 0) + { + options++; + while (*options != 0 && isspace(*options)) options++; + } + } + +/* Set up the errors address, if any. */ + +rc = rf_get_errors_address(addr, rblock, verify, &(addr->p.errors_address)); +if (rc != OK) return rc; + +/* Set up the additional and removeable headers for this address. */ + +rc = rf_get_munge_headers(addr, rblock, &(addr->p.extra_headers), + &(addr->p.remove_headers)); +if (rc != OK) return rc; + +/* If an individual transport is not set, get the transport for this router, if +any. It might be expanded, or it might be unset if this router has verify_only +set. */ + +if (!individual_transport_set) + { + if (!rf_get_transport(rblock->transport_name, &(rblock->transport), addr, + rblock->name, NULL)) + return DEFER; + transport = rblock->transport; + } + +/* Deal with the case of a local transport. The host list is passed over as a +single text string that ends up in $host. */ + +if (transport != NULL && transport->info->local) + { + if (hostlist[0] != 0) + { + host_item *h; + addr->host_list = h = store_get(sizeof(host_item)); + h->name = string_copy(hostlist); + h->address = NULL; + h->port = PORT_NONE; + h->mx = MX_NONE; + h->status = hstatus_unknown; + h->why = hwhy_unknown; + h->last_try = 0; + h->next = NULL; + } + + /* There is nothing more to do other than to queue the address for the + local transport, filling in any uid/gid. This can be done by the common + rf_queue_add() function. */ + + addr->transport = transport; + return rf_queue_add(addr, addr_local, addr_remote, rblock, pw)? + OK : DEFER; + } + +/* There is either no transport (verify_only) or a remote transport. A host +list is mandatory in either case, except when verifying, in which case the +address is just accepted. */ + +if (hostlist[0] == 0) + { + if (verify) goto ROUTED; + addr->message = string_sprintf("error in %s router: no host(s) specified " + "for domain %s", rblock->name, domain); + log_write(0, LOG_MAIN, "%s", addr->message); + return DEFER; + } + +/* Otherwise we finish the routing here by building a chain of host items +for the list of configured hosts, and then finding their addresses. */ + +host_build_hostlist(&(addr->host_list), hostlist, randomize); +rc = rf_lookup_hostlist(rblock, addr, rblock->ignore_target_hosts, lookup_type, + ob->hff_code, addr_new); +if (rc != OK) return rc; + +/* Finally, since we have done all the routing here, there must be a transport +defined for these hosts. It will be a remote one, as a local transport is +dealt with above. However, we don't need one if verifying only. */ + +if (transport == NULL && !verify) + { + log_write(0, LOG_MAIN, "Error in %s router: no transport defined", + rblock->name); + addr->message = US"error in router: transport missing"; + return DEFER; + } + +/* Fill in the transport, queue for remote delivery. The yield of +rf_queue_add() is always TRUE for a remote transport. */ + +ROUTED: + +addr->transport = transport; +(void)rf_queue_add(addr, addr_local, addr_remote, rblock, NULL); +return OK; +} + +/* End of routers/manualroute.c */ diff --git a/src/src/routers/manualroute.h b/src/src/routers/manualroute.h new file mode 100644 index 000000000..1b7302140 --- /dev/null +++ b/src/src/routers/manualroute.h @@ -0,0 +1,39 @@ +/* $Cambridge: exim/src/src/routers/manualroute.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the manualroute router */ + +/* Structure for the private options. */ + +typedef struct { + int hff_code; + BOOL hosts_randomize; + uschar *host_find_failed; + uschar *route_data; + uschar *route_list; +} manualroute_router_options_block; + +/* Data for reading the private options. */ + +extern optionlist manualroute_router_options[]; +extern int manualroute_router_options_count; + +/* Block containing default values. */ + +extern manualroute_router_options_block manualroute_router_option_defaults; + +/* The main and initialization entry points for the router */ + +extern int manualroute_router_entry(router_instance *, address_item *, + struct passwd *, BOOL, address_item **, address_item **, + address_item **, address_item **); + +extern void manualroute_router_init(router_instance *); + +/* End of routers/manualroute.h */ diff --git a/src/src/routers/queryprogram.c b/src/src/routers/queryprogram.c new file mode 100644 index 000000000..f97c70755 --- /dev/null +++ b/src/src/routers/queryprogram.c @@ -0,0 +1,510 @@ +/* $Cambridge: exim/src/src/routers/queryprogram.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" +#include "queryprogram.h" + + + +/* Options specific to the queryprogram router. */ + +optionlist queryprogram_router_options[] = { + { "*expand_command_group", opt_bool | opt_hidden, + (void *)(offsetof(queryprogram_router_options_block, expand_cmd_gid)) }, + { "*expand_command_user", opt_bool | opt_hidden, + (void *)(offsetof(queryprogram_router_options_block, expand_cmd_uid)) }, + { "*set_command_group", opt_bool | opt_hidden, + (void *)(offsetof(queryprogram_router_options_block, cmd_gid_set)) }, + { "*set_command_user", opt_bool | opt_hidden, + (void *)(offsetof(queryprogram_router_options_block, cmd_uid_set)) }, + { "command", opt_stringptr, + (void *)(offsetof(queryprogram_router_options_block, command)) }, + { "command_group",opt_expand_gid, + (void *)(offsetof(queryprogram_router_options_block, cmd_gid)) }, + { "command_user", opt_expand_uid, + (void *)(offsetof(queryprogram_router_options_block, cmd_uid)) }, + { "current_directory", opt_stringptr, + (void *)(offsetof(queryprogram_router_options_block, current_directory)) }, + { "timeout", opt_time, + (void *)(offsetof(queryprogram_router_options_block, timeout)) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int queryprogram_router_options_count = + sizeof(queryprogram_router_options)/sizeof(optionlist); + +/* Default private options block for the queryprogram router. */ + +queryprogram_router_options_block queryprogram_router_option_defaults = { + NULL, /* command */ + 60*60, /* timeout */ + (uid_t)(-1), /* cmd_uid */ + (gid_t)(-1), /* cmd_gid */ + FALSE, /* cmd_uid_set */ + FALSE, /* cmd_gid_set */ + US"/", /* current_directory */ + NULL, /* expand_cmd_gid */ + NULL /* expand_cmd_uid */ +}; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to enable +consistency checks to be done, or anything else that needs to be set up. */ + +void +queryprogram_router_init(router_instance *rblock) +{ +queryprogram_router_options_block *ob = + (queryprogram_router_options_block *)(rblock->options_block); + +/* A command must be given */ + +if (ob->command == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "a command specification is required", rblock->name); + +/* A uid/gid must be supplied */ + +if (!ob->cmd_uid_set && ob->expand_cmd_uid == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "command_user must be specified", rblock->name); +} + + + +/************************************************* +* Process a set of generated new addresses * +*************************************************/ + +/* This function sets up a set of newly generated child addresses and puts them +on the new address chain. + +Arguments: + rblock router block + addr_new new address chain + addr original address + generated list of generated addresses + addr_prop the propagated data block, containing errors_to, + header change stuff, and address_data + +Returns: nothing +*/ + +static void +add_generated(router_instance *rblock, address_item **addr_new, + address_item *addr, address_item *generated, + address_item_propagated *addr_prop) +{ +while (generated != NULL) + { + address_item *next = generated; + generated = next->next; + + next->parent = addr; + orflag(next, addr, af_propagate); + next->p = *addr_prop; + next->start_router = rblock->redirect_router; + + next->next = *addr_new; + *addr_new = next; + + addr->child_count++; + + DEBUG(D_route) + debug_printf("%s router generated %s\n", rblock->name, next->address); + } +} + + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. This router returns: + +DECLINE + . DECLINE returned + . self = DECLINE + +PASS + . PASS returned + . timeout of host lookup and pass_on_timeout set + . self = PASS + +DEFER + . verifying the errors address caused a deferment or a big disaster such + as an expansion failure (rf_get_errors_address) + . expanding a headers_{add,remove} string caused a deferment or another + expansion error (rf_get_munge_headers) + . a problem in rf_get_transport: no transport when one is needed; + failed to expand dynamic transport; failed to find dynamic transport + . bad lookup type + . problem looking up host (rf_lookup_hostlist) + . self = DEFER or FREEZE + . failure to set up uid/gid for running the command + . failure of transport_set_up_command: too many arguments, expansion fail + . failure to create child process + . child process crashed or timed out or didn't return data + . :defer: in data + . DEFER or FREEZE returned + . problem in redirection data + . unknown transport name or trouble expanding router transport + +FAIL + . :fail: in data + . FAIL returned + . self = FAIL + +OK + . address added to addr_local or addr_remote for delivery + . new addresses added to addr_new +*/ + +int +queryprogram_router_entry( + router_instance *rblock, /* data for this instantiation */ + address_item *addr, /* address we are working on */ + struct passwd *pw, /* passwd entry after check_local_user */ + BOOL verify, /* TRUE when verifying */ + address_item **addr_local, /* add it to this if it's local */ + address_item **addr_remote, /* add it to this if it's remote */ + address_item **addr_new, /* put new addresses on here */ + address_item **addr_succeed) /* put old address here on success */ +{ +int fd_in, fd_out, len, rc; +pid_t pid; +struct passwd *upw = NULL; +uschar buffer[1024]; +uschar **argvptr; +uschar *rword, *rdata, *s; +address_item_propagated addr_prop; +queryprogram_router_options_block *ob = + (queryprogram_router_options_block *)(rblock->options_block); +uschar *current_directory = ob->current_directory; +ugid_block ugid; +uid_t uid = ob->cmd_uid; +gid_t gid = ob->cmd_gid; + +DEBUG(D_route) debug_printf("%s router called for %s: domain = %s\n", + rblock->name, addr->address, addr->domain); + +ugid.uid_set = ugid.gid_set = FALSE; + +/* Set up the propagated data block with the current address_data and the +errors address and extra header stuff. */ + +addr_prop.address_data = deliver_address_data; + +rc = rf_get_errors_address(addr, rblock, verify, &(addr_prop.errors_address)); +if (rc != OK) return rc; + +rc = rf_get_munge_headers(addr, rblock, &(addr_prop.extra_headers), + &(addr_prop.remove_headers)); +if (rc != OK) return rc; + +/* Get the fixed or expanded uid under which the command is to run +(initialization ensures that one or the other is set). */ + +if (!ob->cmd_uid_set) + { + if (!route_find_expanded_user(ob->expand_cmd_uid, rblock->name, US"router", + &upw, &uid, &(addr->message))) + return DEFER; + } + +/* Get the fixed or expanded gid, or take the gid from the passwd entry. */ + +if (!ob->cmd_gid_set) + { + if (ob->expand_cmd_gid != NULL) + { + if (route_find_expanded_group(ob->expand_cmd_gid, rblock->name, + US"router", &gid, &(addr->message))) + return DEFER; + } + else if (upw != NULL) + { + gid = upw->pw_gid; + } + else + { + addr->message = string_sprintf("command_user set without command_group " + "for %s router", rblock->name); + return DEFER; + } + } + +DEBUG(D_route) debug_printf("uid=%ld gid=%ld current_directory=%s\n", + (long int)uid, (long int)gid, current_directory); + +if (!transport_set_up_command(&argvptr, /* anchor for arg list */ + ob->command, /* raw command */ + TRUE, /* expand the arguments */ + 0, /* not relevant when... */ + NULL, /* no transporting address */ + US"queryprogram router", /* for error messages */ + &(addr->message))) /* where to put error message */ + { + return DEFER; + } + +/* Create the child process, making it a group leader. */ + +pid = child_open_uid(argvptr, NULL, 0077, &uid, &gid, &fd_in, &fd_out, + current_directory, TRUE); + +if (pid < 0) + { + addr->message = string_sprintf("%s router couldn't create child process: %s", + rblock->name, strerror(errno)); + return DEFER; + } + +/* Nothing is written to the standard input. */ + +close(fd_in); + +/* Wait for the process to finish, applying the timeout, and inspect its return +code. */ + +if ((rc = child_close(pid, ob->timeout)) != 0) + { + if (rc > 0) + addr->message = string_sprintf("%s router: command returned non-zero " + "code %d", rblock->name, rc); + + else if (rc == -256) + { + addr->message = string_sprintf("%s router: command timed out", + rblock->name); + killpg(pid, SIGKILL); /* Kill the whole process group */ + } + + else if (rc == -257) + addr->message = string_sprintf("%s router: wait() failed: %s", + rblock->name, strerror(errno)); + + else + addr->message = string_sprintf("%s router: command killed by signal %d", + rblock->name, -rc); + + return DEFER; + } + +/* Read the pipe to get the command's output, and then close it. */ + +len = read(fd_out, buffer, sizeof(buffer) - 1); +close(fd_out); + +/* Failure to return any data is an error. */ + +if (len <= 0) + { + addr->message = string_sprintf("%s router: command failed to return data", + rblock->name); + return DEFER; + } + +/* Get rid of leading and trailing white space, and pick off the first word of +the result. */ + +while (len > 0 && isspace(buffer[len-1])) len--; +buffer[len] = 0; + +DEBUG(D_route) debug_printf("command wrote: %s\n", buffer); + +rword = buffer; +while (isspace(*rword)) rword++; +rdata = rword; +while (*rdata != 0 && !isspace(*rdata)) rdata++; +if (*rdata != 0) *rdata++ = 0; + +/* The word must be a known yield name. If it is "REDIRECT", the rest of the +line is redirection data, as for a .forward file. It may not contain filter +data, and it may not contain anything other than addresses (no files, no pipes, +no specials). */ + +if (strcmpic(rword, US"REDIRECT") == 0) + { + int filtertype; + redirect_block redirect; + address_item *generated = NULL; + + redirect.string = rdata; + redirect.isfile = FALSE; + + rc = rda_interpret(&redirect, /* redirection data */ + RDO_BLACKHOLE | /* forbid :blackhole: */ + RDO_FAIL | /* forbid :fail: */ + RDO_INCLUDE | /* forbid :include: */ + RDO_REWRITE, /* rewrite generated addresses */ + NULL, /* :include: directory not relevant */ + NULL, /* sieve vacation directory not relevant */ + &ugid, /* uid/gid (but not set) */ + &generated, /* where to hang the results */ + &(addr->message), /* where to put messages */ + NULL, /* don't skip syntax errors */ + &filtertype, /* not used; will always be FILTER_FORWARD */ + string_sprintf("%s router", rblock->name)); + + switch (rc) + { + /* FF_DEFER and FF_FAIL can arise only as a result of explicit commands. + If a configured message was supplied, allow it to be included in an SMTP + response after verifying. */ + + case FF_DEFER: + if (addr->message == NULL) addr->message = US"forced defer"; + else addr->user_message = addr->message; + return DEFER; + + case FF_FAIL: + add_generated(rblock, addr_new, addr, generated, &addr_prop); + if (addr->message == NULL) addr->message = US"forced rejection"; + else addr->user_message = addr->message; + return FAIL; + + case FF_DELIVERED: + break; + + case FF_NOTDELIVERED: /* an empty redirection list is bad */ + addr->message = US"no addresses supplied"; + /* Fall through */ + + case FF_ERROR: + default: + addr->basic_errno = ERRNO_BADREDIRECT; + addr->message = string_sprintf("error in redirect data: %s", addr->message); + return DEFER; + } + + /* Handle the generated addresses, if any. */ + + add_generated(rblock, addr_new, addr, generated, &addr_prop); + + /* Put the original address onto the succeed queue so that any retry items + that get attached to it get processed. */ + + addr->next = *addr_succeed; + *addr_succeed = addr; + + return OK; + } + +/* Handle other returns that are not ACCEPT */ + +if (strcmpic(rword, US"accept") != 0) + { + if (strcmpic(rword, US"decline") == 0) return DECLINE; + if (strcmpic(rword, US"pass") == 0) return PASS; + addr->message = string_copy(rdata); /* data is a message */ + if (strcmpic(rword, US"fail") == 0) return FAIL; + if (strcmpic(rword, US"freeze") == 0) addr->special_action = SPECIAL_FREEZE; + else if (strcmpic(rword, US"defer") != 0) + { + addr->message = string_sprintf("bad command yield: %s %s", rword, rdata); + log_write(0, LOG_PANIC, "%s router: %s", rblock->name, addr->message); + } + return DEFER; + } + +/* The command yielded "ACCEPT". The rest of the string is a number of keyed +fields from which we can fish out values using the "extract" expansion +function. To use this feature, we must put the string into the $value variable, +i.e. set lookup_value. */ + +lookup_value = rdata; +s = expand_string(US"${extract{data}{$value}}"); +if (*s != 0) addr_prop.address_data = string_copy(s); + +s = expand_string(US"${extract{transport}{$value}}"); +lookup_value = NULL; + +/* If we found a transport name, find the actual transport */ + +if (*s != 0) + { + transport_instance *transport; + for (transport = transports; transport != NULL; transport = transport->next) + if (Ustrcmp(transport->name, s) == 0) break; + if (transport == NULL) + { + addr->message = string_sprintf("unknown transport name %s yielded by " + "command", s); + log_write(0, LOG_PANIC, "%s router: %s", rblock->name, addr->message); + return DEFER; + } + addr->transport = transport; + } + +/* No transport given; get the transport from the router configuration. It may +be fixed or expanded, but there will be an error if it is unset, requested by +the last argument not being NULL. */ + +else + { + if (!rf_get_transport(rblock->transport_name, &(rblock->transport), addr, + rblock->name, US"transport")) + return DEFER; + addr->transport = rblock->transport; + } + +/* See if a host list is given, and if so, look up the addresses. */ + +lookup_value = rdata; +s = expand_string(US"${extract{hosts}{$value}}"); + +if (*s != 0) + { + int lookup_type = lk_default; + uschar *ss = expand_string(US"${extract{lookup}{$value}}"); + lookup_value = NULL; + + if (*ss != 0) + { + if (Ustrcmp(ss, "byname") == 0) lookup_type = lk_byname; + else if (Ustrcmp(ss, "bydns") == 0) lookup_type = lk_bydns; + else + { + addr->message = string_sprintf("bad lookup type \"%s\" yielded by " + "command", ss); + log_write(0, LOG_PANIC, "%s router: %s", rblock->name, addr->message); + return DEFER; + } + } + + host_build_hostlist(&(addr->host_list), s, FALSE); /* pro tem no randomize */ + + rc = rf_lookup_hostlist(rblock, addr, rblock->ignore_target_hosts, + lookup_type, hff_defer, addr_new); + if (rc != OK) return rc; + } +lookup_value = NULL; + +/* Put the errors address, extra headers, and address_data into this address */ + +addr->p = addr_prop; + +/* Queue the address for local or remote delivery. */ + +return rf_queue_add(addr, addr_local, addr_remote, rblock, pw)? + OK : DEFER; +} + +/* End of routers/queryprogram.c */ diff --git a/src/src/routers/queryprogram.h b/src/src/routers/queryprogram.h new file mode 100644 index 000000000..407afef4a --- /dev/null +++ b/src/src/routers/queryprogram.h @@ -0,0 +1,42 @@ +/* $Cambridge: exim/src/src/routers/queryprogram.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +/* Private structure for the private options. */ + +typedef struct { + uschar *command; + int timeout; + uid_t cmd_uid; + gid_t cmd_gid; + BOOL cmd_uid_set; + BOOL cmd_gid_set; + uschar *current_directory; + uschar *expand_cmd_gid; + uschar *expand_cmd_uid; +} queryprogram_router_options_block; + +/* Data for reading the private options. */ + +extern optionlist queryprogram_router_options[]; +extern int queryprogram_router_options_count; + +/* Block containing default values. */ + +extern queryprogram_router_options_block queryprogram_router_option_defaults; + +/* The main and initialization entry points for the router */ + +extern int queryprogram_router_entry(router_instance *, address_item *, + struct passwd *, BOOL, address_item **, address_item **, + address_item **, address_item **); + +extern void queryprogram_router_init(router_instance *); + +/* End of routers/queryprogram.h */ diff --git a/src/src/routers/redirect.c b/src/src/routers/redirect.c new file mode 100644 index 000000000..e33a5fe49 --- /dev/null +++ b/src/src/routers/redirect.c @@ -0,0 +1,758 @@ +/* $Cambridge: exim/src/src/routers/redirect.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" +#include "redirect.h" + + + +/* Options specific to the redirect router. */ + +optionlist redirect_router_options[] = { + { "allow_defer", opt_bit | (RDON_DEFER << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "allow_fail", opt_bit | (RDON_FAIL << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "allow_filter", opt_bit | (RDON_FILTER << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "allow_freeze", opt_bit | (RDON_FREEZE << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "check_ancestor", opt_bool, + (void *)offsetof(redirect_router_options_block, check_ancestor) }, + { "check_group", opt_bool, + (void *)offsetof(redirect_router_options_block, check_group) }, + { "check_owner", opt_bool, + (void *)offsetof(redirect_router_options_block, check_owner) }, + { "data", opt_stringptr, + (void *)offsetof(redirect_router_options_block, data) }, + { "directory_transport",opt_stringptr, + (void *)offsetof(redirect_router_options_block, directory_transport_name) }, + { "file", opt_stringptr, + (void *)offsetof(redirect_router_options_block, file) }, + { "file_transport", opt_stringptr, + (void *)offsetof(redirect_router_options_block, file_transport_name) }, + { "forbid_blackhole", opt_bit | (RDON_BLACKHOLE << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "forbid_file", opt_bool, + (void *)offsetof(redirect_router_options_block, forbid_file) }, + { "forbid_filter_existstest", opt_bit | (RDON_EXISTS << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "forbid_filter_logwrite",opt_bit | (RDON_LOG << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "forbid_filter_lookup", opt_bit | (RDON_LOOKUP << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + #ifdef EXIM_PERL + { "forbid_filter_perl", opt_bit | (RDON_PERL << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + #endif + { "forbid_filter_readfile", opt_bit | (RDON_READFILE << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "forbid_filter_readsocket", opt_bit | (RDON_READSOCK << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "forbid_filter_reply",opt_bool, + (void *)offsetof(redirect_router_options_block, forbid_filter_reply) }, + { "forbid_filter_run", opt_bit | (RDON_RUN << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "forbid_include", opt_bit | (RDON_INCLUDE << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "forbid_pipe", opt_bool, + (void *)offsetof(redirect_router_options_block, forbid_pipe) }, + { "hide_child_in_errmsg", opt_bool, + (void *)offsetof(redirect_router_options_block, hide_child_in_errmsg) }, + { "ignore_eacces", opt_bit | (RDON_EACCES << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "ignore_enotdir", opt_bit | (RDON_ENOTDIR << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "include_directory", opt_stringptr, + (void *)offsetof(redirect_router_options_block, include_directory) }, + { "modemask", opt_octint, + (void *)offsetof(redirect_router_options_block, modemask) }, + { "one_time", opt_bool, + (void *)offsetof(redirect_router_options_block, one_time) }, + { "owners", opt_uidlist, + (void *)offsetof(redirect_router_options_block, owners) }, + { "owngroups", opt_gidlist, + (void *)offsetof(redirect_router_options_block, owngroups) }, + { "pipe_transport", opt_stringptr, + (void *)offsetof(redirect_router_options_block, pipe_transport_name) }, + { "qualify_domain", opt_stringptr, + (void *)offsetof(redirect_router_options_block, qualify_domain) }, + { "qualify_preserve_domain", opt_bool, + (void *)offsetof(redirect_router_options_block, qualify_preserve_domain) }, + { "repeat_use", opt_bool | opt_public, + (void *)offsetof(router_instance, repeat_use) }, + { "reply_transport", opt_stringptr, + (void *)offsetof(redirect_router_options_block, reply_transport_name) }, + { "rewrite", opt_bit | (RDON_REWRITE << 16), + (void *)offsetof(redirect_router_options_block, bit_options) }, + { "sieve_vacation_directory", opt_stringptr, + (void *)offsetof(redirect_router_options_block, sieve_vacation_directory) }, + { "skip_syntax_errors", opt_bool, + (void *)offsetof(redirect_router_options_block, skip_syntax_errors) }, + { "syntax_errors_text", opt_stringptr, + (void *)offsetof(redirect_router_options_block, syntax_errors_text) }, + { "syntax_errors_to", opt_stringptr, + (void *)offsetof(redirect_router_options_block, syntax_errors_to) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int redirect_router_options_count = + sizeof(redirect_router_options)/sizeof(optionlist); + +/* Default private options block for the redirect router. */ + +redirect_router_options_block redirect_router_option_defaults = { + NULL, /* directory_transport */ + NULL, /* file_transport */ + NULL, /* pipe_transport */ + NULL, /* reply_transport */ + NULL, /* data */ + NULL, /* directory_transport_name */ + NULL, /* file */ + NULL, /* file_dir */ + NULL, /* file_transport_name */ + NULL, /* include_directory */ + NULL, /* pipe_transport_name */ + NULL, /* reply_transport_name */ + NULL, /* sieve_vacation_directory */ + NULL, /* syntax_errors_text */ + NULL, /* syntax_errors_to */ + NULL, /* qualify_domain */ + NULL, /* owners */ + NULL, /* owngroups */ + 022, /* modemask */ + RDO_REWRITE, /* bit_options */ + FALSE, /* check_ancestor */ + TRUE_UNSET, /* check_owner */ + TRUE_UNSET, /* check_group */ + FALSE, /* forbid_file */ + FALSE, /* forbid_filter_reply */ + FALSE, /* forbid_pipe */ + FALSE, /* hide_child_in_errmsg */ + FALSE, /* one_time */ + FALSE, /* qualify_preserve_domain */ + FALSE /* skip_syntax_errors */ +}; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to enable +consistency checks to be done, or anything else that needs to be set up. */ + +void redirect_router_init(router_instance *rblock) +{ +redirect_router_options_block *ob = + (redirect_router_options_block *)(rblock->options_block); + +/* Either file or data must be set, but not both */ + +if ((ob->file == NULL) == (ob->data == NULL)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "%sone of \"file\" or \"data\" must be specified", + rblock->name, (ob->file == NULL)? "" : "only "); + +/* Onetime aliases can only be real addresses. Headers can't be manipulated. */ + +if (ob->one_time) + { + ob->forbid_pipe = ob->forbid_file = ob->forbid_filter_reply = TRUE; + if (rblock->extra_headers != NULL || rblock->remove_headers != NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "\"headers_add\" and \"headers_remove\" are not permitted with " + "\"one_time\"", rblock->name); + } + +/* The defaults for check_owner and check_group depend on other settings. The +defaults are: Check the owner if check_local_user or owners is set; check the +group if check_local_user is set without a restriction on the group write bit, +or if owngroups is set. */ + +if (ob->check_owner == TRUE_UNSET) + ob->check_owner = rblock->check_local_user || + (ob->owners != NULL && ob->owners[0] != 0); + +if (ob->check_group == TRUE_UNSET) + ob->check_group = (rblock->check_local_user && (ob->modemask & 020) == 0) || + (ob->owngroups != NULL && ob->owngroups[0] != 0); + +/* If explicit qualify domain set, the preserve option is locked out */ + +if (ob->qualify_domain != NULL && ob->qualify_preserve_domain) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "only one of \"qualify_domain\" or \"qualify_preserve_domain\" must be set", + rblock->name); + +/* If allow_filter is set, either user or check_local_user must be set. */ + +if (!rblock->check_local_user && + !rblock->uid_set && + rblock->expand_uid == NULL && + (ob->bit_options & RDO_FILTER) != 0) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "\"user\" or \"check_local_user\" must be set with \"allow_filter\"", + rblock->name); +} + + + +/************************************************* +* Get errors address and header mods * +*************************************************/ + +/* This function is called when new addresses are generated, in order to +sort out errors address and header modifications. We put the errors address +into the parent address (even though it is never used from there because that +address is never transported) so that it can be retrieved if any of the +children gets routed by an "unseen" router. The clone of the child that is +passed on must have the original errors_address value. + +Arguments: + rblock the router control block + addr the address being routed + verify true if verifying + addr_prop point to the propagated block, which is where the + new values are to be placed + +Returns: the result of rf_get_errors_address() or rf_get_munge_headers(), + which is either OK or DEFER +*/ + +static int +sort_errors_and_headers(router_instance *rblock, address_item *addr, + BOOL verify, address_item_propagated *addr_prop) +{ +int frc = rf_get_errors_address(addr, rblock, verify, + &(addr_prop->errors_address)); +if (frc != OK) return frc; +addr->p.errors_address = addr_prop->errors_address; +return rf_get_munge_headers(addr, rblock, &(addr_prop->extra_headers), + &(addr_prop->remove_headers)); +} + + + +/************************************************* +* Process a set of generated new addresses * +*************************************************/ + +/* This function sets up a set of newly generated child addresses and puts them +on the new address chain. Copy in the uid, gid and permission flags for use by +pipes and files, set the parent, and "or" its af_ignore_error flag. Also record +the setting for any starting router. + +If the generated address is the same as one of its ancestors, and the +check_ancestor flag is set, do not use this generated address, but replace it +with a copy of the input address. This is to cope with cases where A is aliased +to B and B has a .forward file pointing to A, though it is usually set on the +forwardfile rather than the aliasfile. We can't just pass on the old +address by returning FAIL, because it must act as a general parent for +generated addresses, and only get marked "done" when all its children are +delivered. + +Arguments: + rblock router block + addr_new new address chain + addr original address + generated list of generated addresses + addr_prop the propagated block, containing the errors_address, + header modification stuff, and address_data + ugidptr points to uid/gid data for files, pipes, autoreplies + pw password entry, set if ob->check_local_user is TRUE + +Returns: nothing +*/ + +static void +add_generated(router_instance *rblock, address_item **addr_new, + address_item *addr, address_item *generated, + address_item_propagated *addr_prop, ugid_block *ugidptr, struct passwd *pw) +{ +redirect_router_options_block *ob = + (redirect_router_options_block *)(rblock->options_block); + +while (generated != NULL) + { + address_item *parent; + address_item *next = generated; + uschar *errors_address = next->p.errors_address; + + generated = next->next; + next->parent = addr; + orflag(next, addr, af_ignore_error); + next->start_router = rblock->redirect_router; + addr->child_count++; + + next->next = *addr_new; + *addr_new = next; + + /* Don't do the "one_time" thing for the first pass of a 2-stage queue run. */ + + if (ob->one_time && !queue_2stage) + { + for (parent = addr; parent->parent != NULL; parent = parent->parent); + next->onetime_parent = parent->address; + } + + if (ob->hide_child_in_errmsg) setflag(next, af_hide_child); + + /* If check_ancestor is set, we want to know if any ancestor of this address + is the address we are about to generate. The check must be done caselessly + unless the ancestor was routed by a case-sensitive router. */ + + if (ob->check_ancestor) + { + for (parent = addr; parent != NULL; parent = parent->parent) + { + if (((parent->router != NULL && parent->router->caseful_local_part)? + Ustrcmp(next->address, parent->address) + : + strcmpic(next->address, parent->address) + ) == 0) + { + DEBUG(D_route) debug_printf("generated parent replaced by child\n"); + next->address = string_copy(addr->address); + break; + } + } + } + + /* A user filter may, under some circumstances, set up an errors address. + If so, we must take care to re-instate it when we copy in the propagated + data so that it overrides any errors_to setting on the router. */ + + next->p = *addr_prop; + if (errors_address != NULL) next->p.errors_address = errors_address; + + /* For pipes, files, and autoreplies, record this router as handling them, + because they don't go through the routing process again. Then set up uid, + gid, home and current directories for transporting. */ + + if (testflag(next, af_pfr)) + { + next->router = rblock; + rf_set_ugid(next, ugidptr); /* Will contain pw values if not overridden */ + + /* When getting the home directory out of the password information, wrap it + in \N...\N to avoid expansion later. In Cygwin, home directories can + contain $ characters. */ + + if (rblock->home_directory != NULL) + next->home_dir = rblock->home_directory; + else if (rblock->check_local_user) + next->home_dir = string_sprintf("\\N%s\\N", pw->pw_dir); + else if (rblock->router_home_directory != NULL && + testflag(addr, af_home_expanded)) + { + next->home_dir = deliver_home; + setflag(next, af_home_expanded); + } + + next->current_dir = rblock->current_directory; + + /* Permission options */ + + if (!ob->forbid_pipe) setflag(next, af_allow_pipe); + if (!ob->forbid_file) setflag(next, af_allow_file); + if (!ob->forbid_filter_reply) setflag(next, af_allow_reply); + + /* If the transport setting fails, the error gets picked up at the outer + level from the setting of basic_errno in the address. */ + + if (next->address[0] == '|') + { + address_pipe = next->address; + if (rf_get_transport(ob->pipe_transport_name, &(ob->pipe_transport), + next, rblock->name, US"pipe_transport")) + next->transport = ob->pipe_transport; + address_pipe = NULL; + } + else if (next->address[0] == '>') + { + if (rf_get_transport(ob->reply_transport_name, &(ob->reply_transport), + next, rblock->name, US"reply_transport")) + next->transport = ob->reply_transport; + } + else /* must be file or directory */ + { + int len = Ustrlen(next->address); + address_file = next->address; + if (next->address[len-1] == '/') + { + if (rf_get_transport(ob->directory_transport_name, + &(ob->directory_transport), next, rblock->name, + US"directory_transport")) + next->transport = ob->directory_transport; + } + else + { + if (rf_get_transport(ob->file_transport_name, &(ob->file_transport), + next, rblock->name, US"file_transport")) + next->transport = ob->file_transport; + } + address_file = NULL; + } + } + + DEBUG(D_route) + { + debug_printf("%s router generated %s\n %serrors_to=%s transport=%s\n", + rblock->name, + next->address, + testflag(next, af_pfr)? "pipe, file, or autoreply\n " : "", + next->p.errors_address, + (next->transport == NULL)? US"NULL" : next->transport->name); + + if (testflag(next, af_uid_set)) + debug_printf(" uid=%ld ", (long int)(next->uid)); + else + debug_printf(" uid=unset "); + + if (testflag(next, af_gid_set)) + debug_printf("gid=%ld ", (long int)(next->gid)); + else + debug_printf("gid=unset "); + + debug_printf("home=%s\n", next->home_dir); + } + } +} + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface description. This router returns: + +DECLINE + . empty address list, or filter did nothing significant + +DEFER + . verifying the errors address caused a deferment or a big disaster such + as an expansion failure (rf_get_errors_address) + . expanding a headers_{add,remove} string caused a deferment or another + expansion error (rf_get_munge_headers) + . :defer: or "freeze" in a filter + . error in address list or filter + . skipped syntax errors, but failed to send the message + +DISCARD + . address was :blackhole:d or "seen finish"ed + +FAIL + . :fail: + +OK + . new addresses added to addr_new +*/ + +int redirect_router_entry( + router_instance *rblock, /* data for this instantiation */ + address_item *addr, /* address we are working on */ + struct passwd *pw, /* passwd entry after check_local_user */ + BOOL verify, /* TRUE when verifying */ + address_item **addr_local, /* add it to this if it's local */ + address_item **addr_remote, /* add it to this if it's remote */ + address_item **addr_new, /* put new addresses on here */ + address_item **addr_succeed) /* put old address here on success */ +{ +redirect_router_options_block *ob = + (redirect_router_options_block *)(rblock->options_block); +address_item *generated = NULL; +uschar *save_qualify_domain_recipient = qualify_domain_recipient; +uschar *discarded = US"discarded"; +address_item_propagated addr_prop; +error_block *eblock = NULL; +ugid_block ugid; +redirect_block redirect; +int filtertype = FILTER_UNSET; +int yield = OK; +int options = ob->bit_options; +int frc = 0; +int xrc = 0; + +addr_local = addr_local; /* Keep picky compilers happy */ +addr_remote = addr_remote; + +/* Initialize the data to be propagated to the children */ + +addr_prop.address_data = deliver_address_data; +addr_prop.domain_data = deliver_domain_data; +addr_prop.localpart_data = deliver_localpart_data; +addr_prop.errors_address = NULL; +addr_prop.extra_headers = NULL; +addr_prop.remove_headers = NULL; + +/* When verifying and testing addresses, the "logwrite" command in filters +must be bypassed. */ + +if (!verify && !address_test_mode) options |= RDO_REALLOG; + +/* Sort out the fixed or dynamic uid/gid. This uid is used (a) for reading the +file (and interpreting a filter) and (b) for running the transports for +generated file and pipe addresses. It is not (necessarily) the same as the uids +that may own the file. Exim panics if an expanded string is not a number and +can't be found in the password file. Other errors set the freezing bit. */ + +if (!rf_get_ugid(rblock, addr, &ugid)) return DEFER; + +if (!ugid.uid_set && pw != NULL) + { + ugid.uid = pw->pw_uid; + ugid.uid_set = TRUE; + } + +if (!ugid.gid_set && pw != NULL) + { + ugid.gid = pw->pw_gid; + ugid.gid_set = TRUE; + } + +/* Call the function that interprets redirection data, either inline or from a +file. This is a separate function so that the system filter can use it. It will +run the function in a subprocess if necessary. If qualify_preserve_domain is +set, temporarily reset qualify_domain_recipient to the current domain so that +any unqualified addresses get qualified with the same domain as the incoming +address. Otherwise, if a local qualify_domain is provided, set that up. */ + +if (ob->qualify_preserve_domain) + qualify_domain_recipient = addr->domain; +else if (ob->qualify_domain != NULL) + { + uschar *new_qdr = rf_expand_data(addr, ob->qualify_domain, &xrc); + if (new_qdr == NULL) return xrc; + qualify_domain_recipient = new_qdr; + } + +redirect.owners = ob->owners; +redirect.owngroups = ob->owngroups; +redirect.modemask = ob->modemask; +redirect.check_owner = ob->check_owner; +redirect.check_group = ob->check_group; +redirect.pw = pw; + +if (ob->file != NULL) + { + redirect.string = ob->file; + redirect.isfile = TRUE; + } +else + { + redirect.string = ob->data; + redirect.isfile = FALSE; + } + +frc = rda_interpret(&redirect, options, ob->include_directory, + ob->sieve_vacation_directory, &ugid, &generated, &(addr->message), + ob->skip_syntax_errors? &eblock : NULL, &filtertype, + string_sprintf("%s router (recipient is %s)", rblock->name, addr->address)); + +qualify_domain_recipient = save_qualify_domain_recipient; + +/* Handle exceptional returns from filtering or processing an address list. +For FAIL and FREEZE we honour any previously set up deliveries by a filter. */ + +switch (frc) + { + case FF_NONEXIST: + addr->message = addr->user_message = NULL; + return DECLINE; + + case FF_BLACKHOLE: + DEBUG(D_route) debug_printf("address :blackhole:d\n"); + generated = NULL; + discarded = US":blackhole:"; + frc = FF_DELIVERED; + break; + + /* FF_DEFER and FF_FAIL can arise only as a result of explicit commands + (:fail: in an alias file or "fail" in a filter). If a configured message was + supplied, allow it to be included in an SMTP response after verifying. */ + + case FF_DEFER: + if (addr->message == NULL) addr->message = US"forced defer"; + else addr->user_message = addr->message; + return DEFER; + + case FF_FAIL: + if ((xrc = sort_errors_and_headers(rblock, addr, verify, &addr_prop)) != OK) + return xrc; + add_generated(rblock, addr_new, addr, generated, &addr_prop, &ugid, pw); + if (addr->message == NULL) addr->message = US"forced rejection"; + else addr->user_message = addr->message; + return FAIL; + + /* As in the case of a system filter, a freeze does not happen after a manual + thaw. In case deliveries were set up by the filter, we set the child count + high so that their completion does not mark the original address done. */ + + case FF_FREEZE: + if (!deliver_manual_thaw) + { + if ((xrc = sort_errors_and_headers(rblock, addr, verify, &addr_prop)) + != OK) return xrc; + add_generated(rblock, addr_new, addr, generated, &addr_prop, &ugid, pw); + if (addr->message == NULL) addr->message = US"frozen by filter"; + addr->special_action = SPECIAL_FREEZE; + addr->child_count = 9999; + return DEFER; + } + frc = FF_NOTDELIVERED; + break; + + /* Handle syntax errors and :include: failures and lookup defers */ + + case FF_ERROR: + case FF_INCLUDEFAIL: + + /* If filtertype is still FILTER_UNSET, it means that the redirection data + was never inspected, so the error was an expansion failure or failure to open + the file, or whatever. In these cases, the existing error message is probably + sufficient. */ + + if (filtertype == FILTER_UNSET) return DEFER; + + /* If it was a filter and skip_syntax_errors is set, we want to set up + the error message so that it can be logged and mailed to somebody. */ + + if (filtertype != FILTER_FORWARD && ob->skip_syntax_errors) + { + eblock = store_get(sizeof(error_block)); + eblock->next = NULL; + eblock->text1 = addr->message; + eblock->text2 = NULL; + addr->message = addr->user_message = NULL; + } + + /* Otherwise set up the error for the address and defer. */ + + else + { + addr->basic_errno = ERRNO_BADREDIRECT; + addr->message = string_sprintf("error in %s %s: %s", + (filtertype != FILTER_FORWARD)? "filter" : "redirect", + (ob->data == NULL)? "file" : "data", + addr->message); + return DEFER; + } + } + + +/* Yield is either FF_DELIVERED (significant action) or FF_NOTDELIVERED (no +significant action). Before dealing with these, however, we must handle the +effect of skip_syntax_errors. + +If skip_syntax_errors was set and there were syntax errors in an address list, +error messages will be present in eblock. Log them and send a message if so +configured. We cannot do this earlier, because the error message must not be +sent as the local user. If there were no valid addresses, generated will be +NULL. In this case, the router declines. + +For a filter file, the error message has been fudged into an eblock. After +dealing with it, the router declines. */ + +if (eblock != NULL) + { + if (!moan_skipped_syntax_errors( + rblock->name, /* For message content */ + eblock, /* Ditto */ + (verify || address_test_mode)? + NULL : ob->syntax_errors_to, /* Who to mail */ + generated != NULL, /* True if not all failed */ + ob->syntax_errors_text)) /* Custom message */ + return DEFER; + + if (filtertype != FILTER_FORWARD || generated == NULL) + { + addr->message = US"syntax error in redirection data"; + return DECLINE; + } + } + +/* Sort out the errors address and any header modifications, and handle the +generated addresses, if any. If there are no generated addresses, we must avoid +calling sort_errors_and_headers() in case this router declines - that function +may modify the errors_address field in the current address, and we don't want +to do that for a decline. */ + +if (generated != NULL) + { + if ((xrc = sort_errors_and_headers(rblock, addr, verify, &addr_prop)) != OK) + return xrc; + add_generated(rblock, addr_new, addr, generated, &addr_prop, &ugid, pw); + } + +/* FF_DELIVERED with no generated addresses is what we get when an address list +contains :blackhole: or a filter contains "seen finish" without having +generated anything. Log what happened to this address, and return DISCARD. */ + +if (frc == FF_DELIVERED) + { + if (generated == NULL && !verify && !address_test_mode) + { + log_write(0, LOG_MAIN, "=> %s <%s> R=%s", discarded, addr->address, + rblock->name); + yield = DISCARD; + } + } + +/* For an address list, FF_NOTDELIVERED always means that no addresses were +generated. For a filter, addresses may or may not have been generated. If none +were, it's the same as an empty address list, and the router declines. However, +if addresses were generated, we can't just decline because successful delivery +of the base address gets it marked "done", so deferred generated addresses +never get tried again. We have to generate a new version of the base address, +as if there were a "deliver" command in the filter file, with the original +address as parent. */ + +else + { + address_item *next; + + if (generated == NULL) return DECLINE; + + next = deliver_make_addr(addr->address, FALSE); + next->parent = addr; + addr->child_count++; + next->next = *addr_new; + *addr_new = next; + + /* Copy relevant flags (af_propagate is a name for the set), and set the + data that propagates. */ + + copyflag(next, addr, af_propagate); + next->p = addr_prop; + + DEBUG(D_route) debug_printf("%s router autogenerated %s\n%s%s%s", + rblock->name, + next->address, + (addr_prop.errors_address != NULL)? " errors to " : "", + (addr_prop.errors_address != NULL)? addr_prop.errors_address : US"", + (addr_prop.errors_address != NULL)? "\n" : ""); + } + +/* Control gets here only when the address has been completely handled. Put the +original address onto the succeed queue so that any retry items that get +attached to it get processed. */ + +addr->next = *addr_succeed; +*addr_succeed = addr; + +return yield; +} + +/* End of routers/redirect.c */ diff --git a/src/src/routers/redirect.h b/src/src/routers/redirect.h new file mode 100644 index 000000000..4a6400743 --- /dev/null +++ b/src/src/routers/redirect.h @@ -0,0 +1,67 @@ +/* $Cambridge: exim/src/src/routers/redirect.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the redirect router */ + +/* Private structure for the private options. */ + +typedef struct { + transport_instance *directory_transport; + transport_instance *file_transport; + transport_instance *pipe_transport; + transport_instance *reply_transport; + + uschar *data; + uschar *directory_transport_name; + uschar *file; + uschar *file_dir; + uschar *file_transport_name; + uschar *include_directory; + uschar *pipe_transport_name; + uschar *reply_transport_name; + uschar *sieve_vacation_directory; + uschar *syntax_errors_text; + uschar *syntax_errors_to; + uschar *qualify_domain; + + uid_t *owners; + gid_t *owngroups; + + int modemask; + int bit_options; + BOOL check_ancestor; + BOOL check_group; + BOOL check_owner; + BOOL forbid_file; + BOOL forbid_filter_reply; + BOOL forbid_pipe; + BOOL hide_child_in_errmsg; + BOOL one_time; + BOOL qualify_preserve_domain; + BOOL skip_syntax_errors; +} redirect_router_options_block; + +/* Data for reading the private options. */ + +extern optionlist redirect_router_options[]; +extern int redirect_router_options_count; + +/* Block containing default values. */ + +extern redirect_router_options_block redirect_router_option_defaults; + +/* The main and initialization entry points for the router */ + +extern int redirect_router_entry(router_instance *, address_item *, + struct passwd *, BOOL, address_item **, address_item **, + address_item **, address_item **); + +extern void redirect_router_init(router_instance *); + +/* End of routers/redirect.h */ diff --git a/src/src/routers/rf_change_domain.c b/src/src/routers/rf_change_domain.c new file mode 100644 index 000000000..d6757d206 --- /dev/null +++ b/src/src/routers/rf_change_domain.c @@ -0,0 +1,86 @@ +/* $Cambridge: exim/src/src/routers/rf_change_domain.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" + + + +/************************************************* +* Change domain in an address * +*************************************************/ + +/* When a router wants to change the address that is being routed, it is like a +redirection. We insert a new parent of the current address to hold the original +information, and change the data in the original address, which is now the +child. The child address is put onto the addr_new chain. Pick up the local part +from the "address" field so as to get it in external form - caseful, and with +any quoting retained. + +Arguments: + addr the address block + domain the new domain + rewrite TRUE if headers lines are to be rewritten + addr_new the new address chain + +Returns: nothing +*/ + +void +rf_change_domain(address_item *addr, uschar *domain, BOOL rewrite, + address_item **addr_new) +{ +address_item *parent = store_get(sizeof(address_item)); +uschar *at = Ustrrchr(addr->address, '@'); +uschar *address = string_sprintf("%.*s@%s", at - addr->address, addr->address, + domain); + +DEBUG(D_route) debug_printf("domain changed to %s\n", domain); + +/* The current address item is made into the parent, and a new address is set +up in the old space. */ + +*parent = *addr; + +/* First copy in initializing values, to wipe out stuff such as the named +domain cache. Then copy over the propagating fields from the parent. Then set +up the new fields. */ + +*addr = address_defaults; +addr->p = parent->p; + +addr->address = address; +addr->unique = string_copy(address); +addr->parent = parent; + +addr->next = *addr_new; +*addr_new = addr; + +/* Rewrite header lines if requested */ + +if (rewrite) + { + header_line *h; + DEBUG(D_route|D_rewrite) debug_printf("rewriting header lines\n"); + for (h = header_list; h != NULL; h = h->next) + { + header_line *newh = + rewrite_header(h, parent->domain, domain, + global_rewrite_rules, rewrite_existflags, TRUE); + if (newh != NULL) + { + h = newh; + header_rewritten = TRUE; + } + } + } +} + +/* End of rf_change_domain.c */ diff --git a/src/src/routers/rf_expand_data.c b/src/src/routers/rf_expand_data.c new file mode 100644 index 000000000..3a28e4375 --- /dev/null +++ b/src/src/routers/rf_expand_data.c @@ -0,0 +1,50 @@ +/* $Cambridge: exim/src/src/routers/rf_expand_data.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" + + +/************************************************* +* Expand data string and handle errors * +*************************************************/ + +/* This little function is used by a couple of routers for expanding things. It +just saves repeating this code too many times. It does an expansion, and +chooses a suitable return code on error. + +Arguments: + addr the address that's being routed + s the string to be expanded + prc pointer to where to put the return code on failure + +Returns: the expanded string, or NULL (with prc set) on failure +*/ + +uschar * +rf_expand_data(address_item *addr, uschar *s, int *prc) +{ +uschar *yield = expand_string(s); +if (yield != NULL) return yield; +if (expand_string_forcedfail) + { + DEBUG(D_route) debug_printf("forced failure for expansion of \"%s\"\n", s); + *prc = DECLINE; + } +else + { + addr->message = string_sprintf("failed to expand \"%s\": %s", s, + expand_string_message); + *prc = DEFER; + } +return NULL; +} + +/* End of routers/rf_expand_data.c */ diff --git a/src/src/routers/rf_functions.h b/src/src/routers/rf_functions.h new file mode 100644 index 000000000..0dc3d0fb1 --- /dev/null +++ b/src/src/routers/rf_functions.h @@ -0,0 +1,33 @@ +/* $Cambridge: exim/src/src/routers/rf_functions.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header for the functions that are shared by the routers */ + + +extern void rf_add_generated(router_instance *, address_item **, + address_item *, address_item *, uschar *, header_line *, + uschar *, ugid_block *, struct passwd *); +extern void rf_change_domain(address_item *, uschar *, BOOL, address_item **); +extern uschar *rf_expand_data(address_item *, uschar *, int *); +extern int rf_get_errors_address(address_item *, router_instance *, + BOOL, uschar **); +extern int rf_get_munge_headers(address_item *, router_instance *, + header_line **, uschar **); +extern BOOL rf_get_transport(uschar *, transport_instance **, address_item *, + uschar *, uschar *); +extern BOOL rf_get_ugid(router_instance *, address_item *, ugid_block *); +extern int rf_lookup_hostlist(router_instance *, address_item *, uschar *, + int, int, address_item **); +extern BOOL rf_queue_add(address_item *, address_item **, address_item **, + router_instance *, struct passwd *); +extern int rf_self_action(address_item *, host_item *, int, BOOL, uschar *, + address_item **); +extern void rf_set_ugid(address_item *, ugid_block *); + +/* End of rf_functions.h */ diff --git a/src/src/routers/rf_get_errors_address.c b/src/src/routers/rf_get_errors_address.c new file mode 100644 index 000000000..e2e75424c --- /dev/null +++ b/src/src/routers/rf_get_errors_address.c @@ -0,0 +1,120 @@ +/* $Cambridge: exim/src/src/routers/rf_get_errors_address.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" + + +/************************************************* +* Get errors address for a router * +*************************************************/ + +/* This function is called by routers to sort out the errors address for a +particular address. If there is a setting in the router block, then expand and +verify it, and if it works, use it. Otherwise use any setting that is in the +address itself. This might be NULL, meaning unset (the message's sender is then +used). Verification isn't done when the original address is just being +verified, as otherwise there might be routing loops if someone sets up a silly +configuration. + +Arguments: + addr the input address + rblock the router instance + verify TRUE when verifying + errors_to point the errors address here + +Returns: OK if no problem + DEFER if verifying the address caused a deferment + or a big disaster (e.g. expansion failure) +*/ + +int +rf_get_errors_address(address_item *addr, router_instance *rblock, + BOOL verify, uschar **errors_to) +{ +uschar *s; + +*errors_to = addr->p.errors_address; +if (rblock->errors_to == NULL) return OK; + +s = expand_string(rblock->errors_to); + +if (s == NULL) + { + if (expand_string_forcedfail) + { + DEBUG(D_route) + debug_printf("forced expansion failure - ignoring errors_to\n"); + return OK; + } + addr->message = string_sprintf("%s router failed to expand \"%s\": %s", + rblock->name, rblock->errors_to, expand_string_message); + return DEFER; + } + +/* If the errors_to address is empty, it means "ignore errors" */ + +if (*s == 0) + { + setflag(addr, af_ignore_error); /* For locally detected errors */ + *errors_to = US""; /* Return path for SMTP */ + return OK; + } + +/* If we are already verifying, do not check the errors address, in order to +save effort (but we do verify when testing an address). When we do verify, set +the sender address to null, because that's what it will be when sending an +error message, and there are now configuration options that control the running +of routers by checking the sender address. When testing an address, there may +not be a sender address. We also need to save and restore the expansion values +associated with an address. */ + +if (verify) + { + *errors_to = s; + DEBUG(D_route) + debug_printf("skipped verify errors_to address: already verifying\n"); + } +else + { + BOOL save_address_test_mode = address_test_mode; + int save1 = 0; + int i; + uschar ***p; + uschar *address_expansions_save[ADDRESS_EXPANSIONS_COUNT]; + address_item *snew = deliver_make_addr(s, FALSE); + + if (sender_address != NULL) + { + save1 = sender_address[0]; + sender_address[0] = 0; + } + + for (i = 0, p = address_expansions; *p != NULL;) + address_expansions_save[i++] = **p++; + address_test_mode = FALSE; + + DEBUG(D_route|D_verify) + debug_printf("------ Verifying errors address %s ------\n", s); + if (verify_address(snew, NULL, vopt_is_recipient | vopt_qualify, -1, -1, + NULL, NULL, NULL) == OK) *errors_to = snew->address; + DEBUG(D_route|D_verify) + debug_printf("------ End verifying errors address %s ------\n", s); + + address_test_mode = save_address_test_mode; + for (i = 0, p = address_expansions; *p != NULL;) + **p++ = address_expansions_save[i++]; + + if (sender_address != NULL) sender_address[0] = save1; + } + +return OK; +} + +/* End of rf_get_errors_address.c */ diff --git a/src/src/routers/rf_get_munge_headers.c b/src/src/routers/rf_get_munge_headers.c new file mode 100644 index 000000000..f728d8bde --- /dev/null +++ b/src/src/routers/rf_get_munge_headers.c @@ -0,0 +1,119 @@ +/* $Cambridge: exim/src/src/routers/rf_get_munge_headers.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" + + +/************************************************* +* Get additional headers for a router * +*************************************************/ + +/* This function is called by both routers to sort out the additional headers +and header remove list for a particular address. + +Arguments: + addr the input address + rblock the router instance + extra_headers points to where to hang the header chain + remove_headers points to where to hang the remove list + +Returns: OK if no problem + DEFER if expanding a string caused a deferment + or a big disaster (e.g. expansion failure) +*/ + +int +rf_get_munge_headers(address_item *addr, router_instance *rblock, + header_line **extra_headers, uschar **remove_headers) +{ +/* Default is to retain existing headers */ + +*extra_headers = addr->p.extra_headers; + +if (rblock->extra_headers != NULL) + { + header_line *h; + uschar *s = expand_string(rblock->extra_headers); + + if (s == NULL) + { + if (!expand_string_forcedfail) + { + addr->message = string_sprintf("%s router failed to expand \"%s\": %s", + rblock->name, rblock->extra_headers, expand_string_message); + return DEFER; + } + } + + /* Expand succeeded. Put extra header at the start of the chain because + further down it may point to headers from other routers, which may be + shared with other addresses. The output function outputs them in reverse + order. */ + + else + { + int slen = Ustrlen(s); + if (slen > 0) + { + h = store_get(sizeof(header_line)); + + /* We used to use string_sprintf() to add the newline if needed, but that + causes problems if the header line is exceedingly long (e.g. adding + something to a pathologically long line). So avoid it. */ + + if (s[slen-1] == '\n') + { + h->text = s; + } + else + { + h->text = store_get(slen+2); + memcpy(h->text, s, slen); + h->text[slen++] = '\n'; + h->text[slen] = 0; + } + + h->next = addr->p.extra_headers; + h->type = htype_other; + h->slen = slen; + *extra_headers = h; + } + } + } + +/* Default is to retain existing removes */ + +*remove_headers = addr->p.remove_headers; + +if (rblock->remove_headers != NULL) + { + uschar *s = expand_string(rblock->remove_headers); + if (s == NULL) + { + if (!expand_string_forcedfail) + { + addr->message = string_sprintf("%s router failed to expand \"%s\": %s", + rblock->name, rblock->remove_headers, expand_string_message); + return DEFER; + } + } + else if (*s != 0) + { + if (addr->p.remove_headers == NULL) + *remove_headers = s; + else + *remove_headers = string_sprintf("%s : %s", addr->p.remove_headers, s); + } + } + +return OK; +} + +/* End of rf_get_munge_headers.c */ diff --git a/src/src/routers/rf_get_transport.c b/src/src/routers/rf_get_transport.c new file mode 100644 index 000000000..a815fa482 --- /dev/null +++ b/src/src/routers/rf_get_transport.c @@ -0,0 +1,92 @@ +/* $Cambridge: exim/src/src/routers/rf_get_transport.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" + + +/************************************************* +* Get transport for a router * +*************************************************/ + +/* If transport_name contains $, it must be expanded each time and used as a +transport name. Otherwise, look up the transport only if the destination is not +already set. + +Some routers (e.g. accept) insist that their transport option is set at +initialization time. However, for some (e.g. file_transport in redirect), there +is no such check, because the transport may not be required. Calls to this +function from the former type of router have require_name = NULL, because it +will never be used. NULL is also used in verify_only cases, where a transport +is not required. + +Arguments: + tpname the text of the transport name + tpptr where to put the transport + addr the address being processed + router_name for error messages + require_name used in the error message if transport is unset + +Returns: TRUE if *tpptr is already set and tpname has no '$' in it; + TRUE if a transport has been placed in tpptr; + FALSE if there's a problem, in which case + addr->message contains a message, and addr->basic_errno has + ERRNO_BADTRANSPORT set in it. +*/ + +BOOL +rf_get_transport(uschar *tpname, transport_instance **tpptr, address_item *addr, + uschar *router_name, uschar *require_name) +{ +uschar *ss; +BOOL expandable; +transport_instance *tp; + +if (tpname == NULL) + { + if (require_name == NULL) return TRUE; + addr->basic_errno = ERRNO_BADTRANSPORT; + addr->message = string_sprintf("%s unset in %s router", require_name, + router_name); + return FALSE; + } + +expandable = Ustrchr(tpname, '$') != NULL; +if (*tpptr != NULL && !expandable) return TRUE; + +if (expandable) + { + ss = expand_string(tpname); + if (ss == NULL) + { + addr->basic_errno = ERRNO_BADTRANSPORT; + addr->message = string_sprintf("failed to expand transport " + "\"%s\" in %s router: %s", tpname, router_name, expand_string_message); + return FALSE; + } + } +else ss = tpname; + +for (tp = transports; tp != NULL; tp = tp->next) + { + if (Ustrcmp(tp->name, ss) == 0) + { + DEBUG(D_route) debug_printf("set transport %s\n", ss); + *tpptr = tp; + return TRUE; + } + } + +addr->basic_errno = ERRNO_BADTRANSPORT; +addr->message = string_sprintf("transport \"%s\" not found in %s router", ss, + router_name); +return FALSE; +} + +/* End of rf_get_transport.c */ diff --git a/src/src/routers/rf_get_ugid.c b/src/src/routers/rf_get_ugid.c new file mode 100644 index 000000000..2be151e2a --- /dev/null +++ b/src/src/routers/rf_get_ugid.c @@ -0,0 +1,82 @@ +/* $Cambridge: exim/src/src/routers/rf_get_ugid.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" + + +/************************************************* +* Get uid/gid for a router * +*************************************************/ + +/* This function is called by routers to sort out the uid/gid values which are +passed with an address for use by local transports. + +Arguments: + rblock the router block + addr the address being worked on + ugid pointer to a ugid block to fill in + +Returns: TRUE if all goes well, else FALSE +*/ + +BOOL +rf_get_ugid(router_instance *rblock, address_item *addr, ugid_block *ugid) +{ +struct passwd *upw = NULL; + +/* Initialize from fixed values */ + +ugid->uid = rblock->uid; +ugid->gid = rblock->gid; +ugid->uid_set = rblock->uid_set; +ugid->gid_set = rblock->gid_set; +ugid->initgroups = rblock->initgroups; + +/* If there is no fixed uid set, see if there's a dynamic one that can +be expanded and possibly looked up. */ + +if (!ugid->uid_set && rblock->expand_uid != NULL) + { + if (route_find_expanded_user(rblock->expand_uid, rblock->name, US"router", + &upw, &(ugid->uid), &(addr->message))) ugid->uid_set = TRUE; + else return FALSE; + } + +/* Likewise for the gid */ + +if (!ugid->gid_set && rblock->expand_gid != NULL) + { + if (route_find_expanded_group(rblock->expand_gid, rblock->name, US"router", + &(ugid->gid), &(addr->message))) ugid->gid_set = TRUE; + else return FALSE; + } + +/* If a uid is set, then a gid must also be available; use one from the passwd +lookup if it happened. */ + +if (ugid->uid_set && !ugid->gid_set) + { + if (upw != NULL) + { + ugid->gid = upw->pw_gid; + ugid->gid_set = TRUE; + } + else + { + addr->message = string_sprintf("user set without group for %s router", + rblock->name); + return FALSE; + } + } + +return TRUE; +} + +/* End of rf_get_ugid.c */ diff --git a/src/src/routers/rf_lookup_hostlist.c b/src/src/routers/rf_lookup_hostlist.c new file mode 100644 index 000000000..48c279e80 --- /dev/null +++ b/src/src/routers/rf_lookup_hostlist.c @@ -0,0 +1,193 @@ +/* $Cambridge: exim/src/src/routers/rf_lookup_hostlist.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" + + + +/************************************************* +* Look up IP addresses for a set of hosts * +*************************************************/ + +/* This function is called by a router to fill in the IP addresses for a set of +hosts that are attached to an address. Each host has its name and MX value set; +and those that need processing have their address fields set NULL. Multihomed +hosts cause additional blocks to be inserted into the chain. + +This function also supports pseudo-hosts whose names end with "/MX". In this +case, MX records are looked up for the name, and the list of hosts obtained +replaces the incoming "host". In other words, "x/MX" is shorthand for "those +hosts pointed to by x's MX records". + +Arguments: + rblock the router block + addr the address being routed + ignore_target_hosts list of hosts to ignore + lookup_type lk_default or lk_byname or lk_bydns + hff_code what to do for host find failed + addr_new passed to rf_self_action for self=reroute + +Returns: OK + DEFER host lookup defer + PASS timeout etc and pass_on_timeout set + self_action: PASS, DECLINE, DEFER, FAIL, FREEZE + hff_code after host find failed +*/ + +int +rf_lookup_hostlist(router_instance *rblock, address_item *addr, + uschar *ignore_target_hosts, int lookup_type, int hff_code, + address_item **addr_new) +{ +BOOL self_send = FALSE; +host_item *h, *next_h, *prev; + +/* Look up each host address. A lookup may add additional items into the chain +if there are multiple addresses. Hence the use of next_h to start each cycle of +the loop at the next original host. If any host is identified as being the local +host, omit it and any subsequent hosts - i.e. treat the list like an ordered +list of MX hosts. If the first host is the local host, act according to the +"self" option in the configuration. */ + +prev = NULL; +for (h = addr->host_list; h != NULL; prev = h, h = next_h) + { + uschar *canonical_name; + int rc, len; + + next_h = h->next; + if (h->address != NULL) continue; + + DEBUG(D_route|D_host_lookup) + debug_printf("finding IP address for %s\n", h->name); + + /* If the name ends with "/MX", we interpret it to mean "the list of hosts + pointed to by MX records with this name". */ + + len = Ustrlen(h->name); + if (len > 3 && strcmpic(h->name + len - 3, US"/mx") == 0) + { + DEBUG(D_route|D_host_lookup) + debug_printf("doing DNS MX lookup for %s\n", h->name); + + h->name[len-3] = 0; + rc = host_find_bydns(h, + ignore_target_hosts, + HOST_FIND_BY_MX, /* look only for MX records */ + NULL, /* SRV service not relevant */ + NULL, /* failing srv domains not relevant */ + NULL, /* no special mx failing domains */ + NULL, /* fully_qualified_name */ + NULL); /* indicate local host removed */ + } + + /* If explicitly configured to look up by name, or if the "host name" is + actually an IP address, do a byname lookup. */ + + else if (lookup_type == lk_byname || string_is_ip_address(h->name, NULL)) + { + DEBUG(D_route|D_host_lookup) debug_printf("calling host_find_byname\n"); + rc = host_find_byname(h, ignore_target_hosts, &canonical_name, TRUE); + } + + /* Otherwise, do a DNS lookup. If that yields "host not found", and the + lookup type is the default (i.e. "bydns" is not explicitly configured), + follow up with a byname lookup, just in case. */ + + else + { + BOOL removed; + DEBUG(D_route|D_host_lookup) debug_printf("doing DNS lookup\n"); + rc = host_find_bydns(h, ignore_target_hosts, HOST_FIND_BY_A, NULL, NULL, + NULL, &canonical_name, &removed); + if (rc == HOST_FOUND) + { + if (removed) setflag(addr, af_local_host_removed); + } + else if (rc == HOST_FIND_FAILED) + { + if (lookup_type == lk_default) + { + DEBUG(D_route|D_host_lookup) + debug_printf("DNS lookup failed: trying getipnodebyname\n"); + rc = host_find_byname(h, ignore_target_hosts, &canonical_name, TRUE); + } + } + } + + /* Temporary failure defers, unless pass_on_timeout is set */ + + if (rc == HOST_FIND_AGAIN) + { + if (rblock->pass_on_timeout) + { + DEBUG(D_route) + debug_printf("%s router timed out and pass_on_timeout set\n", + rblock->name); + return PASS; + } + addr->message = string_sprintf("host lookup for %s did not complete " + "(DNS timeout?)", h->name); + addr->basic_errno = ERRNO_DNSDEFER; + return DEFER; + } + + /* Permanent failure is controlled by host_find_failed */ + + if (rc == HOST_FIND_FAILED) + { + if (hff_code == hff_pass) return PASS; + if (hff_code == hff_decline) return DECLINE; + + addr->message = + string_sprintf("lookup of host \"%s\" failed in %s router%s", + h->name, rblock->name, + host_find_failed_syntax? ": syntax error in name" : ""); + + if (hff_code == hff_defer) return DEFER; + if (hff_code == hff_fail) return FAIL; + + addr->special_action = SPECIAL_FREEZE; + return DEFER; + } + + /* A local host gets chopped, with its successors, if there are previous + hosts. Otherwise the self option is used. If it is set to "send", any + subsequent hosts that are also the local host do NOT get chopped. */ + + if (rc == HOST_FOUND_LOCAL && !self_send) + { + if (prev != NULL) + { + DEBUG(D_route) + { + debug_printf("Removed from host list:\n"); + for (; h != NULL; h = h->next) debug_printf(" %s\n", h->name); + } + prev->next = NULL; + setflag(addr, af_local_host_removed); + break; + } + rc = rf_self_action(addr, h, rblock->self_code, rblock->self_rewrite, + rblock->self, addr_new); + if (rc != OK) + { + addr->host_list = NULL; /* Kill the host list for */ + return rc; /* anything other than "send" */ + } + self_send = TRUE; + } + } + +return OK; +} + +/* End of rf_lookup_hostlist.c */ diff --git a/src/src/routers/rf_queue_add.c b/src/src/routers/rf_queue_add.c new file mode 100644 index 000000000..23a05b1cd --- /dev/null +++ b/src/src/routers/rf_queue_add.c @@ -0,0 +1,108 @@ +/* $Cambridge: exim/src/src/routers/rf_queue_add.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" + + +/************************************************* +* Queue address for transport * +*************************************************/ + +/* This function is called to put an address onto the local or remote transport +queue, as appropriate. When the driver is for verifying only, a transport need +not be set, in which case it doesn't actually matter which queue the address +gets put on. + +The generic uid/gid options are inspected and put into the address if they are +set. For a remote transport, if there are fallback hosts, they are added to the +address. + +Arguments: + addr the address, with the transport field set (if not verify only) + paddr_local pointer to the anchor of the local transport chain + paddr_remote pointer to the anchor of the remote transport chain + rblock the router block + pw password entry if check_local_user was set, or NULL + +Returns: FALSE on error; the only case is failing to get a uid/gid +*/ + +BOOL +rf_queue_add(address_item *addr, address_item **paddr_local, + address_item **paddr_remote, router_instance *rblock, struct passwd *pw) +{ +addr->p.domain_data = deliver_domain_data; /* Save these values for */ +addr->p.localpart_data = deliver_localpart_data; /* use in the transport */ + +/* Handle a local transport */ + +if (addr->transport != NULL && addr->transport->info->local) + { + ugid_block ugid; + + /* Default uid/gid and transport-time home directory are from the passwd file + when check_local_user is set, but can be overridden by explicit settings. + When getting the home directory out of the password information, set the + flag that prevents expansion later. */ + + if (pw != NULL) + { + addr->uid = pw->pw_uid; + addr->gid = pw->pw_gid; + setflag(addr, af_uid_set|af_gid_set|af_home_expanded); + addr->home_dir = string_copy(US pw->pw_dir); + } + + if (!rf_get_ugid(rblock, addr, &ugid)) return FALSE; + rf_set_ugid(addr, &ugid); + + /* transport_home_directory (in rblock->home_directory) takes priority; + otherwise use the expanded value of router_home_directory. The flag also + tells the transport not to re-expand it. */ + + if (rblock->home_directory != NULL) + { + addr->home_dir = rblock->home_directory; + clearflag(addr, af_home_expanded); + } + else if (addr->home_dir == NULL && testflag(addr, af_home_expanded)) + addr->home_dir = deliver_home; + + addr->current_dir = rblock->current_directory; + + addr->next = *paddr_local; + *paddr_local = addr; + } + +/* For a remote transport, set up the fallback host list, and keep a count of +the total number of addresses routed to remote transports. */ + +else + { + addr->fallback_hosts = rblock->fallback_hostlist; + addr->next = *paddr_remote; + *paddr_remote = addr; + remote_delivery_count++; + } + +DEBUG(D_route) + { + debug_printf("queued for %s transport: local_part = %s\ndomain = %s\n" + " errors_to=%s\n", + (addr->transport == NULL)? US"" : addr->transport->name, + addr->local_part, addr->domain, addr->p.errors_address); + debug_printf(" domain_data=%s localpart_data=%s\n", addr->p.domain_data, + addr->p.localpart_data); + } + +return TRUE; +} + +/* End of rf_queue_add.c */ diff --git a/src/src/routers/rf_self_action.c b/src/src/routers/rf_self_action.c new file mode 100644 index 000000000..0808054ab --- /dev/null +++ b/src/src/routers/rf_self_action.c @@ -0,0 +1,142 @@ +/* $Cambridge: exim/src/src/routers/rf_self_action.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "rf_functions.h" + + + +/************************************************* +* Decode actions for self reference * +*************************************************/ + +/* This function is called from a number of routers on receiving +HOST_FOUND_LOCAL when looking up a supposedly remote host. The action is +controlled by a generic configuration option called "self" on each router, +which can be one of: + + . freeze: Log the incident, freeze, and return DEFER + + . defer: Log the incident and return DEFER + + . fail: Fail the address + + . send: Carry on with the delivery regardless - + this makes sense only if the SMTP + listener on this machine is a differently + configured MTA + + . pass: The router passes; the address + gets passed to the next router, overriding + the setting of no_more + + . reroute: Change the domain to the given domain + and return REROUTE so it gets passed back + to the routers. + + . reroute:rewrite: The same, but headers containing the + old domain get rewritten. + +These string values are interpreted earlier on, and passed into this function +as the values of "code" and "rewrite". + +Arguments: + addr the address being routed + host the host that is local, with MX set (or -1 if MX not used) + code the action to be taken (one of the self_xxx enums) + rewrite TRUE if rewriting headers required for REROUTED + new new domain to be used for REROUTED + addr_new child chain for REROUTEED + +Returns: DEFER, REROUTED, PASS, FAIL, or OK, according to the value of code. +*/ + +int +rf_self_action(address_item *addr, host_item *host, int code, BOOL rewrite, + uschar *new, address_item **addr_new) +{ +uschar *msg = (host->mx >= 0)? + US"lowest numbered MX record points to local host" : + US"remote host address is the local host"; + +switch (code) + { + case self_freeze: + + /* If there is no message id, this is happening during an address + verification, so give information about the address that is being verified, + and where it has come from. Otherwise, during message delivery, the normal + logging for the address will be sufficient. */ + + if (message_id[0] == 0) + { + if (sender_fullhost == NULL) + { + log_write(0, LOG_MAIN, "%s: %s (while routing <%s>)", msg, + addr->domain, addr->address); + } + else + { + log_write(0, LOG_MAIN, "%s: %s (while verifying <%s> from host %s)", + msg, addr->domain, addr->address, sender_fullhost); + } + } + else + log_write(0, LOG_MAIN, "%s: %s", msg, addr->domain); + + addr->message = msg; + addr->special_action = SPECIAL_FREEZE; + return DEFER; + + case self_defer: + addr->message = msg; + return DEFER; + + case self_reroute: + DEBUG(D_route) + { + debug_printf("%s: %s", msg, addr->domain); + debug_printf(": domain changed to %s\n", new); + } + rf_change_domain(addr, new, rewrite, addr_new); + return REROUTED; + + case self_send: + DEBUG(D_route) + { + debug_printf("%s: %s", msg, addr->domain); + debug_printf(": configured to try delivery anyway\n"); + } + return OK; + + case self_pass: /* This is soft failure; pass to next router */ + DEBUG(D_route) + { + debug_printf("%s: %s", msg, addr->domain); + debug_printf(": passed to next router (self = pass)\n"); + } + addr->message = msg; + addr->self_hostname = string_copy(host->name); + return PASS; + + case self_fail: + DEBUG(D_route) + { + debug_printf("%s: %s", msg, addr->domain); + debug_printf(": address failed (self = fail)\n"); + } + addr->message = msg; + return FAIL; + } + +return DEFER; /* paranoia */ +} + +/* End of rf_self_action.c */ diff --git a/src/src/routers/rf_set_ugid.c b/src/src/routers/rf_set_ugid.c new file mode 100644 index 000000000..d5ab1fa43 --- /dev/null +++ b/src/src/routers/rf_set_ugid.c @@ -0,0 +1,46 @@ +/* $Cambridge: exim/src/src/routers/rf_set_ugid.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "rf_functions.h" + + +/************************************************* +* Set uid/gid from block into address * +*************************************************/ + +/* This function copies any set uid or gid from a ugid block into an +address. + +Arguments: + addr the address + ugid the ugid block + +Returns: nothing +*/ + +void +rf_set_ugid(address_item *addr, ugid_block *ugid) +{ +if (ugid->uid_set) + { + addr->uid = ugid->uid; + setflag(addr, af_uid_set); + } + +if (ugid->gid_set) + { + addr->gid = ugid->gid; + setflag(addr, af_gid_set); + } + +if (ugid->initgroups) setflag(addr, af_initgroups); +} + +/* End of rf_set_ugid.c */ diff --git a/src/src/transports/Makefile b/src/src/transports/Makefile new file mode 100644 index 000000000..2047b1435 --- /dev/null +++ b/src/src/transports/Makefile @@ -0,0 +1,26 @@ +# $Cambridge: exim/src/src/transports/Makefile,v 1.1 2004/10/07 13:10:02 ph10 Exp $ + +# Make file for building a library containing all the available transports and +# calling it transports.a. This is called from the main make file, after cd'ing +# to the transports subdirectory. + +OBJ = appendfile.o autoreply.o lmtp.o pipe.o smtp.o tf_maildir.o + +transports.a: $(OBJ) + /bin/rm -f transports.a + $(AR) transports.a $(OBJ) + $(RANLIB) $@ + /bin/rm -rf ../drtables.o + +.SUFFIXES: .o .c +.c.o:; $(CC) -c $(CFLAGS) $(INCLUDE) $*.c + +appendfile.o: $(HDRS) appendfile.c appendfile.h tf_maildir.h +autoreply.o: $(HDRS) autoreply.c autoreply.h +lmtp.o: $(HDRS) lmtp.c lmtp.h +pipe.o: $(HDRS) pipe.c pipe.h +smtp.o: $(HDRS) smtp.c smtp.h + +tf_maildir.o: $(HDRS) tf_maildir.c tf_maildir.h appendfile.h + +# End diff --git a/src/src/transports/README b/src/src/transports/README new file mode 100644 index 000000000..4b5d270a7 --- /dev/null +++ b/src/src/transports/README @@ -0,0 +1,43 @@ +$Cambridge: exim/src/src/transports/README,v 1.1 2004/10/07 13:10:02 ph10 Exp $ + +TRANSPORTS: + +A delivery attempt results in one of the following values being placed in +addr->transport_return: + + OK success + DEFER temporary failure + FAIL permanent failure + PANIC disaster - causes exim to bomb + +The field is initialized to DEFER when the address is created, in order that +unexpected process crashes or other problems don't cause the message to be +deleted. + +For non-OK values, additional information is placed in addr->errno, +addr->more_errno, and optionally in addr->message. These are inspected only if +the status is not OK, with one exception (see below). + +In addition, the addr->special_action field can be set to request a non-default +action. The default action after FAIL is to return to sender; the default +action after DEFER is nothing. The alternatives are: + + SPECIAL_NONE (default) no special action + SPECIAL_FREEZE freeze the message + SPECIAL_WARN send warning message + +The SPECIAL_WARN action is the exception referred to above. It is picked up +only after a *successful* delivery; it causes a warning message to be sent +containing the text of warn_message to warn_to. It can be used in appendfile, +for example, to send a warning message when the mailbox size crosses a given +threshold. + +If the transport is handling a batch of several addresses, it may either put an +individual value in each address structure, and return TRUE, or it may put a +common value in the first address, and return FALSE. + +Remote transports usually return TRUE and local transports usually return +FALSE; however, the lmtp transport may return either value, depending on what +happens inside it. + +**** diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c new file mode 100644 index 000000000..f7cb912ae --- /dev/null +++ b/src/src/transports/appendfile.c @@ -0,0 +1,3147 @@ +/* $Cambridge: exim/src/src/transports/appendfile.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "appendfile.h" + +#ifdef SUPPORT_MAILDIR +#include "tf_maildir.h" +#endif + + +/* Encodings for mailbox formats, and their names. MBX format is actually +supported only if SUPPORT_MBX is set. */ + +enum { mbf_unix, mbf_mbx, mbf_smail, mbf_maildir, mbf_mailstore }; + +static char *mailbox_formats[] = { + "unix", "mbx", "smail", "maildir", "mailstore" }; + + +/* Check warn threshold only if quota size set or not a percentage threshold + percentage check should only be done if quota > 0 */ + +#define THRESHOLD_CHECK (ob->quota_warn_threshold_value > 0 && \ + (!ob->quota_warn_threshold_is_percent || ob->quota_value > 0)) + + +/* Options specific to the appendfile transport. They must be in alphabetic +order (note that "_" comes before the lower case letters). Some of them are +stored in the publicly visible instance block - these are flagged with the +opt_public flag. */ + +optionlist appendfile_transport_options[] = { + { "*set_use_fcntl_lock",opt_bool | opt_hidden, + (void *)offsetof(appendfile_transport_options_block, set_use_fcntl) }, + { "*set_use_flock_lock",opt_bool | opt_hidden, + (void *)offsetof(appendfile_transport_options_block, set_use_flock) }, + { "*set_use_lockfile", opt_bool | opt_hidden, + (void *)offsetof(appendfile_transport_options_block, set_use_lockfile) }, +#ifdef SUPPORT_MBX + { "*set_use_mbx_lock", opt_bool | opt_hidden, + (void *)offsetof(appendfile_transport_options_block, set_use_mbx_lock) }, +#endif + { "allow_fifo", opt_bool, + (void *)offsetof(appendfile_transport_options_block, allow_fifo) }, + { "allow_symlink", opt_bool, + (void *)offsetof(appendfile_transport_options_block, allow_symlink) }, + { "batch_id", opt_stringptr | opt_public, + (void *)offsetof(transport_instance, batch_id) }, + { "batch_max", opt_int | opt_public, + (void *)offsetof(transport_instance, batch_max) }, + { "check_group", opt_bool, + (void *)offsetof(appendfile_transport_options_block, check_group) }, + { "check_owner", opt_bool, + (void *)offsetof(appendfile_transport_options_block, check_owner) }, + { "check_string", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, check_string) }, + { "create_directory", opt_bool, + (void *)offsetof(appendfile_transport_options_block, create_directory) }, + { "create_file", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, create_file_string) }, + { "directory", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, dirname) }, + { "directory_file", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, dirfilename) }, + { "directory_mode", opt_octint, + (void *)offsetof(appendfile_transport_options_block, dirmode) }, + { "escape_string", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, escape_string) }, + { "file", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, filename) }, + { "file_format", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, file_format) }, + { "file_must_exist", opt_bool, + (void *)offsetof(appendfile_transport_options_block, file_must_exist) }, + { "lock_fcntl_timeout", opt_time, + (void *)offsetof(appendfile_transport_options_block, lock_fcntl_timeout) }, + { "lock_flock_timeout", opt_time, + (void *)offsetof(appendfile_transport_options_block, lock_flock_timeout) }, + { "lock_interval", opt_time, + (void *)offsetof(appendfile_transport_options_block, lock_interval) }, + { "lock_retries", opt_int, + (void *)offsetof(appendfile_transport_options_block, lock_retries) }, + { "lockfile_mode", opt_octint, + (void *)offsetof(appendfile_transport_options_block, lockfile_mode) }, + { "lockfile_timeout", opt_time, + (void *)offsetof(appendfile_transport_options_block, lockfile_timeout) }, + { "mailbox_filecount", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, mailbox_filecount_string) }, + { "mailbox_size", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, mailbox_size_string) }, +#ifdef SUPPORT_MAILDIR + { "maildir_format", opt_bool, + (void *)offsetof(appendfile_transport_options_block, maildir_format ) } , + { "maildir_quota_directory_regex", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, maildir_dir_regex) }, + { "maildir_retries", opt_int, + (void *)offsetof(appendfile_transport_options_block, maildir_retries) }, + { "maildir_tag", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, maildir_tag) }, + { "maildir_use_size_file", opt_bool, + (void *)offsetof(appendfile_transport_options_block, maildir_use_size_file ) } , +#endif /* SUPPORT_MAILDIR */ +#ifdef SUPPORT_MAILSTORE + { "mailstore_format", opt_bool, + (void *)offsetof(appendfile_transport_options_block, mailstore_format ) }, + { "mailstore_prefix", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, mailstore_prefix ) }, + { "mailstore_suffix", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, mailstore_suffix ) }, +#endif /* SUPPORT_MAILSTORE */ +#ifdef SUPPORT_MBX + { "mbx_format", opt_bool, + (void *)offsetof(appendfile_transport_options_block, mbx_format ) } , +#endif /* SUPPORT_MBX */ + { "message_prefix", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, message_prefix) }, + { "message_suffix", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, message_suffix) }, + { "mode", opt_octint, + (void *)offsetof(appendfile_transport_options_block, mode) }, + { "mode_fail_narrower",opt_bool, + (void *)offsetof(appendfile_transport_options_block, mode_fail_narrower) }, + { "notify_comsat", opt_bool, + (void *)offsetof(appendfile_transport_options_block, notify_comsat) }, + { "quota", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, quota) }, + { "quota_directory", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, quota_directory) }, + { "quota_filecount", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, quota_filecount) }, + { "quota_is_inclusive", opt_bool, + (void *)offsetof(appendfile_transport_options_block, quota_is_inclusive) }, + { "quota_size_regex", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, quota_size_regex) }, + { "quota_warn_message", opt_stringptr | opt_public, + (void *)offsetof(transport_instance, warn_message) }, + { "quota_warn_threshold", opt_stringptr, + (void *)offsetof(appendfile_transport_options_block, quota_warn_threshold) }, + { "use_bsmtp", opt_bool, + (void *)offsetof(appendfile_transport_options_block, use_bsmtp) }, + { "use_crlf", opt_bool, + (void *)offsetof(appendfile_transport_options_block, use_crlf) }, + { "use_fcntl_lock", opt_bool_set, + (void *)offsetof(appendfile_transport_options_block, use_fcntl) }, + { "use_flock_lock", opt_bool_set, + (void *)offsetof(appendfile_transport_options_block, use_flock) }, + { "use_lockfile", opt_bool_set, + (void *)offsetof(appendfile_transport_options_block, use_lockfile) }, +#ifdef SUPPORT_MBX + { "use_mbx_lock", opt_bool_set, + (void *)offsetof(appendfile_transport_options_block, use_mbx_lock) }, +#endif /* SUPPORT_MBX */ +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int appendfile_transport_options_count = + sizeof(appendfile_transport_options)/sizeof(optionlist); + +/* Default private options block for the appendfile transport. */ + +appendfile_transport_options_block appendfile_transport_option_defaults = { + NULL, /* filename */ + NULL, /* dirname */ + US"q${base62:$tod_epoch}-$inode", /* dirfilename */ + NULL, /* message_prefix (default reset in init if not bsmtp) */ + NULL, /* message_suffix (ditto) */ + US"anywhere", /* create_file_string (string value for create_file) */ + NULL, /* quota */ + NULL, /* quota_directory */ + NULL, /* quota_filecount */ + NULL, /* quota_size_regex */ + NULL, /* quota_warn_threshold */ + NULL, /* mailbox_size_string */ + NULL, /* mailbox_filecount_string */ + US"^(?:cur|new|\\..*)$", /* maildir_dir_regex */ + NULL, /* maildir_tag */ + NULL, /* mailstore_prefix */ + NULL, /* mailstore_suffix */ + NULL, /* check_string (default changed for non-bsmtp file)*/ + NULL, /* escape_string (ditto) */ + NULL, /* file_format */ + -1, /* mailbox_size_value */ + -1, /* mailbox_filecount_value */ + 0, /* quota_value */ + 0, /* quota_filecount_value */ + 0, /* quota_warn_threshold_value */ + APPENDFILE_MODE, /* mode */ + APPENDFILE_DIRECTORY_MODE, /* dirmode */ + APPENDFILE_LOCKFILE_MODE, /* lockfile_mode */ + 30*60, /* lockfile_timeout */ + 0, /* lock_fcntl_timeout */ + 0, /* lock_flock_timeout */ + 10, /* lock_retries */ + 3, /* lock_interval */ + 10, /* maildir_retries */ + create_anywhere,/* create_file */ + 0, /* options */ + FALSE, /* allow_fifo */ + FALSE, /* allow_symlink */ + FALSE, /* check_group */ + TRUE, /* check_owner */ + TRUE, /* create_directory */ + FALSE, /* notify_comsat */ + TRUE, /* use_lockfile */ + FALSE, /* set_use_lockfile */ + TRUE, /* use_fcntl */ + FALSE, /* set_use_fcntl */ + FALSE, /* use_flock */ + FALSE, /* set_use_flock */ + FALSE, /* use_mbx_lock */ + FALSE, /* set_use_mbx_lock */ + FALSE, /* use_bsmtp */ + FALSE, /* use_crlf */ + FALSE, /* file_must_exist */ + TRUE, /* mode_fail_narrower */ + FALSE, /* maildir_format */ + FALSE, /* maildir_use_size_file */ + FALSE, /* mailstore_format */ + FALSE, /* mbx_format */ + FALSE, /* quota_warn_threshold_is_percent */ + TRUE /* quota_is_inclusive */ +}; + + + +/************************************************* +* Setup entry point * +*************************************************/ + +/* Called for each delivery in the privileged state, just before the uid/gid +are changed and the main entry point is called. We use this function to +expand any quota settings, so that it can access files that may not be readable +by the user. It is also used to pick up external mailbox size information, if +set. + +Arguments: + tblock points to the transport instance + addrlist addresses about to be delivered (not used) + dummy not used (doesn't pass back data) + errmsg where to put an error message + +Returns: OK, FAIL, or DEFER +*/ + +static int +appendfile_transport_setup(transport_instance *tblock, address_item *addrlist, + transport_feedback *dummy, uschar **errmsg) +{ +appendfile_transport_options_block *ob = + (appendfile_transport_options_block *)(tblock->options_block); +uschar *q = ob->quota; +int *v = &(ob->quota_value); +int default_value = 0; +int i; + +addrlist = addrlist; /* Keep picky compilers happy */ +dummy = dummy; + +/* Loop for quota, quota_filecount, quota_warn_threshold, mailbox_size, +mailbox_filecount */ + +for (i = 0; i < 5; i++) + { + if (q == NULL) *v = default_value; else + { + double d; + uschar *rest; + uschar *s = expand_string(q); + + if (s == NULL) + { + *errmsg = string_sprintf("Expansion of \"%s\" in %s transport failed: " + "%s", q, tblock->name, expand_string_message); + return search_find_defer? DEFER : FAIL; + } + + d = Ustrtod(s, &rest); + + /* Handle following characters K, M, %, the latter being permitted + for quota_warn_threshold only. A threshold with no quota setting is + just ignored. */ + + if (tolower(*rest) == 'k') { d *= 1024.0; rest++; } + else if (tolower(*rest) == 'm') { d *= 1024.0*1024.0; rest++; } + else if (*rest == '%' && i == 2) + { + if (ob->quota_value <= 0 && !ob->maildir_use_size_file) d = 0; + else if ((int)d < 0 || (int)d > 100) + { + *errmsg = string_sprintf("Invalid quota_warn_threshold percentage (%d)" + " for %s transport", (int)d, tblock->name); + return FAIL; + } + ob->quota_warn_threshold_is_percent = TRUE; + rest++; + } + + while (isspace(*rest)) rest++; + + if (*rest != 0) + { + *errmsg = string_sprintf("Malformed value \"%s\" (expansion of \"%s\") " + "in %s transport", s, q, tblock->name); + return FAIL; + } + + *v = (int)d; + } + + switch (i) + { + case 0: + q = ob->quota_filecount; + v = &(ob->quota_filecount_value); + break; + + case 1: + q = ob->quota_warn_threshold; + v = &(ob->quota_warn_threshold_value); + break; + + case 2: + q = ob->mailbox_size_string; + v = &(ob->mailbox_size_value); + default_value = -1; + break; + + case 3: + q = ob->mailbox_filecount_string; + v = &(ob->mailbox_filecount_value); + break; + } + } + +return OK; +} + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +appendfile_transport_init(transport_instance *tblock) +{ +appendfile_transport_options_block *ob = + (appendfile_transport_options_block *)(tblock->options_block); + +/* Set up the setup entry point, to be called in the privileged state */ + +tblock->setup = appendfile_transport_setup; + +/* Lock_retries must be greater than zero */ + +if (ob->lock_retries == 0) ob->lock_retries = 1; + +/* Only one of a file name or directory name must be given. */ + +if (ob->filename != NULL && ob->dirname != NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "only one of \"file\" or \"directory\" can be specified", tblock->name); + +/* If a file name was specified, neither quota_filecount nor quota_directory +must be given. */ + +if (ob->filename != NULL) + { + if (ob->quota_filecount != NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "quota_filecount must not be set without \"directory\"", tblock->name); + if (ob->quota_directory != NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "quota_directory must not be set without \"directory\"", tblock->name); + } + +/* The default locking depends on whether MBX is set or not. Change the +built-in default if none of the lock options has been explicitly set. At least +one form of locking is required in all cases, but mbx locking changes the +meaning of fcntl and flock locking. */ + +/* Not all operating systems provide flock(). For those that do, if flock is +requested, the default for fcntl is FALSE. */ + +if (ob->use_flock) + { + #ifdef NO_FLOCK + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "flock() support was not available in the operating system when this " + "binary was built", tblock->name); + #endif /* NO_FLOCK */ + if (!ob->set_use_fcntl) ob->use_fcntl = FALSE; + } + +#ifdef SUPPORT_MBX +if (ob->mbx_format) + { + if (!ob->set_use_lockfile && !ob->set_use_fcntl && !ob->set_use_flock && + !ob->set_use_mbx_lock) + { + ob->use_lockfile = ob->use_flock = FALSE; + ob->use_mbx_lock = ob->use_fcntl = TRUE; + } + else if (ob->use_mbx_lock) + { + if (!ob->set_use_lockfile) ob->use_lockfile = FALSE; + if (!ob->set_use_fcntl) ob->use_fcntl = FALSE; + if (!ob->set_use_flock) ob->use_flock = FALSE; + if (!ob->use_fcntl && !ob->use_flock) ob->use_fcntl = TRUE; + } + } +#endif /* SUPPORT_MBX */ + +if (!ob->use_fcntl && !ob->use_flock && !ob->use_lockfile && !ob->use_mbx_lock) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "no locking configured", tblock->name); + +/* Unset timeouts for non-used locking types */ + +if (!ob->use_fcntl) ob->lock_fcntl_timeout = 0; +if (!ob->use_flock) ob->lock_flock_timeout = 0; + +/* If a directory name was specified, only one of maildir or mailstore may be +specified, and if quota_filecount or quota_directory is given, quota must +be set. */ + +if (ob->dirname != NULL) + { + if (ob->maildir_format && ob->mailstore_format) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "only one of maildir and mailstore may be specified", tblock->name); + if (ob->quota_filecount != NULL && ob->quota == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "quota must be set if quota_filecount is set", tblock->name); + if (ob->quota_directory != NULL && ob->quota == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n " + "quota must be set if quota_directory is set", tblock->name); + } + +/* If a fixed uid field is set, then a gid field must also be set. */ + +if (tblock->uid_set && !tblock->gid_set && tblock->expand_gid == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "user set without group for the %s transport", tblock->name); + +/* If "create_file" is set, check that a valid option is given, and set the +integer variable. */ + +if (ob->create_file_string != NULL) + { + int value = 0; + if (Ustrcmp(ob->create_file_string, "anywhere") == 0) value = create_anywhere; + else if (Ustrcmp(ob->create_file_string, "belowhome") == 0) value = + create_belowhome; + else if (Ustrcmp(ob->create_file_string, "inhome") == 0) + value = create_inhome; + else + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "invalid value given for \"file_create\" for the %s transport: %s", + tblock->name, ob->create_file_string); + ob->create_file = value; + } + +/* If quota_warn_threshold is set, set up default for warn_message. It may +not be used if the actual threshold for a given delivery ends up as zero, +of if it's given as a percentage and there's no quota setting. */ + +if (ob->quota_warn_threshold != NULL) + { + if (tblock->warn_message == NULL) tblock->warn_message = US + "To: $local_part@$domain\n" + "Subject: Your mailbox\n\n" + "This message is automatically created by mail delivery software (Exim).\n\n" + "The size of your mailbox has exceeded a warning threshold that is\n" + "set by the system administrator.\n"; + } + +/* If batch SMTP is set, force the check and escape strings, and arrange that +headers are also escaped. */ + +if (ob->use_bsmtp) + { + ob->check_string = US"."; + ob->escape_string = US".."; + ob->options |= topt_escape_headers; + } + +/* If not batch SMTP, not maildir, not mailstore, and directory is not set, +insert default values for for the affixes and the check/escape strings. */ + +else if (ob->dirname == NULL && !ob->maildir_format && !ob->mailstore_format) + { + if (ob->message_prefix == NULL) ob->message_prefix = + US"From ${if def:return_path{$return_path}{MAILER-DAEMON}} ${tod_bsdinbox}\n"; + if (ob->message_suffix == NULL) ob->message_suffix = US"\n"; + if (ob->check_string == NULL) ob->check_string = US"From "; + if (ob->escape_string == NULL) ob->escape_string = US">From "; + + } + +/* Set up the bitwise options for transport_write_message from the various +driver options. Only one of body_only and headers_only can be set. */ + +ob->options |= + (tblock->body_only? topt_no_headers : 0) | + (tblock->headers_only? topt_no_body : 0) | + (tblock->return_path_add? topt_add_return_path : 0) | + (tblock->delivery_date_add? topt_add_delivery_date : 0) | + (tblock->envelope_to_add? topt_add_envelope_to : 0) | + ((ob->use_crlf || ob->mbx_format)? topt_use_crlf : 0); +} + + + +/************************************************* +* Notify comsat * +*************************************************/ + +/* The comsat daemon is the thing that provides asynchronous notification of +the arrival of local messages, if requested by the user by "biff y". It is a +BSD thing that uses a TCP/IP protocol for communication. A message consisting +of the text "user@offset" must be sent, where offset is the place in the +mailbox where new mail starts. There is no scope for telling it which file to +look at, which makes it a less than useful if mail is being delivered into a +non-standard place such as the user's home directory. In fact, it doesn't seem +to pay much attention to the offset. + +Arguments: + user user name + offset offset in mailbox + +Returns: nothing +*/ + +static void +notify_comsat(uschar *user, int offset) +{ +struct servent *sp; +host_item host; +host_item *h; +uschar buffer[256]; + +DEBUG(D_transport) debug_printf("notify_comsat called\n"); + +sprintf(CS buffer, "%.200s@%d\n", user, offset); + +if ((sp = getservbyname("biff", "udp")) == NULL) + { + DEBUG(D_transport) debug_printf("biff/udp is an unknown service"); + return; + } + +host.name = US"localhost"; +host.next = NULL; + + +/* This code is all set up to look up "localhost" and use all its addresses +until one succeeds. However, it appears that at least on some systems, comsat +doesn't listen on the ::1 address. So for the moment, just force the address to +be 127.0.0.1. At some future stage, when IPv6 really is superseding IPv4, this +can be changed. */ + +/****** +if (host_find_byname(&host, NULL, NULL, FALSE) == HOST_FIND_FAILED) + { + DEBUG(D_transport) debug_printf("\"localhost\" unknown\n"); + return; + } +******/ + +host.address = US"127.0.0.1"; + + +for (h = &host; h != NULL; h = h->next) + { + int sock, rc; + int host_af = (Ustrchr(h->address, ':') != NULL)? AF_INET6 : AF_INET; + + DEBUG(D_transport) debug_printf("calling comsat on %s\n", h->address); + + sock = ip_socket(SOCK_DGRAM, host_af); + if (sock < 0) continue; + + /* Connect never fails for a UDP socket, so don't set a timeout. */ + + (void)ip_connect(sock, host_af, h->address, ntohs(sp->s_port), 0); + rc = send(sock, buffer, Ustrlen(buffer) + 1, 0); + close(sock); + + if (rc >= 0) break; + DEBUG(D_transport) + debug_printf("send to comsat failed for %s: %s\n", strerror(errno), + h->address); + } +} + + + +/************************************************* +* Check the format of a file * +*************************************************/ + +/* This function is called when file_format is set, to check that an existing +file has the right format. The format string contains text/transport pairs. The +string matching is literal. we just read big_buffer_size bytes, because this is +all about the first few bytes of a file. + +Arguments: + cfd the open file + tblock the transport block + addr the address block - for inserting error data + +Returns: pointer to the required transport, or NULL +*/ + +transport_instance * +check_file_format(int cfd, transport_instance *tblock, address_item *addr) +{ +uschar *format = + ((appendfile_transport_options_block *)(tblock->options_block))->file_format; +uschar data[256]; +int len = read(cfd, data, sizeof(data)); +int sep = 0; +uschar *s; + +DEBUG(D_transport) debug_printf("checking file format\n"); + +/* An empty file matches the current transport */ + +if (len == 0) return tblock; + +/* Search the formats for a match */ + +while ((s = string_nextinlist(&format,&sep,big_buffer,big_buffer_size))!= NULL) + { + int slen = Ustrlen(s); + BOOL match = len >= slen && Ustrncmp(data, s, slen) == 0; + uschar *tp = string_nextinlist(&format, &sep, big_buffer, big_buffer_size); + if (match) + { + transport_instance *tt; + for (tt = transports; tt != NULL; tt = tt->next) + if (Ustrcmp(tp, tt->name) == 0) + { + DEBUG(D_transport) + debug_printf("file format -> %s transport\n", tt->name); + return tt; + } + addr->basic_errno = ERRNO_BADTRANSPORT; + addr->message = string_sprintf("%s transport (for %.*s format) not found", + tp, slen, data); + return NULL; + } + } + +/* Failed to find a match */ + +addr->basic_errno = ERRNO_FORMATUNKNOWN; +addr->message = US"mailbox file format unrecognized"; +return NULL; +} + + + + +/************************************************* +* Check directory's files for quota * +*************************************************/ + +/* This function is called if quota is set for one of the delivery modes that +delivers into a specific directory. It scans the directory and stats all the +files in order to get a total size and count. This is an expensive thing to do, +but some people are prepared to bear the cost. Alternatively, if size_regex is +set, it is used as a regex to try to extract the size from the file name, a +strategy that some people use on maildir files on systems where the users have +no shell access. + +The function is global, because it is also called from tf_maildir.c for maildir +folders (which should contain only regular files). + +Note: Any problems can be written to debugging output, but cannot be written to +the log, because we are running as an unprivileged user here. + +Arguments: + dirname the name of the directory + countptr where to add the file count (because this function recurses) + regex a compiled regex to get the size from a name + +Returns: the sum of the sizes of the stattable files + zero if the directory cannot be opened +*/ + +int +check_dir_size(uschar *dirname, int *countptr, const pcre *regex) +{ +DIR *dir; +int sum = 0; +int count = *countptr; +struct dirent *ent; +struct stat statbuf; + +dir = opendir(CS dirname); +if (dir == NULL) return 0; + +while ((ent = readdir(dir)) != NULL) + { + uschar *name = US ent->d_name; + uschar buffer[1024]; + + if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue; + + count++; + + /* If there's a regex, try to find the size using it */ + + if (regex != NULL) + { + int ovector[6]; + if (pcre_exec(regex, NULL, CS name, Ustrlen(name), 0, 0, ovector,6) >= 2) + { + uschar *endptr; + int size = Ustrtol(name + ovector[2], &endptr, 10); + if (endptr == name + ovector[3]) + { + sum += size; + DEBUG(D_transport) + debug_printf("check_dir_size: size from %s is %d\n", name, size); + continue; + } + } + DEBUG(D_transport) + debug_printf("check_dir_size: regex did not match %s\n", name); + } + + /* No regex or no match for the regex, or captured non-digits */ + + if (!string_format(buffer, sizeof(buffer), "%s/%s", dirname, name)) + { + DEBUG(D_transport) + debug_printf("check_dir_size: name too long: dir=%s name=%s\n", dirname, + name); + continue; + } + + if (Ustat(buffer, &statbuf) < 0) + { + DEBUG(D_transport) + debug_printf("check_dir_size: stat error %d for %s: %s\n", errno, buffer, + strerror(errno)); + continue; + } + + if ((statbuf.st_mode & S_IFMT) == S_IFREG) + sum += statbuf.st_size; + else if ((statbuf.st_mode & S_IFMT) == S_IFDIR) + sum += check_dir_size(buffer, &count, regex); + } + +closedir(dir); +DEBUG(D_transport) + debug_printf("check_dir_size: dir=%s sum=%d count=%d\n", dirname, sum, count); +*countptr = count; +return sum; +} + + + + +/************************************************* +* Apply a lock to a file descriptor * +*************************************************/ + +/* This function applies a lock to a file descriptor, using a blocking or +non-blocking lock, depending on the timeout value. It can apply either or +both of a fcntl() and a flock() lock. However, not all OS support flock(); +for those that don't, the use_flock option cannot be set. + +Arguments: + fd the file descriptor + fcntltype type of lock, specified as F_WRLCK or F_RDLCK (that is, in + fcntl() format); the flock() type is deduced if needed + dofcntl do fcntl() locking + fcntltime non-zero to use blocking fcntl() + doflock do flock() locking + flocktime non-zero to use blocking flock() + +Returns: yield of the fcntl() or flock() call, with errno preserved; + sigalrm_seen set if there has been a timeout +*/ + +static int +apply_lock(int fd, int fcntltype, BOOL dofcntl, int fcntltime, BOOL doflock, + int flocktime) +{ +int yield = 0; +int save_errno; +struct flock lock_data; +lock_data.l_type = fcntltype; +lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0; + +sigalrm_seen = FALSE; + +if (dofcntl) + { + if (fcntltime > 0) + { + alarm(fcntltime); + yield = fcntl(fd, F_SETLKW, &lock_data); + save_errno = errno; + alarm(0); + errno = save_errno; + } + else yield = fcntl(fd, F_SETLK, &lock_data); + } + +#ifndef NO_FLOCK +if (doflock && (yield >= 0)) + { + int flocktype = (fcntltype == F_WRLCK)? LOCK_EX : LOCK_SH; + if (flocktime > 0) + { + alarm(flocktime); + yield = flock(fd, flocktype); + save_errno = errno; + alarm(0); + errno = save_errno; + } + else yield = flock(fd, flocktype | LOCK_NB); + } +#endif /* NO_FLOCK */ + +return yield; +} + + + + +#ifdef SUPPORT_MBX +/************************************************* +* Copy message into MBX mailbox * +*************************************************/ + +/* This function is called when a message intended for a MBX mailbox has been +written to a temporary file. We can now get the size of the message and then +copy it in MBX format to the mailbox. + +Arguments: + to_fd fd to write to (the real mailbox) + from_fd fd to read from (the temporary file) + saved_size current size of mailbox + +Returns: OK if all went well, DEFER otherwise, with errno preserved + the number of bytes written are added to transport_count + by virtue of calling transport_write_block() +*/ + +/* Values taken from c-client */ + +#define MBX_HDRSIZE 2048 +#define MBX_NUSERFLAGS 30 + +static int +copy_mbx_message(int to_fd, int from_fd, int saved_size) +{ +int used, size; +struct stat statbuf; + +/* If the current mailbox size is zero, write a header block */ + +if (saved_size == 0) + { + int i; + uschar *s; + memset (deliver_out_buffer, '\0', MBX_HDRSIZE); + sprintf(CS(s = deliver_out_buffer), "*mbx*\015\012%08lx00000000\015\012", + (long int)time(NULL)); + for (i = 0; i < MBX_NUSERFLAGS; i++) + sprintf (CS(s += Ustrlen(s)), "\015\012"); + if (!transport_write_block (to_fd, deliver_out_buffer, MBX_HDRSIZE)) + return DEFER; + } + +DEBUG(D_transport) debug_printf("copying MBX message from temporary file\n"); + +/* Now construct the message's header from the time and the RFC822 file +size, including CRLFs, which is the size of the input (temporary) file. */ + +if (fstat(from_fd, &statbuf) < 0) return DEFER; +size = statbuf.st_size; + +sprintf (CS deliver_out_buffer, "%s,%lu;%08lx%04x-%08x\015\012", + tod_stamp(tod_mbx), (long unsigned int)size, 0L, 0, 0); +used = Ustrlen(deliver_out_buffer); + +/* Rewind the temporary file, and copy it over in chunks. */ + +lseek(from_fd, 0 , SEEK_SET); + +while (size > 0) + { + int len = read(from_fd, deliver_out_buffer + used, + DELIVER_OUT_BUFFER_SIZE - used); + if (len <= 0) + { + if (len == 0) errno = ERRNO_MBXLENGTH; + return DEFER; + } + if (!transport_write_block(to_fd, deliver_out_buffer, used + len)) + return DEFER; + size -= len; + used = 0; + } + +return OK; +} +#endif /* SUPPORT_MBX */ + + + +/************************************************* +* Check creation is permitted * +*************************************************/ + +/* This function checks whether a given file name is permitted to be created, +as controlled by the create_file option. If no home directory is set, however, +we can't do any tests. + +Arguments: + filename the file name + create_file the ob->create_file option + +Returns: TRUE if creation is permitted +*/ + +static BOOL +check_creation(uschar *filename, int create_file) +{ +BOOL yield = TRUE; + +if (deliver_home != NULL && create_file != create_anywhere) + { + int len = Ustrlen(deliver_home); + uschar *file = filename; + + while (file[0] == '/' && file[1] == '/') file++; + if (Ustrncmp(file, deliver_home, len) != 0 || file[len] != '/' || + ( Ustrchr(file+len+2, '/') != NULL && + ( + create_file != create_belowhome || + Ustrstr(file+len, "/../") != NULL + ) + ) + ) yield = FALSE; + + /* If yield is TRUE, the file name starts with the home directory, and does + not contain any instances of "/../" in the "belowhome" case. However, it may + still contain symbolic links. We can check for this by making use of + realpath(), which most Unixes seem to have (but make it possible to cut this + out). We can't just use realpath() on the whole file name, because we know + the file itself doesn't exist, and intermediate directories may also not + exist. What we want to know is the real path of the longest existing part of + the path. That must match the home directory's beginning, whichever is the + shorter. */ + + #ifndef NO_REALPATH + if (yield && create_file == create_belowhome) + { + uschar *slash, *next; + uschar *rp = NULL; + for (slash = Ustrrchr(file, '/'); /* There is known to be one */ + rp == NULL && slash > file; /* Stop if reached beginning */ + slash = next) + { + *slash = 0; + rp = US realpath(CS file, CS big_buffer); + next = Ustrrchr(file, '/'); + *slash = '/'; + } + + /* If rp == NULL it means that none of the relevant directories exist. + This is not a problem here - it means that no symbolic links can exist, + which is all we are worried about. Otherwise, we must compare it + against the start of the home directory. However, that may itself + contain symbolic links, so we have to "realpath" it as well, if + possible. */ + + if (rp != NULL) + { + uschar hdbuffer[PATH_MAX+1]; + uschar *rph = deliver_home; + int rlen = Ustrlen(big_buffer); + + rp = US realpath(CS deliver_home, CS hdbuffer); + if (rp != NULL) + { + rph = hdbuffer; + len = Ustrlen(rph); + } + + if (rlen > len) rlen = len; + if (Ustrncmp(rph, big_buffer, rlen) != 0) + { + yield = FALSE; + DEBUG(D_transport) debug_printf("Real path \"%s\" does not match \"%s\"\n", + big_buffer, deliver_home); + } + } + } + #endif /* NO_REALPATH */ + } + +return yield; +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for general interface details. This transport always +returns FALSE, indicating that the status which has been placed in the first +address should be copied to any other addresses in a batch. + +Appendfile delivery is tricky and has led to various security problems in other +mailers. The logic used here is therefore laid out in some detail. When this +function is called, we are running in a subprocess which has had its gid and +uid set to the appropriate values. Therefore, we cannot write directly to the +exim logs. Any errors must be handled by setting appropriate return codes. +Note that the default setting for addr->transport_return is DEFER, so it need +not be set unless some other value is required. + +The code below calls geteuid() rather than getuid() to get the current uid +because in weird configurations not running setuid root there may be a +difference. In the standard configuration, where setuid() has been used in the +delivery process, there will be no difference between the uid and the euid. + +(1) If the af_file flag is set, this is a delivery to a file after .forward or + alias expansion. Otherwise, there must be a configured file name or + directory name. + +The following items apply in the case when a file name (as opposed to a +directory name) is given, that is, when appending to a single file: + +(2f) Expand the file name. + +(3f) If the file name is /dev/null, return success (optimization). + +(4f) If the file_format options is set, open the file for reading, and check + that the bytes at the start of the file match one of the given strings. + If the check indicates a transport other than the current one should be + used, pass control to that other transport. Otherwise continue. An empty + or non-existent file matches the current transport. The file is closed + after the check. + +(5f) If a lock file is required, create it (see extensive separate comments + below about the algorithm for doing this). It is important to do this + before opening the mailbox if NFS is in use. + +(6f) Stat the file, using lstat() rather than stat(), in order to pick up + details of any symbolic link. + +(7f) If the file already exists: + + Check the owner and group if necessary, and defer if they are wrong. + + If it is a symbolic link AND the allow_symlink option is set (NOT the + default), go back to (6f) but this time use stat() instead of lstat(). + + If it's not a regular file (or FIFO when permitted), defer delivery. + + Check permissions. If the required permissions are *less* than the + existing ones, or supplied by the address (often by the user via filter), + chmod() the file. Otherwise, defer. + + Save the inode number. + + Open with O_RDRW + O_APPEND, thus failing if the file has vanished. + + If open fails because the file does not exist, go to (6f); on any other + failure, defer. + + Check the inode number hasn't changed - I realize this isn't perfect (an + inode can be reused) but it's cheap and will catch some of the races. + + Check it's still a regular file (or FIFO if permitted). + + Check that the owner and permissions haven't changed. + + If file_format is set, check that the file still matches the format for + the current transport. If not, defer delivery. + +(8f) If file does not exist initially: + + Open with O_WRONLY + O_EXCL + O_CREAT with configured mode, unless we know + this is via a symbolic link (only possible if allow_symlinks is set), in + which case don't use O_EXCL, as it dosn't work. + + If open fails because the file already exists, go to (6f). To avoid + looping for ever in a situation where the file is continuously being + created and deleted, all of this happens inside a loop that operates + lock_retries times and includes the fcntl and flock locking. If the + loop completes without the file getting opened, defer and request + freezing, because something really weird is happening. + + If open fails for any other reason, defer for subsequent delivery except + when this is a file delivery resulting from an alias or forward expansion + and the error is EPERM or ENOENT or EACCES, in which case FAIL as this is + most likely a user rather than a configuration error. + +(9f) We now have the file checked and open for writing. If so configured, lock + it using fcntl, flock, or MBX locking rules. If this fails, close the file + and goto (6f), up to lock_retries times, after sleeping for a while. If it + still fails, give up and defer delivery. + +(10f)Save the access time (for subsequent restoration) and the size of the + file, for comsat and for re-setting if delivery fails in the middle - + e.g. for quota exceeded. + +The following items apply in the case when a directory name is given: + +(2d) Create a new file in the directory using a temporary name, by opening for + writing and with O_CREAT. If maildir format is being used, the file + is created in a temporary subdirectory with a prescribed name. If + mailstore format is being used, the envelope file is first created with a + temporary name, then the data file. + +The following items apply in all cases: + +(11) We now have the file open for writing, and locked if it was given as a + file name. Write the message and flush the file, unless there is a setting + of the local quota option, in which case we can check for its excession + without doing any writing. + + In the case of MBX format mailboxes, the message is first written to a + temporary file, in order to get its correct length. This is then copied to + the real file, preceded by an MBX header. + + If there is a quota error on writing, defer the address. Timeout logic + will determine for how long retries are attempted. We restore the mailbox + to its original length if it's a single file. There doesn't seem to be a + uniform error code for quota excession (it even differs between SunOS4 + and some versions of SunOS5) so a system-dependent macro called + ERRNO_QUOTA is used for it, and the value gets put into errno_quota at + compile time. + + For any other error (most commonly disk full), do the same. + +The following applies after appending to a file: + +(12f)Restore the atime; notify_comsat if required; close the file (which + unlocks it if it was locked). Delete the lock file if it exists. + +The following applies after writing a unique file in a directory: + +(12d)For maildir format, rename the file into the new directory. For mailstore + format, rename the envelope file to its correct name. Otherwise, generate + a unique name from the directory_file option, and rename to that, possibly + trying a few times if the file exists and re-expanding the name gives a + different string. + +This transport yields FAIL only when a file name is generated by an alias or +forwarding operation and attempting to open it gives EPERM, ENOENT, or EACCES. +All other failures return DEFER (in addr->transport_return). */ + + +BOOL +appendfile_transport_entry( + transport_instance *tblock, /* data for this instantiation */ + address_item *addr) /* address we are working on */ +{ +appendfile_transport_options_block *ob = + (appendfile_transport_options_block *)(tblock->options_block); +struct stat statbuf; +uschar *fdname = NULL; +uschar *filename = NULL; +uschar *hitchname = NULL; +uschar *dataname = NULL; +uschar *lockname = NULL; +uschar *newname = NULL; +uschar *nametag = NULL; +uschar *cr = US""; +uschar *filecount_msg = US""; +uschar *path; +struct utimbuf times; +struct timeval msg_tv; +BOOL isdirectory = FALSE; +BOOL isfifo = FALSE; +BOOL wait_for_tick = FALSE; +uid_t uid = geteuid(); /* See note above */ +gid_t gid = getegid(); +int mbformat; +int mode = (addr->mode > 0)? addr->mode : ob->mode; +int saved_size = -1; +int mailbox_size = ob->mailbox_size_value; +int mailbox_filecount = ob->mailbox_filecount_value; +int hd = -1; +int fd = -1; +int yield = FAIL; +int i; + +#ifdef SUPPORT_MBX +int save_fd = 0; +int mbx_lockfd = -1; +uschar mbx_lockname[40]; +FILE *temp_file = NULL; +#endif /* SUPPORT_MBX */ + +#ifdef SUPPORT_MAILDIR +int maildirsize_fd = -1; /* fd for maildirsize file */ +int maildir_save_errno; +#endif + + +DEBUG(D_transport) debug_printf("appendfile transport entered\n"); + +/* An "address_file" or "address_directory" transport is used to deliver to +files specified via .forward or an alias file. Prior to release 4.20, the +"file" and "directory" options were ignored in this case. This has been changed +to allow the redirection data to specify what is in effect a folder, whose +location is determined by the options on the transport. + +Compatibility with the case when neither option is set is retained by forcing a +value for the file or directory name. A directory delivery is assumed if the +last character of the path from the router is '/'. + +The file path is in the local part of the address, but not in the $local_part +variable (that holds the parent local part). It is, however, in the +$address_file variable. Below, we update the local part in the address if it +changes by expansion, so that the final path ends up in the log. */ + +if (testflag(addr, af_file) && + ob->filename == NULL && + ob->dirname == NULL) + { + fdname = US"$address_file"; + if (address_file[Ustrlen(address_file)-1] == '/' || + ob->maildir_format || + ob->mailstore_format) + isdirectory = TRUE; + } + +/* Handle (a) an "address file" delivery where "file" or "directory" is +explicitly set and (b) a non-address_file delivery, where one of "file" or +"directory" must be set; initialization ensures that they are not both set. */ + +if (fdname == NULL) + { + fdname = ob->filename; + if (fdname == NULL) + { + fdname = ob->dirname; + isdirectory = TRUE; + } + if (fdname == NULL) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Mandatory file or directory option " + "missing from %s transport", tblock->name); + return FALSE; + } + } + +/* Maildir and mailstore require a directory */ + +if ((ob->maildir_format || ob->mailstore_format) && !isdirectory) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("mail%s_format requires \"directory\" " + "to be specified for the %s transport", + ob->maildir_format? "dir" : "store", tblock->name); + return FALSE; + } + +path = expand_string(fdname); + +if (path == NULL) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (file or directory " + "name for %s transport) failed: %s", fdname, tblock->name, + expand_string_message); + return FALSE; + } + +if (path[0] != '/') + { + addr->message = string_sprintf("appendfile: file or directory name " + "\"%s\" is not absolute", path); + addr->basic_errno = ERRNO_NOTABSOLUTE; + return FALSE; + } + +/* For a file delivery, make sure the local part in the address is updated to +the true local part. */ + +if (testflag(addr, af_file)) addr->local_part = string_copy(path); + +/* The available mailbox formats depend on whether it is a directory or a file +delivery. */ + +if (isdirectory) + { + mbformat = + #ifdef SUPPORT_MAILDIR + (ob->maildir_format)? mbf_maildir : + #endif + #ifdef SUPPORT_MAILSTORE + (ob->mailstore_format)? mbf_mailstore : + #endif + mbf_smail; + } +else + { + mbformat = + #ifdef SUPPORT_MBX + (ob->mbx_format)? mbf_mbx : + #endif + mbf_unix; + } + +DEBUG(D_transport) + { + debug_printf("appendfile: mode=%o notify_comsat=%d quota=%d warning=%d%s\n" + " %s=%s format=%s\n message_prefix=%s\n message_suffix=%s\n " + "maildir_use_size_file=%s\n", + mode, ob->notify_comsat, ob->quota_value, ob->quota_warn_threshold_value, + ob->quota_warn_threshold_is_percent? "%" : "", + isdirectory? "directory" : "file", + path, mailbox_formats[mbformat], + (ob->message_prefix == NULL)? US"null" : string_printing(ob->message_prefix), + (ob->message_suffix == NULL)? US"null" : string_printing(ob->message_suffix), + (ob->maildir_use_size_file)? "yes" : "no"); + + if (!isdirectory) debug_printf(" locking by %s%s%s%s%s\n", + ob->use_lockfile? "lockfile " : "", + ob->use_mbx_lock? "mbx locking (" : "", + ob->use_fcntl? "fcntl " : "", + ob->use_flock? "flock" : "", + ob->use_mbx_lock? ")" : ""); + } + +/* If the -N option is set, can't do any more. */ + +if (dont_deliver) + { + DEBUG(D_transport) + debug_printf("*** delivery by %s transport bypassed by -N option\n", + tblock->name); + addr->transport_return = OK; + return FALSE; + } + +/* Handle the case of a file name. If the file name is /dev/null, we can save +ourselves some effort and just give a success return right away. */ + +if (!isdirectory) + { + BOOL use_lstat = TRUE; + BOOL file_opened = FALSE; + BOOL allow_creation_here = TRUE; + + if (Ustrcmp(path, "/dev/null") == 0) + { + addr->transport_return = OK; + return FALSE; + } + + /* Set the name of the file to be opened, and the file to which the data + is written, and find out if we are permitted to create a non-existent file. */ + + dataname = filename = path; + allow_creation_here = check_creation(filename, ob->create_file); + + /* If ob->create_directory is set, attempt to create the directories in + which this mailbox lives, but only if we are permitted to create the file + itself. We know we are dealing with an absolute path, because this was + checked above. */ + + if (ob->create_directory && allow_creation_here) + { + uschar *p = Ustrrchr(path, '/'); + *p = '\0'; + if (!directory_make(NULL, path, ob->dirmode, FALSE)) + { + addr->basic_errno = errno; + addr->message = + string_sprintf("failed to create directories for %s: %s", path, + strerror(errno)); + DEBUG(D_transport) debug_printf("%s transport: %s\n", tblock->name, path); + return FALSE; + } + *p = '/'; + } + + /* If file_format is set we must check that any existing file matches one of + the configured formats by checking the bytes it starts with. A match then + indicates a specific transport - if it is not this one, pass control to it. + Otherwise carry on here. An empty or non-existent file matches the current + transport. We don't need to distinguish between non-existence and other open + failures because if an existing file fails to open here, it will also fail + again later when O_RDWR is used. */ + + if (ob->file_format != NULL) + { + int cfd = Uopen(path, O_RDONLY, 0); + if (cfd >= 0) + { + transport_instance *tt = check_file_format(cfd, tblock, addr); + close(cfd); + + /* If another transport is indicated, call it and return; if no transport + was found, just return - the error data will have been set up.*/ + + if (tt != tblock) + { + if (tt != NULL) + { + set_process_info("delivering %s to %s using %s", message_id, + addr->local_part, tt->name); + debug_print_string(tt->debug_string); + addr->transport = tt; + (tt->info->code)(tt, addr); + } + return FALSE; + } + } + } + + /* The locking of mailbox files is worse than the naming of cats, which is + known to be "a difficult matter" (T.S. Eliot) and just as cats must have + three different names, so several different styles of locking are used. + + Research in other programs that lock mailboxes shows that there is no + universally standard method. Having mailboxes NFS-mounted on the system that + is delivering mail is not the best thing, but people do run like this, + and so the code must do its best to cope. + + Three different locking mechanisms are supported. The initialization function + checks that at least one is configured. + + LOCK FILES + + Unless no_use_lockfile is set, we attempt to build a lock file in a way that + will work over NFS. Only after that is done do we actually open the mailbox + and apply locks to it (if configured). + + Originally, Exim got the file opened before doing anything about locking. + However, a very occasional problem was observed on Solaris 2 when delivering + over NFS. It is seems that when a file is opened with O_APPEND, the file size + gets remembered at open time. If another process on another host (that's + important) has the file open and locked and writes to it and then releases + the lock while the first process is waiting to get the lock, the first + process may fail to write at the new end point of the file - despite the very + definite statement about O_APPEND in the man page for write(). Experiments + have reproduced this problem, but I do not know any way of forcing a host to + update its attribute cache for an open NFS file. It would be nice if it did + so when a lock was taken out, but this does not seem to happen. Anyway, to + reduce the risk of this problem happening, we now create the lock file + (if configured) *before* opening the mailbox. That will prevent two different + Exims opening the file simultaneously. It may not prevent clashes with MUAs, + however, but Pine at least seems to operate in the same way. + + Lockfiles should normally be used when NFS is involved, because of the above + problem. + + The logic for creating the lock file is: + + . The name of the lock file is .lock + + . First, create a "hitching post" name by adding the primary host name, + current time and pid to the lock file name. This should be unique. + + . Create the hitching post file using WRONLY + CREAT + EXCL. + + . If that fails EACCES, we assume it means that the user is unable to create + files in the mail spool directory. Some installations might operate in this + manner, so there is a configuration option to allow this state not to be an + error - we proceed to lock using fcntl only, after the file is open. + + . Otherwise, an error causes a deferment of the address. + + . Hard link the hitching post to the lock file name. + + . If the link succeeds, we have successfully created the lock file. Simply + close and unlink the hitching post file. + + . If the link does not succeed, proceed as follows: + + o Fstat the hitching post file, and then close and unlink it. + + o Now examine the stat data. If the number of links to the file is exactly + 2, the linking succeeded but for some reason, e.g. an NFS server crash, + the return never made it back, so the link() function gave a failure + return. + + . This method allows for the lock file to be created by some other process + right up to the moment of the attempt to hard link it, and is also robust + against NFS server crash-reboots, which would probably result in timeouts + in the middle of link(). + + . System crashes may cause lock files to get left lying around, and some means + of flushing them is required. The approach of writing a pid (used by smail + and by elm) into the file isn't useful when NFS may be in use. Pine uses a + timeout, which seems a better approach. Since any program that writes to a + mailbox using a lock file should complete its task very quickly, Pine + removes lock files that are older than 5 minutes. We allow the value to be + configurable on the transport. + + FCNTL LOCKING + + If use_fcntl_lock is set, then Exim gets an exclusive fcntl() lock on the + mailbox once it is open. This is done by default with a non-blocking lock. + Failures to lock cause retries after a sleep, but only for a certain number + of tries. A blocking lock is deliberately not used so that we don't hold the + mailbox open. This minimizes the possibility of the NFS problem described + under LOCK FILES above, if for some reason NFS deliveries are happening + without lock files. However, the use of a non-blocking lock and sleep, though + the safest approach, does not give the best performance on very busy systems. + A blocking lock plus timeout does better. Therefore Exim has an option to + allow it to work this way. If lock_fcntl_timeout is set greater than zero, it + enables the use of blocking fcntl() calls. + + FLOCK LOCKING + + If use_flock_lock is set, then Exim gets an exclusive flock() lock in the + same manner as for fcntl locking above. No-blocking/timeout is also set as + above in lock_flock_timeout. Not all operating systems provide or support + flock(). For those that don't (as determined by the definition of LOCK_SH in + /usr/include/sys/file.h), use_flock_lock may not be set. For some OS, flock() + is implemented (not precisely) on top of fcntl(), which means there's no + point in actually using it. + + MBX LOCKING + + If use_mbx_lock is set (this is supported only if SUPPORT_MBX is defined) + then the rules used for locking in c-client are used. Exim takes out a shared + lock on the mailbox file, and an exclusive lock on the file whose name is + /tmp/... The shared lock on the mailbox stops + any other MBX client from getting an exclusive lock on it and expunging it. + It also stops any other MBX client from unlinking the /tmp lock when it has + finished with it. + + The exclusive lock on the /tmp file prevents any other MBX client from + updating the mailbox in any way. When writing is finished, if an exclusive + lock on the mailbox can be obtained, indicating there are no current sharers, + the /tmp file is unlinked. + + MBX locking can use either fcntl() or flock() locking. If neither + use_fcntl_lock or use_flock_lock is set, it defaults to using fcntl() only. + The calls for getting these locks are by default non-blocking, as for non-mbx + locking, but can be made blocking by setting lock_fcntl_timeout and/or + lock_flock_timeout as appropriate. As MBX delivery doesn't work over NFS, it + probably makes sense to set timeouts for any MBX deliveries. */ + + + /* Build a lock file if configured to do so - the existence of a lock + file is subsequently checked by looking for a non-negative value of the + file descriptor hd - even though the file is no longer open. */ + + if (ob->use_lockfile) + { + lockname = string_sprintf("%s.lock", filename); + hitchname = string_sprintf( "%s.%s.%08x.%08x", lockname, primary_hostname, + (unsigned int)(time(NULL)), (unsigned int)getpid()); + + DEBUG(D_transport) debug_printf("lock name: %s\nhitch name: %s\n", lockname, + hitchname); + + /* Lock file creation retry loop */ + + for (i = 0; i < ob->lock_retries; sleep(ob->lock_interval), i++) + { + int rc; + hd = Uopen(hitchname, O_WRONLY | O_CREAT | O_EXCL, ob->lockfile_mode); + + if (hd < 0) + { + addr->basic_errno = errno; + addr->message = + string_sprintf("creating lock file hitching post %s " + "(euid=%ld egid=%ld)", hitchname, (long int)geteuid(), + (long int)getegid()); + return FALSE; + } + + /* Attempt to hitch the hitching post to the lock file. If link() + succeeds (the common case, we hope) all is well. Otherwise, fstat the + file, and get rid of the hitching post. If the number of links was 2, + the link was created, despite the failure of link(). If the hitch was + not successful, try again, having unlinked the lock file if it is too + old. + + There's a version of Linux (2.0.27) which doesn't update its local cache + of the inode after link() by default - which many think is a bug - but + if the link succeeds, this code will be OK. It just won't work in the + case when link() fails after having actually created the link. The Linux + NFS person is fixing this; a temporary patch is available if anyone is + sufficiently worried. */ + + if ((rc = Ulink(hitchname, lockname)) != 0) fstat(hd, &statbuf); + close(hd); + Uunlink(hitchname); + if (rc != 0 && statbuf.st_nlink != 2) + { + if (ob->lockfile_timeout > 0 && Ustat(lockname, &statbuf) == 0 && + time(NULL) - statbuf.st_ctime > ob->lockfile_timeout) + { + DEBUG(D_transport) debug_printf("unlinking timed-out lock file\n"); + Uunlink(lockname); + } + DEBUG(D_transport) debug_printf("link of hitching post failed - retrying\n"); + continue; + } + + DEBUG(D_transport) debug_printf("lock file created\n"); + break; + } + + /* Check for too many tries at creating the lock file */ + + if (i >= ob->lock_retries) + { + addr->basic_errno = ERRNO_LOCKFAILED; + addr->message = string_sprintf("failed to lock mailbox %s (lock file)", + filename); + return FALSE; + } + } + + + /* We now have to get the file open. First, stat() it and act on existence or + non-existence. This is in a loop to handle the case of a file's being created + or deleted as we watch, and also to handle retries when the locking fails. + Rather than holding the file open while waiting for the fcntl() and/or + flock() lock, we close and do the whole thing again. This should be safer, + especially for NFS files, which might get altered from other hosts, making + their cached sizes incorrect. + + With the default settings, no symlinks are permitted, but there is an option + to permit symlinks for those sysadmins that know what they are doing. + Shudder. However, insist that the initial symlink is owned by the right user. + Thus lstat() is used initially; if a symlink is discovered, the loop is + repeated such that stat() is used, to look at the end file. */ + + for (i = 0; i < ob->lock_retries; i++) + { + int sleep_before_retry = TRUE; + file_opened = FALSE; + + if((use_lstat? Ulstat(filename, &statbuf) : Ustat(filename, &statbuf)) != 0) + { + /* Let's hope that failure to stat (other than non-existence) is a + rare event. */ + + if (errno != ENOENT) + { + addr->basic_errno = errno; + addr->message = string_sprintf("attempting to stat mailbox %s", + filename); + goto RETURN; + } + + /* File does not exist. If it is required to pre-exist this state is an + error. */ + + if (ob->file_must_exist) + { + addr->basic_errno = errno; + addr->message = string_sprintf("mailbox %s does not exist, " + "but file_must_exist is set", filename); + goto RETURN; + } + + /* If not permitted to create this file because it isn't in or below + the home directory, generate an error. */ + + if (!allow_creation_here) + { + addr->basic_errno = ERRNO_BADCREATE; + addr->message = string_sprintf("mailbox %s does not exist, " + "but creation outside the home directory is not permitted", + filename); + goto RETURN; + } + + /* Attempt to create and open the file. If open fails because of + pre-existence, go round the loop again. For any other error, defer the + address, except for an alias or forward generated file name with EPERM, + ENOENT, or EACCES, as those are most likely to be user errors rather + than Exim config errors. When a symbolic link is permitted and points + to a non-existent file, we get here with use_lstat = FALSE. In this case + we mustn't use O_EXCL, since it doesn't work. The file is opened RDRW for + consistency and because MBX locking requires it in order to be able to + get a shared lock. */ + + fd = Uopen(filename, O_RDWR | O_APPEND | O_CREAT | + (use_lstat? O_EXCL : 0), mode); + if (fd < 0) + { + if (errno == EEXIST) continue; + addr->basic_errno = errno; + addr->message = string_sprintf("while creating mailbox %s", + filename); + if (testflag(addr, af_file) && + (errno == EPERM || errno == ENOENT || errno == EACCES)) + addr->transport_return = FAIL; + goto RETURN; + } + + /* We have successfully created and opened the file. Ensure that the group + and the mode are correct. */ + + Uchown(filename, uid, gid); + Uchmod(filename, mode); + } + + + /* The file already exists. Test its type, ownership, and permissions, and + save the inode for checking later. If symlinks are permitted (not the + default or recommended state) it may be a symlink that already exists. + Check its ownership and then look for the file at the end of the link(s). + This at least prevents one user creating a symlink for another user in + a sticky directory. */ + + else + { + int oldmode = (int)statbuf.st_mode; + ino_t inode = statbuf.st_ino; + BOOL islink = (oldmode & S_IFMT) == S_IFLNK; + + isfifo = FALSE; /* In case things are changing */ + + /* Check owner if required - the default. */ + + if (ob->check_owner && statbuf.st_uid != uid) + { + addr->basic_errno = ERRNO_BADUGID; + addr->message = string_sprintf("mailbox %s%s has wrong uid " + "(%ld != %ld)", filename, + islink? " (symlink)" : "", + (long int)(statbuf.st_uid), (long int)uid); + goto RETURN; + } + + /* Group is checked only if check_group is set. */ + + if (ob->check_group && statbuf.st_gid != gid) + { + addr->basic_errno = ERRNO_BADUGID; + addr->message = string_sprintf("mailbox %s%s has wrong gid (%d != %d)", + filename, islink? " (symlink)" : "", statbuf.st_gid, gid); + goto RETURN; + } + + /* If symlinks are permitted (not recommended), the lstat() above will + have found the symlink. Its ownership has just been checked; go round + the loop again, using stat() instead of lstat(). That will never yield a + mode of S_IFLNK. */ + + if (islink && ob->allow_symlink) + { + use_lstat = FALSE; + i--; /* Don't count this time round */ + continue; + } + + /* An actual file exists. Check that it is a regular file, or FIFO + if permitted. */ + + if (ob->allow_fifo && (oldmode & S_IFMT) == S_IFIFO) isfifo = TRUE; + + else if ((oldmode & S_IFMT) != S_IFREG) + { + addr->basic_errno = ERRNO_NOTREGULAR; + addr->message = string_sprintf("mailbox %s is not a regular file%s", + filename, ob->allow_fifo? " or named pipe" : ""); + goto RETURN; + } + + /* If the mode is not what it would be for a newly created file, change + the permissions if the mode is supplied for the address. Otherwise, + reduce but do not extend the permissions. If the newly created + permissions are greater than the existing permissions, don't change + things when the mode is not from the address. */ + + if ((oldmode = (oldmode & 07777)) != mode) + { + int diffs = oldmode ^ mode; + if (addr->mode > 0 || (diffs & oldmode) == diffs) + { + DEBUG(D_transport) debug_printf("chmod %o %s\n", mode, filename); + if (Uchmod(filename, mode) < 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("attempting to chmod mailbox %s", + filename); + goto RETURN; + } + oldmode = mode; + } + + /* Mode not from address, and newly-created permissions are greater + than existing permissions. Default is to complain, but it can be + configured to go ahead and try to deliver anyway if that's what + the administration wants. */ + + else if (ob->mode_fail_narrower) + { + addr->basic_errno = ERRNO_BADMODE; + addr->message = string_sprintf("mailbox %s has the wrong mode %o " + "(%o expected)", filename, oldmode, mode); + goto RETURN; + } + } + + /* We are happy with the existing file. Open it, and then do further + tests to ensure that it is the same file that we were just looking at. + If the file does not now exist, restart this loop, going back to using + lstat again. For an NFS error, just defer; other opening errors are + more serious. The file is opened RDWR so that its format can be checked, + and also MBX locking requires the use of a shared (read) lock. However, + a FIFO is opened WRONLY + NDELAY so that it fails if there is no process + reading the pipe. */ + + fd = Uopen(filename, isfifo? (O_WRONLY|O_NDELAY) : (O_RDWR|O_APPEND), + mode); + if (fd < 0) + { + if (errno == ENOENT) + { + use_lstat = TRUE; + continue; + } + addr->basic_errno = errno; + if (isfifo) + { + addr->message = string_sprintf("while opening named pipe %s " + "(could mean no process is reading it)", filename); + } + else if (errno != EWOULDBLOCK) + { + addr->message = string_sprintf("while opening mailbox %s", filename); + } + goto RETURN; + } + + /* This fstat really shouldn't fail, as we have an open file! There's a + dilemma here. We use fstat in order to be sure we are peering at the file + we have got open. However, that won't tell us if the file was reached + via a symbolic link. We checked this above, but there is a race exposure + if the link was created between the previous lstat and the open. However, + it would have to be created with the same inode in order to pass the + check below. If ob->allow_symlink is set, causing the use of stat rather + than lstat above, symbolic links may be there anyway, and the checking is + weaker. */ + + if (fstat(fd, &statbuf) < 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("attempting to stat open mailbox %s", + filename); + goto RETURN; + } + + /* Check the inode; this is isn't a perfect check, but gives some + confidence. */ + + if (inode != statbuf.st_ino) + { + addr->basic_errno = ERRNO_INODECHANGED; + addr->message = string_sprintf("opened mailbox %s inode number changed " + "from %d to %ld", filename, inode, statbuf.st_ino); + addr->special_action = SPECIAL_FREEZE; + goto RETURN; + } + + /* Check it's still a regular file or FIFO, and the uid, gid, and + permissions have not changed. */ + + if ((!isfifo && (statbuf.st_mode & S_IFMT) != S_IFREG) || + (isfifo && (statbuf.st_mode & S_IFMT) != S_IFIFO)) + { + addr->basic_errno = ERRNO_NOTREGULAR; + addr->message = + string_sprintf("opened mailbox %s is no longer a %s", filename, + isfifo? "named pipe" : "regular file"); + addr->special_action = SPECIAL_FREEZE; + goto RETURN; + } + + if ((ob->check_owner && statbuf.st_uid != uid) || + (ob->check_group && statbuf.st_gid != gid)) + { + addr->basic_errno = ERRNO_BADUGID; + addr->message = + string_sprintf("opened mailbox %s has wrong uid or gid", filename); + addr->special_action = SPECIAL_FREEZE; + goto RETURN; + } + + if ((statbuf.st_mode & 07777) != oldmode) + { + addr->basic_errno = ERRNO_BADMODE; + addr->message = string_sprintf("opened mailbox %s has wrong mode %o " + "(%o expected)", filename, statbuf.st_mode & 07777, mode); + addr->special_action = SPECIAL_FREEZE; + goto RETURN; + } + + /* If file_format is set, check that the format of the file has not + changed. Error data is set by the testing function. */ + + if (ob->file_format != NULL && + check_file_format(fd, tblock, addr) != tblock) + { + addr->message = US"open mailbox has changed format"; + goto RETURN; + } + + /* The file is OK. Carry on to do the locking. */ + } + + /* We now have an open file, and must lock it using fcntl(), flock() or MBX + locking rules if configured to do so. If a lock file is also required, it + was created above and hd was left >= 0. At least one form of locking is + required by the initialization function. If locking fails here, close the + file and go round the loop all over again, after waiting for a bit, unless + blocking locking was used. */ + + file_opened = TRUE; + if ((ob->lock_fcntl_timeout > 0) || (ob->lock_flock_timeout > 0)) + sleep_before_retry = FALSE; + + /* Simple fcntl() and/or flock() locking */ + + if (!ob->use_mbx_lock && (ob->use_fcntl || ob->use_flock)) + { + if (apply_lock(fd, F_WRLCK, ob->use_fcntl, ob->lock_fcntl_timeout, + ob->use_flock, ob->lock_flock_timeout) >= 0) break; + } + + /* MBX locking rules */ + + #ifdef SUPPORT_MBX + else if (ob->use_mbx_lock) + { + if (apply_lock(fd, F_RDLCK, ob->use_fcntl, ob->lock_fcntl_timeout, + ob->use_flock, ob->lock_flock_timeout) >= 0 && + fstat(fd, &statbuf) >= 0) + { + sprintf(CS mbx_lockname, "/tmp/.%lx.%lx", (long)statbuf.st_dev, + (long)statbuf.st_ino); + + if (Ulstat(mbx_lockname, &statbuf) >= 0) + { + if ((statbuf.st_mode & S_IFMT) == S_IFLNK) + { + addr->basic_errno = ERRNO_LOCKFAILED; + addr->message = string_sprintf("symbolic link on MBX lock file %s", + mbx_lockname); + goto RETURN; + } + if (statbuf.st_nlink > 1) + { + addr->basic_errno = ERRNO_LOCKFAILED; + addr->message = string_sprintf("hard link to MBX lock file %s", + mbx_lockname); + goto RETURN; + } + } + + mbx_lockfd = Uopen(mbx_lockname, O_RDWR | O_CREAT, 0600); + if (mbx_lockfd < 0) + { + addr->basic_errno = ERRNO_LOCKFAILED; + addr->message = string_sprintf("failed to open MBX lock file %s :%s", + mbx_lockname, strerror(errno)); + goto RETURN; + } + + Uchmod(mbx_lockname, 0600); + + if (apply_lock(mbx_lockfd, F_WRLCK, ob->use_fcntl, + ob->lock_fcntl_timeout, ob->use_flock, ob->lock_flock_timeout) >= 0) + { + struct stat ostatbuf; + + /* This tests for a specific race condition. Ensure that we still + have the same file. */ + + if (Ulstat(mbx_lockname, &statbuf) == 0 && + fstat(mbx_lockfd, &ostatbuf) == 0 && + statbuf.st_dev == ostatbuf.st_dev && + statbuf.st_ino == ostatbuf.st_ino) + break; + DEBUG(D_transport) debug_printf("MBX lockfile %s changed " + "between creation and locking\n", mbx_lockname); + } + + DEBUG(D_transport) debug_printf("failed to lock %s: %s\n", mbx_lockname, + strerror(errno)); + close(mbx_lockfd); + mbx_lockfd = -1; + } + else + { + DEBUG(D_transport) debug_printf("failed to fstat or get read lock on %s: %s\n", + filename, strerror(errno)); + } + } + #endif /* SUPPORT_MBX */ + + else break; /* No on-file locking required; break the open/lock loop */ + + DEBUG(D_transport) + debug_printf("fcntl(), flock(), or MBX locking failed - retrying\n"); + + close(fd); + fd = -1; + use_lstat = TRUE; /* Reset to use lstat first */ + + + /* If a blocking call timed out, break the retry loop if the total time + so far is not less than than retries * interval. Use the larger of the + flock() and fcntl() timeouts. */ + + if (sigalrm_seen && + (i+1) * ((ob->lock_fcntl_timeout > ob->lock_flock_timeout)? + ob->lock_fcntl_timeout : ob->lock_flock_timeout) >= + ob->lock_retries * ob->lock_interval) + i = ob->lock_retries; + + /* Wait a bit before retrying, except when it was a blocked fcntl() or + flock() that caused the problem. */ + + if (i < ob->lock_retries && sleep_before_retry) sleep(ob->lock_interval); + } + + /* Test for exceeding the maximum number of tries. Either the file remains + locked, or, if we haven't got it open, something is terribly wrong... */ + + if (i >= ob->lock_retries) + { + if (!file_opened) + { + addr->basic_errno = ERRNO_EXISTRACE; + addr->message = string_sprintf("mailbox %s: existence unclear", filename); + addr->special_action = SPECIAL_FREEZE; + } + else + { + addr->basic_errno = ERRNO_LOCKFAILED; + addr->message = string_sprintf("failed to lock mailbox %s (fcntl/flock)", + filename); + } + goto RETURN; + } + + DEBUG(D_transport) debug_printf("mailbox %s is locked\n", filename); + + /* Save access time (for subsequent restoration), modification time (for + restoration if updating fails), size of file (for comsat and for re-setting if + delivery fails in the middle - e.g. for quota exceeded). */ + + if (fstat(fd, &statbuf) < 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while fstatting opened mailbox %s", + filename); + goto RETURN; + } + + times.actime = statbuf.st_atime; + times.modtime = statbuf.st_mtime; + saved_size = statbuf.st_size; + if (mailbox_size < 0) mailbox_size = saved_size; + mailbox_filecount = 0; /* Not actually relevant for single-file mailbox */ + } + +/* Prepare for writing to a new file (as opposed to appending to an old one). +There are several different formats, but there is preliminary stuff concerned +with quotas that applies to all of them. Finding the current size by directory +scanning is expensive; for maildirs some fudges have been invented: + + (1) A regex can be used to extract a file size from its name; + (2) If maildir_use_size is set, a maildirsize file is used to cache the + mailbox size. +*/ + +else + { + uschar *check_path = path; /* Default quota check path */ + const pcre *regex = NULL; /* Regex for file size from file name */ + + if (!check_creation(string_sprintf("%s/any", path), ob->create_file)) + { + addr->basic_errno = ERRNO_BADCREATE; + addr->message = string_sprintf("tried to create file in %s, but " + "file creation outside the home directory is not permitted", path); + goto RETURN; + } + + #ifdef SUPPORT_MAILDIR + /* For a maildir delivery, ensure that all the relevant directories exist */ + + if (mbformat == mbf_maildir && !maildir_ensure_directories(path, addr, + ob->create_directory, ob->dirmode)) + return FALSE; + #endif /* SUPPORT_MAILDIR */ + + /* If we are going to do a quota check, of if maildir_use_size_file is set + for a maildir delivery, compile the regular expression if there is one. We + may also need to adjust the path that is used. We need to do this for + maildir_use_size_file even if the quota is unset, because we still want to + create the file. When maildir support is not compiled, + ob->maildir_use_size_file is always FALSE. */ + + if (ob->quota_value > 0 || THRESHOLD_CHECK || ob->maildir_use_size_file) + { + const uschar *error; + int offset; + + /* Compile the regex if there is one */ + + if (ob->quota_size_regex != NULL) + { + regex = pcre_compile(CS ob->quota_size_regex, PCRE_COPT, + (const char **)&error, &offset, NULL); + if (regex == NULL) + { + addr->message = string_sprintf("appendfile: regular expression " + "error: %s at offset %d while compiling %s", error, offset, + ob->quota_size_regex); + return FALSE; + } + else + { + DEBUG(D_transport) debug_printf("using regex for file sizes: %s\n", + ob->quota_size_regex); + } + } + + /* Use an explicitly configured directory if set */ + + if (ob->quota_directory != NULL) + { + check_path = expand_string(ob->quota_directory); + if (check_path == NULL) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (quota_directory " + "name for %s transport) failed: %s", ob->quota_directory, + tblock->name, expand_string_message); + return FALSE; + } + + if (check_path[0] != '/') + { + addr->message = string_sprintf("appendfile: quota_directory name " + "\"%s\" is not absolute", check_path); + addr->basic_errno = ERRNO_NOTABSOLUTE; + return FALSE; + } + } + + #ifdef SUPPORT_MAILDIR + /* Otherwise, if we are handling a maildir delivery, and the directory + contains a file called maildirfolder, this is a maildir++ feature telling + us that this is a sub-directory of the real inbox. We should therefore do + the quota check on the parent directory. Beware of the special case when + the directory name itself ends in a slash. */ + + else if (mbformat == mbf_maildir) + { + struct stat statbuf; + if (Ustat(string_sprintf("%s/maildirfolder", path), &statbuf) >= 0) + { + uschar *new_check_path = string_copy(check_path); + uschar *slash = Ustrrchr(new_check_path, '/'); + if (slash != NULL) + { + if (slash[1] == 0) + { + *slash = 0; + slash = Ustrrchr(new_check_path, '/'); + } + if (slash != NULL) + { + *slash = 0; + check_path = new_check_path; + } + } + } + } + #endif /* SUPPORT_MAILDIR */ + } + + /* If we are using maildirsize files, we need to ensure that such a file + exists and, if necessary, recalculate its contents. As a byproduct of this, + we obtain the current size of the maildir. If no quota is to be enforced + (ob->quota_value == 0), we still need the size if a threshold check will + happen later. + + Another regular expression is used to determine which directories inside the + maildir are going to be counted. */ + + #ifdef SUPPORT_MAILDIR + if (ob->maildir_use_size_file) + { + const pcre *dir_regex = NULL; + const uschar *error; + int offset; + + if (ob->maildir_dir_regex != NULL) + { + dir_regex = pcre_compile(CS ob->maildir_dir_regex, PCRE_COPT, + (const char **)&error, &offset, NULL); + if (dir_regex == NULL) + { + addr->message = string_sprintf("appendfile: regular expression " + "error: %s at offset %d while compiling %s", error, offset, + ob->maildir_dir_regex); + return FALSE; + } + else + { + DEBUG(D_transport) + debug_printf("using regex for maildir directory selection: %s\n", + ob->maildir_dir_regex); + } + } + + /* Quota enforcement; create and check the file. There is some discussion + about whether this should happen if the quota is unset. At present, Exim + always creates the file. If we ever want to change this, uncomment + appropriate lines below, possibly doing a check on some option. */ + +/* if (???? || ob->quota_value > 0) */ + + { + int size, filecount; + + maildirsize_fd = maildir_ensure_sizefile(check_path, ob, regex, dir_regex, + &size, &filecount); + + if (maildirsize_fd == -1) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while opening or reading " + "%s/maildirsize", check_path); + return FALSE; + } + + if (mailbox_size < 0) mailbox_size = size; + if (mailbox_filecount < 0) mailbox_filecount = filecount; + } + + /* No quota enforcement; ensure file does *not* exist; calculate size if + needed. */ + +/* else + * { + * time_t old_latest; + * (void)unlink(CS string_sprintf("%s/maildirsize", check_path)); + * if (THRESHOLD_CHECK) + * mailbox_size = maildir_compute_size(check_path, &mailbox_filecount, &old_latest, + * regex, dir_regex, FALSE); + * } +*/ + + } + #endif /* SUPPORT_MAILDIR */ + + /* Otherwise (mailbox_size is not yet set), if we are going to do a quota + check later on, find the current size of the mailbox. (We don't need to check + ob->quota_filecount_value, because it can only be set if ob->quota_value is + set.) */ + + if ((mailbox_size < 0 || mailbox_filecount < 0) && + (ob->quota_value > 0 || THRESHOLD_CHECK)) + { + int size, filecount; + DEBUG(D_transport) + debug_printf("quota checks on directory %s\n", check_path); + size = check_dir_size(check_path, &filecount, regex); + if (mailbox_size < 0) mailbox_size = size; + if (mailbox_filecount < 0) mailbox_filecount = filecount; + } + + /* Handle the case of creating a unique file in a given directory (not in + maildir or mailstore format - this is how smail did it). A temporary name is + used to create the file. Later, when it is written, the name is changed to a + unique one. There is no need to lock the file. An attempt is made to create + the directory if it does not exist. */ + + if (mbformat == mbf_smail) + { + DEBUG(D_transport) + debug_printf("delivering to new file in %s\n", path); + filename = dataname = + string_sprintf("%s/temp.%d.%s", path, (int)getpid(), primary_hostname); + fd = Uopen(filename, O_WRONLY|O_CREAT, mode); + if (fd < 0 && /* failed to open, and */ + (errno != ENOENT || /* either not non-exist */ + !ob->create_directory || /* or not allowed to make */ + !directory_make(NULL, path, ob->dirmode, FALSE) || /* or failed to create dir */ + (fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0)) /* or then failed to open */ + { + addr->basic_errno = errno; + addr->message = string_sprintf("while creating file %s", filename); + return FALSE; + } + } + + #ifdef SUPPORT_MAILDIR + + /* Handle the case of a unique file in maildir format. The file is written to + the tmp subdirectory, with a prescribed form of name. */ + + else if (mbformat == mbf_maildir) + { + DEBUG(D_transport) + debug_printf("delivering in maildir format in %s\n", path); + + nametag = ob->maildir_tag; + + /* Check that nametag expands successfully; a hard failure causes a panic + return. The actual expansion for use happens again later, when + $message_size is accurately known. */ + + if (nametag != NULL && expand_string(nametag) == NULL && + !expand_string_forcedfail) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (maildir_tag " + "for %s transport) failed: %s", nametag, tblock->name, + expand_string_message); + return FALSE; + } + + /* We ensured the existence of all the relevant directories above. Attempt + to open the temporary file a limited number of times. I think this rather + scary-looking for statement is actually OK. If open succeeds, the loop is + broken; if not, there is a test on the value of i. Get the time again + afresh each time round the loop. Its value goes into a variable that is + checked at the end, to make sure we don't release this process until the + clock has ticked. */ + + for (i = 1;; i++) + { + uschar *basename; + + (void)gettimeofday(&msg_tv, NULL); + basename = string_sprintf("%lu.H%luP%lu.%s", msg_tv.tv_sec, + msg_tv.tv_usec, getpid(), primary_hostname); + + filename = dataname = string_sprintf("tmp/%s", basename); + newname = string_sprintf("new/%s", basename); + + if (Ustat(filename, &statbuf) == 0) + errno = EEXIST; + else if (errno == ENOENT) + { + fd = Uopen(filename, O_WRONLY | O_CREAT | O_EXCL, mode); + if (fd >= 0) break; + DEBUG (D_transport) debug_printf ("open failed for %s: %s\n", + filename, strerror(errno)); + } + + /* Too many retries - give up */ + + if (i >= ob->maildir_retries) + { + addr->message = string_sprintf ("failed to open %s (%d tr%s)", + filename, i, (i == 1)? "y" : "ies"); + addr->basic_errno = errno; + return FALSE; + } + + /* Open or stat failed but we haven't tried too many times yet. */ + + sleep(2); + } + + /* Note that we have to ensure the clock has ticked before leaving */ + + wait_for_tick = TRUE; + + /* Why are these here? Put in because they are present in the non-maildir + directory case above. */ + + Uchown(filename, uid, gid); + Uchmod(filename, mode); + } + + #endif /* SUPPORT_MAILDIR */ + + #ifdef SUPPORT_MAILSTORE + + /* Handle the case of a unique file in mailstore format. First write the + envelope to a temporary file, then open the main file. The unique base name + for the files consists of the message id plus the pid of this delivery + process. */ + + else + { + FILE *env_file; + address_item *taddr; + mailstore_basename = string_sprintf("%s/%s-%s", path, message_id, + string_base62((long int)getpid())); + + DEBUG(D_transport) + debug_printf("delivering in mailstore format in %s\n", path); + + filename = string_sprintf("%s.tmp", mailstore_basename); + newname = string_sprintf("%s.env", mailstore_basename); + dataname = string_sprintf("%s.msg", mailstore_basename); + + fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode); + if (fd < 0 && /* failed to open, and */ + (errno != ENOENT || /* either not non-exist */ + !ob->create_directory || /* or not allowed to make */ + !directory_make(NULL, path, ob->dirmode, FALSE) || /* or failed to create dir */ + (fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0)) /* or then failed to open */ + { + addr->basic_errno = errno; + addr->message = string_sprintf("while creating file %s", filename); + return FALSE; + } + + /* Why are these here? Put in because they are present in the non-maildir + directory case above. */ + + Uchown(filename, uid, gid); + Uchmod(filename, mode); + + /* Built a C stream from the open file descriptor. */ + + if ((env_file = fdopen(fd, "wb")) == NULL) + { + addr->basic_errno = errno; + addr->transport_return = PANIC; + addr->message = string_sprintf("fdopen of %s (" + "for %s transport) failed", filename, tblock->name); + close(fd); + Uunlink(filename); + return FALSE; + } + + /* Write the envelope file, then close it. */ + + if (ob->mailstore_prefix != NULL) + { + uschar *s = expand_string(ob->mailstore_prefix); + if (s == NULL) + { + if (!expand_string_forcedfail) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (mailstore " + "prefix for %s transport) failed: %s", ob->mailstore_prefix, + tblock->name, expand_string_message); + fclose(env_file); + Uunlink(filename); + return FALSE; + } + } + else + { + int n = Ustrlen(s); + fprintf(env_file, "%s", CS s); + if (n == 0 || s[n-1] != '\n') fprintf(env_file, "\n"); + } + } + + fprintf(env_file, "%s\n", sender_address); + + for (taddr = addr; taddr!= NULL; taddr = taddr->next) + fprintf(env_file, "%s@%s\n", taddr->local_part, taddr->domain); + + if (ob->mailstore_suffix != NULL) + { + uschar *s = expand_string(ob->mailstore_suffix); + if (s == NULL) + { + if (!expand_string_forcedfail) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (mailstore " + "suffix for %s transport) failed: %s", ob->mailstore_suffix, + tblock->name, expand_string_message); + fclose(env_file); + Uunlink(filename); + return FALSE; + } + } + else + { + int n = Ustrlen(s); + fprintf(env_file, "%s", CS s); + if (n == 0 || s[n-1] != '\n') fprintf(env_file, "\n"); + } + } + + if (fclose(env_file) != 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while closing %s", filename); + Uunlink(filename); + return FALSE; + } + + DEBUG(D_transport) debug_printf("Envelope file %s written\n", filename); + + /* Now open the data file, and ensure that it has the correct ownership and + mode. */ + + fd = Uopen(dataname, O_WRONLY|O_CREAT|O_EXCL, mode); + if (fd < 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while creating file %s", dataname); + Uunlink(filename); + return FALSE; + } + Uchown(dataname, uid, gid); + Uchmod(dataname, mode); + } + + #endif /* SUPPORT_MAILSTORE */ + + + /* In all cases of writing to a new file, ensure that the file which is + going to be renamed has the correct ownership and mode. */ + + Uchown(filename, uid, gid); + Uchmod(filename, mode); + } + + +/* At last we can write the message to the file, preceded by any configured +prefix line, and followed by any configured suffix line. If there are any +writing errors, we must defer. */ + +DEBUG(D_transport) debug_printf("writing to file %s\n", dataname); + +yield = OK; +errno = 0; + +/* If there is a local quota setting, check that we are not going to exceed it +with this message if quota_is_inclusive is set; if it is not set, the check +is for the mailbox already being over quota (i.e. the current message is not +included in the check). */ + +if (ob->quota_value > 0) + { + DEBUG(D_transport) + { + debug_printf("Exim quota = %d old size = %d this message = %d " + "(%sincluded)\n", ob->quota_value, mailbox_size, message_size, + ob->quota_is_inclusive? "" : "not "); + debug_printf(" file count quota = %d count = %d\n", + ob->quota_filecount_value, mailbox_filecount); + } + if (mailbox_size + (ob->quota_is_inclusive? message_size:0) > ob->quota_value) + { + DEBUG(D_transport) debug_printf("mailbox quota exceeded\n"); + yield = DEFER; + errno = ERRNO_EXIMQUOTA; + } + else if (ob->quota_filecount_value > 0 && + mailbox_filecount + (ob->quota_is_inclusive ? 1:0) > + ob->quota_filecount_value) + { + DEBUG(D_transport) debug_printf("mailbox file count quota exceeded\n"); + yield = DEFER; + errno = ERRNO_EXIMQUOTA; + filecount_msg = US" filecount"; + } + } + +/* If we are writing in MBX format, what we actually do is to write the message +to a temporary file, and then copy it to the real file once we know its size. +This is the most straightforward way of getting the correct length in the +separator line. So, what we do here is to save the real file descriptor, and +replace it with one for a temporary file. The temporary file gets unlinked once +opened, so that it goes away on closure. */ + +#ifdef SUPPORT_MBX +if (yield == OK && ob->mbx_format) + { + temp_file = tmpfile(); + if (temp_file == NULL) + { + addr->basic_errno = errno; + addr->message = US"while setting up temporary file"; + yield = DEFER; + goto RETURN; + } + save_fd = fd; + fd = fileno(temp_file); + DEBUG(D_transport) debug_printf("writing to temporary file\n"); + } +#endif /* SUPPORT_MBX */ + +/* Zero the count of bytes written. It is incremented by the transport_xxx() +functions. */ + +transport_count = 0; + +/* Write any configured prefix text first */ + +if (yield == OK && ob->message_prefix != NULL && ob->message_prefix[0] != 0) + { + uschar *prefix = expand_string(ob->message_prefix); + if (prefix == NULL) + { + errno = ERRNO_EXPANDFAIL; + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (prefix for %s " + "transport) failed", ob->message_prefix, tblock->name); + yield = DEFER; + } + else if (!transport_write_string(fd, "%s", prefix)) yield = DEFER; + } + +/* If the use_bsmtp option is on, we need to write SMTP prefix information. The +various different values for batching are handled outside; if there is more +than one address available here, all must be included. If any address is a +file, use its parent in the RCPT TO. */ + +if (yield == OK && ob->use_bsmtp) + { + transport_count = 0; + if (ob->use_crlf) cr = US"\r"; + if (!transport_write_string(fd, "MAIL FROM:<%s>%s\n", return_path, cr)) + yield = DEFER; + else + { + address_item *a; + for (a = addr; a != NULL; a = a->next) + { + address_item *b = testflag(a, af_pfr)? a->parent: a; + if (!transport_write_string(fd, "RCPT TO:<%s>%s\n", + transport_rcpt_address(b, tblock->rcpt_include_affixes), cr)) + { yield = DEFER; break; } + } + if (yield == OK && !transport_write_string(fd, "DATA%s\n", cr)) + yield = DEFER; + } + } + +/* Now the message itself. The options for transport_write_message were set up +at initialization time. */ + +if (yield == OK) + { + if (!transport_write_message(addr, fd, ob->options, 0, tblock->add_headers, + tblock->remove_headers, ob->check_string, ob->escape_string, + tblock->rewrite_rules, tblock->rewrite_existflags)) + yield = DEFER; + } + +/* Now a configured suffix. */ + +if (yield == OK && ob->message_suffix != NULL && ob->message_suffix[0] != 0) + { + uschar *suffix = expand_string(ob->message_suffix); + if (suffix == NULL) + { + errno = ERRNO_EXPANDFAIL; + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (suffix for %s " + "transport) failed", ob->message_suffix, tblock->name); + yield = DEFER; + } + else if (!transport_write_string(fd, "%s", suffix)) yield = DEFER; + } + +/* If batch smtp, write the terminating dot. */ + +if (yield == OK && ob->use_bsmtp && + !transport_write_string(fd, ".%s\n", cr)) yield = DEFER; + +/* If MBX format is being used, all that writing was to the temporary file. +However, if there was an earlier failure (Exim quota exceeded, for example), +the temporary file won't have got opened - and no writing will have been done. +If writing was OK, we restore the fd, and call a function that copies the +message in MBX format into the real file. Otherwise use the temporary name in +any messages. */ + +#ifdef SUPPORT_MBX +if (temp_file != NULL && ob->mbx_format) + { + int mbx_save_errno; + fd = save_fd; + + if (yield == OK) + { + transport_count = 0; /* Reset transport count for actual write */ + yield = copy_mbx_message(fd, fileno(temp_file), saved_size); + } + else if (errno >= 0) dataname = US"temporary file"; + + /* Preserve errno while closing the temporary file. */ + + mbx_save_errno = errno; + fclose(temp_file); + errno = mbx_save_errno; + } +#endif /* SUPPORT_MBX */ + +/* Force out the remaining data to check for any errors; some OS don't allow +fsync() to be called for a FIFO. */ + +if (yield == OK && !isfifo && fsync(fd) < 0) yield = DEFER; + +/* Update message_size to the accurate count of bytes written, including +added headers. */ + +message_size = transport_count; + +/* If using a maildir++ quota file, add this message's size to it, and +close the file descriptor. */ + +#ifdef SUPPORT_MAILDIR +if (yield == OK && maildirsize_fd >= 0) + maildir_record_length(maildirsize_fd, message_size); + +maildir_save_errno = errno; /* Preserve errno while closing the file */ +close(maildirsize_fd); +errno = maildir_save_errno; +#endif /* SUPPORT_MAILDIR */ + +/* If there is a quota warning threshold and we are have crossed it with this +message, set the SPECIAL_WARN flag in the address, to cause a warning message +to be sent. */ + +if (THRESHOLD_CHECK) + { + int threshold = ob->quota_warn_threshold_value; + if (ob->quota_warn_threshold_is_percent) + threshold = (int)(((double)ob->quota_value * threshold) / 100); + DEBUG(D_transport) + debug_printf("quota = %d threshold = %d old size = %d message size = %d\n", + ob->quota_value, threshold, mailbox_size, message_size); + if (mailbox_size <= threshold && mailbox_size + message_size > threshold) + addr->special_action = SPECIAL_WARN; + + /******* You might think that the test ought to be this: + * + * if (ob->quota_value > 0 && threshold > 0 && mailbox_size > 0 && + * mailbox_size <= threshold && mailbox_size + message_size > threshold) + * + * (indeed, I was sent a patch with that in). However, it is possible to + * have a warning threshold without actually imposing a quota, and I have + * therefore kept Exim backwards compatible. + ********/ + + } + +/* Handle error while writing the file. Control should come here directly after +the error, with the reason in errno. In the case of expansion failure in prefix +or suffix, it will be ERRNO_EXPANDFAIL. */ + +if (yield != OK) + { + addr->special_action = SPECIAL_NONE; /* Cancel any quota warning */ + + /* Save the error number. If positive, it will ultimately cause a strerror() + call to generate some text. */ + + addr->basic_errno = errno; + + /* For system or Exim quota excession, or disk full, set more_errno to the + time since the file was last read. If delivery was into a directory, the + time since last read logic is not relevant, in general. However, for maildir + deliveries we can approximate it by looking at the last modified time of the + "new" subdirectory. Since Exim won't be adding new messages, a change to the + "new" subdirectory implies that an MUA has moved a message from there to the + "cur" directory. */ + + if (errno == errno_quota || errno == ERRNO_EXIMQUOTA || errno == ENOSPC) + { + addr->more_errno = 0; + if (!isdirectory) addr->more_errno = (int)(time(NULL) - times.actime); + + #ifdef SUPPORT_MAILDIR + else if (mbformat == mbf_maildir) + { + struct stat statbuf; + if (Ustat("new", &statbuf) < 0) + { + DEBUG(D_transport) debug_printf("maildir quota exceeded: " + "stat error %d for \"new\": %s\n", errno, strerror(errno)); + } + else /* Want a repeatable time when in test harness */ + { + addr->more_errno = running_in_test_harness? 10 : + (int)time(NULL) - statbuf.st_mtime; + } + DEBUG(D_transport) + debug_printf("maildir: time since \"new\" directory modified = %s\n", + readconf_printtime(addr->more_errno)); + } + #endif /* SUPPORT_MAILDIR */ + } + + /* Handle system quota excession. Add an explanatory phrase for the error + message, since some systems don't have special quota-excession errors, + and on those that do, "quota" doesn't always mean anything to the user. */ + + if (errno == errno_quota) + { + #ifndef EDQUOT + addr->message = string_sprintf("mailbox is full " + "(quota exceeded while writing to file %s)", filename); + #else + addr->message = string_sprintf("mailbox is full"); + #endif /* EDQUOT */ + DEBUG(D_transport) debug_printf("System quota exceeded for %s%s%s\n", + dataname, + isdirectory? US"" : US": time since file read = ", + isdirectory? US"" : readconf_printtime(addr->more_errno)); + } + + /* Handle Exim's own quota-imposition */ + + else if (errno == ERRNO_EXIMQUOTA) + { + addr->message = string_sprintf("mailbox is full " + "(MTA-imposed%s quota exceeded while writing to %s)", filecount_msg, + dataname); + addr->user_message = US"mailbox is full"; + DEBUG(D_transport) debug_printf("Exim%s quota exceeded for %s%s%s\n", + filecount_msg, dataname, + isdirectory? US"" : US": time since file read = ", + isdirectory? US"" : readconf_printtime(addr->more_errno)); + } + + /* Handle a process failure while writing via a filter; the return + from child_close() is in more_errno. */ + + else if (errno == ERRNO_FILTER_FAIL) + addr->message = string_sprintf("filter process failure %d while writing " + "to %s", addr->more_errno, dataname); + + /* Handle failure to expand header changes */ + + else if (errno == ERRNO_CHHEADER_FAIL) + { + yield = PANIC; + addr->message = + string_sprintf("failed to expand headers_add or headers_remove while " + "writing to %s: %s", dataname, expand_string_message); + } + + /* Handle failure to complete writing of a data block */ + + else if (errno == ERRNO_WRITEINCOMPLETE) + { + addr->message = string_sprintf("failed to write data block while " + "writing to %s", dataname); + } + + /* Handle length mismatch on MBX copying */ + + #ifdef SUPPORT_MBX + else if (errno == ERRNO_MBXLENGTH) + { + addr->message = string_sprintf("length mismatch while copying MBX " + "temporary file to %s", dataname); + } + #endif /* SUPPORT_MBX */ + + /* For other errors, a general-purpose explanation, if the message is + not already set. */ + + else if (addr->message == NULL) + addr->message = string_sprintf("error while writing to %s", dataname); + + /* For a file, reset the file size to what it was before we started, leaving + the last modification time unchanged, so it will get reset also. All systems + investigated so far have ftruncate(), whereas not all have the F_FREESP + fcntl() call (BSDI & FreeBSD do not). */ + + if (!isdirectory) ftruncate(fd, saved_size); + } + +/* Handle successful writing - we want the modification time to be now for +appended files. Remove the default backstop error number. For a directory, now +is the time to rename the file with a unique name. As soon as such a name +appears it may get used by another process, so we close the file first and +check that all is well. */ + +else + { + times.modtime = time(NULL); + addr->basic_errno = 0; + + /* Handle the case of writing to a new file in a directory. This applies + to all single-file formats - maildir, mailstore, and "smail format". */ + + if (isdirectory) + { + if (fstat(fd, &statbuf) < 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while fstatting opened message file %s", + filename); + yield = DEFER; + } + + else if (close(fd) < 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("close() error for %s", + (ob->mailstore_format)? dataname : filename); + yield = DEFER; + } + + /* File is successfully written and closed. Arrange to rename it. For the + different kinds of single-file delivery, some games can be played with the + name. The message size is by this time set to the accurate value so that + its value can be used in expansions. */ + + else + { + uschar *renamename = newname; + fd = -1; + + DEBUG(D_transport) debug_printf("renaming temporary file\n"); + + /* If there is no rename name set, we are in a non-maildir, non-mailstore + situation. The name is built by expanding the directory_file option, and + we make the inode number available for use in this. The expansion was + checked for syntactic validity above, before we wrote the file. + + We have to be careful here, in case the file name exists. (In the other + cases, the names used are constructed to be unique.) The rename() + function just replaces an existing file - we don't want that! So instead + of calling rename(), we must use link() and unlink(). + + In this case, if the link fails because of an existing file, we wait + for one second and try the expansion again, to see if it produces a + different value. Do this up to 5 times unless the name stops changing. + This makes it possible to build values that are based on the time, and + still cope with races from multiple simultaneous deliveries. */ + + if (newname == NULL) + { + int i; + uschar *renameleaf; + uschar *old_renameleaf = US""; + + for (i = 0; ; sleep(1), i++) + { + deliver_inode = statbuf.st_ino; + renameleaf = expand_string(ob->dirfilename); + deliver_inode = 0; + + if (renameleaf == NULL) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Expansion of \"%s\" " + "(directory_file for %s transport) failed: %s", + ob->dirfilename, tblock->name, expand_string_message); + goto RETURN; + } + + renamename = string_sprintf("%s/%s", path, renameleaf); + if (Ulink(filename, renamename) < 0) + { + DEBUG(D_transport) debug_printf("link failed: %s\n", + strerror(errno)); + if (errno != EEXIST || i >= 4 || + Ustrcmp(renameleaf, old_renameleaf) == 0) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while renaming %s as %s", + filename, renamename); + yield = DEFER; + break; + } + old_renameleaf = renameleaf; + DEBUG(D_transport) debug_printf("%s exists - trying again\n", + renamename); + } + else + { + Uunlink(filename); + filename = NULL; + break; + } + } /* re-expand loop */ + } /* not mailstore or maildir */ + + /* For maildir and mailstore formats, the new name was created earlier, + except that for maildir, there is the possibility of adding a "tag" on + the end of the name by expanding the value of nametag. This usually + includes a reference to the message size. The expansion of nametag was + checked above, before the file was opened. It either succeeded, or + provoked a soft failure. So any failure here can be treated as soft. + Ignore non-printing characters and / and put a colon at the start if the + first character is alphanumeric. */ + + else + { + if (nametag != NULL) + { + uschar *iptr = expand_string(nametag); + if (iptr != NULL) + { + uschar *etag = store_get(Ustrlen(iptr) + 2); + uschar *optr = etag; + while (*iptr != 0) + { + if (mac_isgraph(*iptr) && *iptr != '/') + { + if (optr == etag && isalnum(*iptr)) *optr++ = ':'; + *optr++ = *iptr; + } + iptr++; + } + *optr = 0; + renamename = string_sprintf("%s%s", newname, etag); + } + } + + /* Do the rename. If the name is too long and a tag exists, try again + without the tag. */ + + if (Urename(filename, renamename) < 0 && + (nametag == NULL || errno != ENAMETOOLONG || + (renamename = newname, Urename(filename, renamename) < 0))) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while renaming %s as %s", + filename, renamename); + yield = DEFER; + } + + /* Rename succeeded */ + + else + { + DEBUG(D_transport) debug_printf("renamed %s as %s\n", filename, + renamename); + filename = dataname = NULL; /* Prevents attempt to unlink at end */ + } + } /* maildir or mailstore */ + } /* successful write + close */ + } /* isdirectory */ + } /* write success */ + + +/* For a file, restore the last access time (atime), and set the modification +time as required - changed if write succeeded, unchanged if not. */ + +if (!isdirectory) utime(CS filename, ×); + +/* Notify comsat if configured to do so. It only makes sense if the configured +file is the one that the comsat daemon knows about. */ + +if (ob->notify_comsat && yield == OK && deliver_localpart != NULL) + notify_comsat(deliver_localpart, saved_size); + +/* Pass back the final return code in the address structure */ + +DEBUG(D_transport) + debug_printf("appendfile yields %d with errno=%d more_errno=%d\n", + yield, addr->basic_errno, addr->more_errno); + +addr->transport_return = yield; + +/* Close the file, which will release the fcntl lock. For a directory write it +is closed above, except in cases of error which goto RETURN, when we also need +to remove the original file(s). For MBX locking, if all has gone well, before +closing the file, see if we can get an exclusive lock on it, in which case we +can unlink the /tmp lock file before closing it. This is always a non-blocking +lock; there's no need to wait if we can't get it. If everything has gone right +but close fails, defer the message. Then unlink the lock file, if present. This +point in the code is jumped to from a number of places when errors are +detected, in order to get the file closed and the lock file tidied away. */ + +RETURN: + +#ifdef SUPPORT_MBX +if (mbx_lockfd >= 0) + { + if (yield == OK && apply_lock(fd, F_WRLCK, ob->use_fcntl, 0, + ob->use_flock, 0) >= 0) + { + DEBUG(D_transport) + debug_printf("unlinking MBX lock file %s\n", mbx_lockname); + Uunlink(mbx_lockname); + } + close(mbx_lockfd); + } +#endif /* SUPPORT_MBX */ + +if (fd >= 0 && close(fd) < 0 && yield == OK) + { + addr->basic_errno = errno; + addr->message = string_sprintf("while closing %s", filename); + addr->transport_return = DEFER; + } + +if (hd >= 0) Uunlink(lockname); + +/* We get here with isdirectory and filename set only in error situations. */ + +if (isdirectory && filename != NULL) + { + Uunlink(filename); + if (dataname != filename) Uunlink(dataname); + } + +/* If wait_for_tick is TRUE, we have done a delivery where the uniqueness of a +file name relies on time + pid. We must not allow the process to finish until +the clock has move on by at least one microsecond. Usually we expect this +already to be the case, but machines keep getting faster... */ + +if (wait_for_tick) exim_wait_tick(&msg_tv, 1); + +/* A return of FALSE means that if there was an error, a common error was +put in the first address of a batch. */ + +return FALSE; +} + +/* End of transport/appendfile.c */ diff --git a/src/src/transports/appendfile.h b/src/src/transports/appendfile.h new file mode 100644 index 000000000..036584080 --- /dev/null +++ b/src/src/transports/appendfile.h @@ -0,0 +1,97 @@ +/* $Cambridge: exim/src/src/transports/appendfile.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *filename; + uschar *dirname; + uschar *dirfilename; + uschar *message_prefix; + uschar *message_suffix; + uschar *create_file_string; + uschar *quota; + uschar *quota_directory; + uschar *quota_filecount; + uschar *quota_size_regex; + uschar *quota_warn_threshold; + uschar *mailbox_size_string; + uschar *mailbox_filecount_string; + uschar *maildir_dir_regex; + uschar *maildir_tag; + uschar *mailstore_prefix; + uschar *mailstore_suffix; + uschar *check_string; + uschar *escape_string; + uschar *file_format; + int mailbox_size_value; + int mailbox_filecount_value; + int quota_value; + int quota_filecount_value; + int quota_warn_threshold_value; + int mode; + int dirmode; + int lockfile_mode; + int lockfile_timeout; + int lock_fcntl_timeout; + int lock_flock_timeout; + int lock_retries; + int lock_interval; + int maildir_retries; + int create_file; + int options; + BOOL allow_fifo; + BOOL allow_symlink; + BOOL check_group; + BOOL check_owner; + BOOL create_directory; + BOOL notify_comsat; + BOOL use_lockfile; + BOOL set_use_lockfile; + BOOL use_fcntl; + BOOL set_use_fcntl; + BOOL use_flock; + BOOL set_use_flock; + BOOL use_mbx_lock; + BOOL set_use_mbx_lock; + BOOL use_bsmtp; + BOOL use_crlf; + BOOL file_must_exist; + BOOL mode_fail_narrower; + BOOL maildir_format; + BOOL maildir_use_size_file; + BOOL mailstore_format; + BOOL mbx_format; + BOOL quota_warn_threshold_is_percent; + BOOL quota_is_inclusive; +} appendfile_transport_options_block; + +/* Restricted creation options */ + +enum { create_anywhere, create_belowhome, create_inhome }; + +/* Data for reading the private options. */ + +extern optionlist appendfile_transport_options[]; +extern int appendfile_transport_options_count; + +/* Block containing default values. */ + +extern appendfile_transport_options_block appendfile_transport_option_defaults; + +/* The main and init entry points for the transport */ + +extern BOOL appendfile_transport_entry(transport_instance *, address_item *); +extern void appendfile_transport_init(transport_instance *); + +/* Function that is shared with tf_maildir.c */ + +extern int check_dir_size(uschar *, int *, const pcre *); + +/* End of transports/appendfile.h */ diff --git a/src/src/transports/autoreply.c b/src/src/transports/autoreply.c new file mode 100644 index 000000000..341b7b0c0 --- /dev/null +++ b/src/src/transports/autoreply.c @@ -0,0 +1,817 @@ +/* $Cambridge: exim/src/src/transports/autoreply.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "autoreply.h" + + + +/* Options specific to the autoreply transport. They must be in alphabetic +order (note that "_" comes before the lower case letters). Those starting +with "*" are not settable by the user but are used by the option-reading +software for alternative value types. Some options are publicly visible and so +are stored in the driver instance block. These are flagged with opt_public. */ + +optionlist autoreply_transport_options[] = { + { "bcc", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, bcc) }, + { "cc", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, cc) }, + { "file", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, file) }, + { "file_expand", opt_bool, + (void *)offsetof(autoreply_transport_options_block, file_expand) }, + { "file_optional", opt_bool, + (void *)offsetof(autoreply_transport_options_block, file_optional) }, + { "from", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, from) }, + { "headers", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, headers) }, + { "log", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, logfile) }, + { "mode", opt_octint, + (void *)offsetof(autoreply_transport_options_block, mode) }, + { "never_mail", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, never_mail) }, + { "once", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, oncelog) }, + { "once_file_size", opt_int, + (void *)offsetof(autoreply_transport_options_block, once_file_size) }, + { "once_repeat", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, once_repeat) }, + { "reply_to", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, reply_to) }, + { "return_message", opt_bool, + (void *)offsetof(autoreply_transport_options_block, return_message) }, + { "subject", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, subject) }, + { "text", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, text) }, + { "to", opt_stringptr, + (void *)offsetof(autoreply_transport_options_block, to) }, +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int autoreply_transport_options_count = + sizeof(autoreply_transport_options)/sizeof(optionlist); + +/* Default private options block for the autoreply transport. */ + +autoreply_transport_options_block autoreply_transport_option_defaults = { + NULL, /* from */ + NULL, /* reply_to */ + NULL, /* to */ + NULL, /* cc */ + NULL, /* bcc */ + NULL, /* subject */ + NULL, /* headers */ + NULL, /* text */ + NULL, /* file */ + NULL, /* logfile */ + NULL, /* oncelog */ + NULL, /* once_repeat */ + NULL, /* never_mail */ + 0600, /* mode */ + 0, /* once_file_size */ + FALSE, /* file_expand */ + FALSE, /* file_optional */ + FALSE /* return message */ +}; + + + +/* Type of text for the checkexpand() function */ + +enum { cke_text, cke_hdr, cke_file }; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +autoreply_transport_init(transport_instance *tblock) +{ +/* +autoreply_transport_options_block *ob = + (autoreply_transport_options_block *)(tblock->options_block); +*/ + +/* If a fixed uid field is set, then a gid field must also be set. */ + +if (tblock->uid_set && !tblock->gid_set && tblock->expand_gid == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "user set without group for the %s transport", tblock->name); +} + + + + +/************************************************* +* Expand string and check * +*************************************************/ + +/* If the expansion fails, the error is set up in the address. Expanded +strings must be checked to ensure they contain only printing characters +and white space. If not, the function fails. + +Arguments: + s string to expand + addr address that is being worked on + name transport name, for error text + type type, for checking content: + cke_text => no check + cke_hdr => header, allow \n + whitespace + cke_file => file name, no non-printers allowed + +Returns: expanded string if expansion succeeds; + NULL otherwise +*/ + +static uschar * +checkexpand(uschar *s, address_item *addr, uschar *name, int type) +{ +uschar *t; +uschar *ss = expand_string(s); + +if (ss == NULL) + { + addr->transport_return = FAIL; + addr->message = string_sprintf("Expansion of \"%s\" failed in %s transport: " + "%s", s, name, expand_string_message); + return NULL; + } + +if (type != cke_text) for (t = ss; *t != 0; t++) + { + int c = *t; + if (mac_isprint(c)) continue; + if (type == cke_hdr && c == '\n' && (t[1] == ' ' || t[1] == '\t')) continue; + s = string_printing(s); + addr->transport_return = FAIL; + addr->message = string_sprintf("Expansion of \"%s\" in %s transport " + "contains non-printing character %d", s, name, c); + return NULL; + } + +return ss; +} + + + + +/************************************************* +* Check a header line for never_mail * +*************************************************/ + +/* This is called to check to, cc, and bcc for addresses in the never_mail +list. Any that are found are removed. + +Arguments: + listptr points to the list of addresses + never_mail an address list, already expanded + +Returns: nothing +*/ + +static void +check_never_mail(uschar **listptr, uschar *never_mail) +{ +uschar *s = *listptr; + +while (*s != 0) + { + uschar *error, *next; + uschar *e = parse_find_address_end(s, FALSE); + int terminator = *e; + int start, end, domain, rc; + + /* Temporarily terminate the string at the address end while extracting + the operative address within. */ + + *e = 0; + next = parse_extract_address(s, &error, &start, &end, &domain, FALSE); + *e = terminator; + + /* If there is some kind of syntax error, just give up on this header + line. */ + + if (next == NULL) break; + + /* See if the address is on the never_mail list */ + + rc = match_address_list(next, /* address to check */ + TRUE, /* start caseless */ + FALSE, /* don't expand the list */ + &never_mail, /* the list */ + NULL, /* no caching */ + -1, /* no expand setup */ + 0, /* separator from list */ + NULL); /* no lookup value return */ + + if (rc == OK) /* Remove this address */ + { + DEBUG(D_transport) + debug_printf("discarding recipient %s (matched never_mail)\n", next); + if (terminator == ',') e++; + memmove(s, e, Ustrlen(e) + 1); + } + else /* Skip over this address */ + { + s = e; + if (terminator == ',') s++; + } + } + +/* Check to see if we removed the last address, leaving a terminating comma +that needs to be removed */ + +s = *listptr + Ustrlen(*listptr); +while (s > *listptr && (isspace(s[-1]) || s[-1] == ',')) s--; +*s = 0; + +/* Check to see if there any addresses left; if not, set NULL */ + +s = *listptr; +while (s != 0 && isspace(*s)) s++; +if (*s == 0) *listptr = NULL; +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. This transport always returns +FALSE, indicating that the top address has the status for all - though in fact +this transport can handle only one address at at time anyway. */ + +BOOL +autoreply_transport_entry( + transport_instance *tblock, /* data for this instantiation */ + address_item *addr) /* address we are working on */ +{ +int fd, pid, rc; +int cache_fd = -1; +int log_fd = -1; +int cache_size = 0; +int add_size = 0; +EXIM_DB *dbm_file = NULL; +BOOL file_expand, return_message; +uschar *from, *reply_to, *to, *cc, *bcc, *subject, *headers, *text, *file; +uschar *logfile, *oncelog; +uschar *cache_buff = NULL; +uschar *cache_time = NULL; +header_line *h; +time_t now = time(NULL); +time_t once_repeat_sec = 0; +FILE *f; +FILE *ff = NULL; + +autoreply_transport_options_block *ob = + (autoreply_transport_options_block *)(tblock->options_block); + +DEBUG(D_transport) debug_printf("%s transport entered\n", tblock->name); + +/* Set up for the good case */ + +addr->transport_return = OK; +addr->basic_errno = 0; + +/* If the address is pointing to a reply block, then take all the data +from that block. It has typically been set up by a mail filter processing +router. Otherwise, the data must be supplied by this transport, and +it has to be expanded here. */ + +if (addr->reply != NULL) + { + DEBUG(D_transport) debug_printf("taking data from address\n"); + from = addr->reply->from; + reply_to = addr->reply->reply_to; + to = addr->reply->to; + cc = addr->reply->cc; + bcc = addr->reply->bcc; + subject = addr->reply->subject; + headers = addr->reply->headers; + text = addr->reply->text; + file = addr->reply->file; + logfile = addr->reply->logfile; + oncelog = addr->reply->oncelog; + once_repeat_sec = addr->reply->once_repeat; + file_expand = addr->reply->file_expand; + expand_forbid = addr->reply->expand_forbid; + return_message = addr->reply->return_message; + } +else + { + uschar *oncerepeat = ob->once_repeat; + + DEBUG(D_transport) debug_printf("taking data from transport\n"); + from = ob->from; + reply_to = ob->reply_to; + to = ob->to; + cc = ob->cc; + bcc = ob->bcc; + subject = ob->subject; + headers = ob->headers; + text = ob->text; + file = ob->file; + logfile = ob->logfile; + oncelog = ob->oncelog; + file_expand = ob->file_expand; + return_message = ob->return_message; + + if ((from != NULL && + (from = checkexpand(from, addr, tblock->name, cke_hdr)) == NULL) || + (reply_to != NULL && + (reply_to = checkexpand(reply_to, addr, tblock->name, cke_hdr)) == NULL) || + (to != NULL && + (to = checkexpand(to, addr, tblock->name, cke_hdr)) == NULL) || + (cc != NULL && + (cc = checkexpand(cc, addr, tblock->name, cke_hdr)) == NULL) || + (bcc != NULL && + (bcc = checkexpand(bcc, addr, tblock->name, cke_hdr)) == NULL) || + (subject != NULL && + (subject = checkexpand(subject, addr, tblock->name, cke_hdr)) == NULL) || + (headers != NULL && + (headers = checkexpand(headers, addr, tblock->name, cke_text)) == NULL) || + (text != NULL && + (text = checkexpand(text, addr, tblock->name, cke_text)) == NULL) || + (file != NULL && + (file = checkexpand(file, addr, tblock->name, cke_file)) == NULL) || + (logfile != NULL && + (logfile = checkexpand(logfile, addr, tblock->name, cke_file)) == NULL) || + (oncelog != NULL && + (oncelog = checkexpand(oncelog, addr, tblock->name, cke_file)) == NULL) || + (oncerepeat != NULL && + (oncerepeat = checkexpand(oncerepeat, addr, tblock->name, cke_file)) == NULL)) + return FALSE; + + if (oncerepeat != NULL) + { + once_repeat_sec = readconf_readtime(oncerepeat, 0, FALSE); + if (once_repeat_sec < 0) + { + addr->transport_return = FAIL; + addr->message = string_sprintf("Invalid time value \"%s\" for " + "\"once_repeat\" in %s transport", oncerepeat, tblock->name); + return FALSE; + } + } + } + +/* If the never_mail option is set, we have to scan all the recipients and +remove those that match. */ + +if (ob->never_mail != NULL) + { + uschar *never_mail = expand_string(ob->never_mail); + + if (never_mail == NULL) + { + addr->transport_return = FAIL; + addr->message = string_sprintf("Failed to expand \"%s\" for " + "\"never_mail\" in %s transport", ob->never_mail, tblock->name); + return FALSE; + } + + if (to != NULL) check_never_mail(&to, never_mail); + if (cc != NULL) check_never_mail(&cc, never_mail); + if (bcc != NULL) check_never_mail(&bcc, never_mail); + + if (to == NULL && cc == NULL && bcc == NULL) + { + DEBUG(D_transport) + debug_printf("*** all recipients removed by never_mail\n"); + return OK; + } + } + +/* If the -N option is set, can't do any more. */ + +if (dont_deliver) + { + DEBUG(D_transport) + debug_printf("*** delivery by %s transport bypassed by -N option\n", + tblock->name); + return FALSE; + } + + +/* If the oncelog field is set, we send want to send only one message to the +given recipient(s). This works only on the "To" field. If there is no "To" +field, the message is always sent. If the To: field contains more than one +recipient, the effect might not be quite as envisaged. If once_file_size is +set, instead of a dbm file, we use a regular file containing a circular buffer +recipient cache. */ + +if (oncelog != NULL && to != NULL) + { + time_t then = 0; + + /* Handle fixed-size cache file. */ + + if (ob->once_file_size > 0) + { + uschar *p; + struct stat statbuf; + cache_fd = Uopen(oncelog, O_CREAT|O_RDWR, ob->mode); + + if (cache_fd < 0 || fstat(cache_fd, &statbuf) != 0) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("Failed to %s \"once\" file %s when " + "sending message from %s transport: %s", + (cache_fd < 0)? "open" : "stat", oncelog, tblock->name, + strerror(errno)); + goto END_OFF; + } + + /* Get store in the temporary pool and read the entire file into it. We get + an amount of store that is big enough to add the new entry on the end if we + need to do that. */ + + cache_size = statbuf.st_size; + add_size = sizeof(time_t) + Ustrlen(to) + 1; + cache_buff = store_get(cache_size + add_size); + + if (read(cache_fd, cache_buff, cache_size) != cache_size) + { + addr->transport_return = DEFER; + addr->basic_errno = errno; + addr->message = US"error while reading \"once\" file"; + goto END_OFF; + } + + DEBUG(D_transport) debug_printf("%d bytes read from %s\n", cache_size, oncelog); + + /* Scan the data for this recipient. Each entry in the file starts with + a time_t sized time value, followed by the address, followed by a binary + zero. If we find a match, put the time into "then", and the place where it + was found into "cache_time". Otherwise, "then" is left at zero. */ + + p = cache_buff; + while (p < cache_buff + cache_size) + { + uschar *s = p + sizeof(time_t); + uschar *nextp = s + Ustrlen(s) + 1; + if (Ustrcmp(to, s) == 0) + { + memcpy(&then, p, sizeof(time_t)); + cache_time = p; + break; + } + p = nextp; + } + } + + /* Use a DBM file for the list of previous recipients. */ + + else + { + EXIM_DATUM key_datum, result_datum; + EXIM_DBOPEN(oncelog, O_RDWR|O_CREAT, ob->mode, &dbm_file); + if (dbm_file == NULL) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("Failed to open %s file %s when sending " + "message from %s transport: %s", EXIM_DBTYPE, oncelog, tblock->name, + strerror(errno)); + goto END_OFF; + } + + EXIM_DATUM_INIT(key_datum); /* Some DBM libraries need datums */ + EXIM_DATUM_INIT(result_datum); /* to be cleared */ + EXIM_DATUM_DATA(key_datum) = CS to; + EXIM_DATUM_SIZE(key_datum) = Ustrlen(to) + 1; + + if (EXIM_DBGET(dbm_file, key_datum, result_datum)) + { + /* If the datum size is that of a binary time, we are in the new world + where messages are sent periodically. Otherwise the file is an old one, + where the datum was filled with a tod_log time, which is assumed to be + different in size. For that, only one message is ever sent. This change + introduced at Exim 3.00. In a couple of years' time the test on the size + can be abolished. */ + + if (EXIM_DATUM_SIZE(result_datum) == sizeof(time_t)) + { + memcpy(&then, EXIM_DATUM_DATA(result_datum), sizeof(time_t)); + } + else then = now; + } + } + + /* Either "then" is set zero, if no message has yet been sent, or it + is set to the time of the last sending. */ + + if (then != 0 && (once_repeat_sec <= 0 || now - then < once_repeat_sec)) + { + DEBUG(D_transport) debug_printf("message previously sent to %s%s\n", to, + (once_repeat_sec > 0)? " and repeat time not reached" : ""); + log_fd = Uopen(logfile, O_WRONLY|O_APPEND|O_CREAT, ob->mode); + if (log_fd >= 0) + { + uschar *ptr = log_buffer; + sprintf(CS ptr, "%s\n previously sent to %.200s\n", tod_stamp(tod_log), to); + while(*ptr) ptr++; + write(log_fd, log_buffer, ptr - log_buffer); + close(log_fd); + } + goto END_OFF; + } + + DEBUG(D_transport) debug_printf("%s %s\n", (then <= 0)? + "no previous message sent to" : "repeat time reached for", to); + } + +/* We are going to send a message. Ensure any requested file is available. */ + +if (file != NULL) + { + ff = Ufopen(file, "rb"); + if (ff == NULL && !ob->file_optional) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("Failed to open file %s when sending " + "message from %s transport: %s", file, tblock->name, strerror(errno)); + return FALSE; + } + } + +/* Make a subprocess to send the message */ + +pid = child_open_exim(&fd); + +/* Creation of child failed; defer this delivery. */ + +if (pid < 0) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("Failed to create child process to send " + "message from %s transport: %s", tblock->name, strerror(errno)); + DEBUG(D_transport) debug_printf("%s\n", addr->message); + return FALSE; + } + +/* Create the message to be sent - recipients are taken from the headers, +as the -t option is used. The "headers" stuff *must* be last in case there +are newlines in it which might, if placed earlier, screw up other headers. */ + +f = fdopen(fd, "wb"); + +if (from != NULL) fprintf(f, "From: %s\n", from); +if (reply_to != NULL) fprintf(f, "Reply-To: %s\n", reply_to); +if (to != NULL) fprintf(f, "To: %s\n", to); +if (cc != NULL) fprintf(f, "Cc: %s\n", cc); +if (bcc != NULL) fprintf(f, "Bcc: %s\n", bcc); +if (subject != NULL) fprintf(f, "Subject: %s\n", subject); + +/* Generate In-Reply-To from the message_id header; there should +always be one, but code defensively. */ + +for (h = header_list; h != NULL; h = h->next) + if (h->type == htype_id) break; + +if (h != NULL) + { + uschar *s = Ustrchr(h->text, ':') + 1; + while (isspace(*s)) s++; + fprintf(f, "In-Reply-To: %s", s); + } + +/* Add an Auto-Submitted: header */ + +fprintf(f, "Auto-Submitted: auto-replied\n"); + +/* Add any specially requested headers */ + +if (headers != NULL) fprintf(f, "%s\n", headers); +fprintf(f, "\n"); + +if (text != NULL) + { + fprintf(f, "%s", CS text); + if (text[Ustrlen(text)-1] != '\n') fprintf(f, "\n"); + } + +if (ff != NULL) + { + while (Ufgets(big_buffer, big_buffer_size, ff) != NULL) + { + if (file_expand) + { + uschar *s = expand_string(big_buffer); + DEBUG(D_transport) + { + if (s == NULL) + debug_printf("error while expanding line from file:\n %s\n %s\n", + big_buffer, expand_string_message); + } + fprintf(f, "%s", (s == NULL)? CS big_buffer : CS s); + } + else fprintf(f, "%s", CS big_buffer); + } + } + +/* Copy the original message if required, observing the return size +limit. */ + +if (return_message) + { + if (bounce_return_size_limit > 0) + { + struct stat statbuf; + int max = (bounce_return_size_limit/DELIVER_IN_BUFFER_SIZE + 1) * + DELIVER_IN_BUFFER_SIZE; + if (fstat(deliver_datafile, &statbuf) == 0 && statbuf.st_size > max) + { + int size = statbuf.st_size; /* Because might be a long */ + fprintf(f, "\n" +"------ This is a copy of the message, including all the headers.\n" +"------ The body of the message is %d characters long; only the first\n" +"------ %d or so are included here.\n\n", size, (max/1000)*1000); + } + else fprintf(f, "\n" +"------ This is a copy of the message, including all the headers. ------\n\n"); + } + else fprintf(f, "\n" +"------ This is a copy of the message, including all the headers. ------\n\n"); + + fflush(f); + transport_count = 0; + transport_write_message(addr, fileno(f), + (tblock->body_only? topt_no_headers : 0) | + (tblock->headers_only? topt_no_body : 0) | + (tblock->return_path_add? topt_add_return_path : 0) | + (tblock->delivery_date_add? topt_add_delivery_date : 0) | + (tblock->envelope_to_add? topt_add_envelope_to : 0), + bounce_return_size_limit, tblock->add_headers, tblock->remove_headers, + NULL, NULL, tblock->rewrite_rules, tblock->rewrite_existflags); + } + +/* End the message and wait for the child process to end; no timeout. */ + +fclose(f); +rc = child_close(pid, 0); + +/* Update the "sent to" log whatever the yield. This errs on the side of +missing out a message rather than risking sending more than one. We either have +cache_fd set to a fixed size, circular buffer file, or dbm_file set to an open +DBM file (or neither, if "once" is not set). */ + +/* Update fixed-size cache file. If cache_time is set, we found a previous +entry; that is the spot into which to put the current time. Otherwise we have +to add a new record; remove the first one in the file if the file is too big. +We always rewrite the entire file in a single write operation. This is +(hopefully) going to be the safest thing because there is no interlocking +between multiple simultaneous deliveries. */ + +if (cache_fd >= 0) + { + uschar *from = cache_buff; + int size = cache_size; + (void)lseek(cache_fd, 0, SEEK_SET); + + if (cache_time == NULL) + { + cache_time = from + size; + memcpy(cache_time + sizeof(time_t), to, add_size - sizeof(time_t)); + size += add_size; + + if (cache_size > 0 && size > ob->once_file_size) + { + from += sizeof(time_t) + Ustrlen(from + sizeof(time_t)) + 1; + size -= (from - cache_buff); + } + } + + memcpy(cache_time, &now, sizeof(time_t)); + write(cache_fd, from, size); + } + +/* Update DBM file */ + +else if (dbm_file != NULL) + { + EXIM_DATUM key_datum, value_datum; + EXIM_DATUM_INIT(key_datum); /* Some DBM libraries need to have */ + EXIM_DATUM_INIT(value_datum); /* cleared datums. */ + EXIM_DATUM_DATA(key_datum) = CS to; + EXIM_DATUM_SIZE(key_datum) = Ustrlen(to) + 1; + + /* Many OS define the datum value, sensibly, as a void *. However, there + are some which still have char *. By casting this address to a char * we + can avoid warning messages from the char * systems. */ + + EXIM_DATUM_DATA(value_datum) = CS (&now); + EXIM_DATUM_SIZE(value_datum) = (int)sizeof(time_t); + EXIM_DBPUT(dbm_file, key_datum, value_datum); + } + +/* If sending failed, defer to try again - but if once is set the next +try will skip, of course. However, if there were no recipients in the +message, we do not fail. */ + +if (rc != 0) + { + if (rc == EXIT_NORECIPIENTS) + { + DEBUG(D_any) debug_printf("%s transport: message contained no recipients\n", + tblock->name); + } + else + { + addr->transport_return = DEFER; + addr->message = string_sprintf("Failed to send message from %s " + "transport (%d)", tblock->name, rc); + goto END_OFF; + } + } + +/* Log the sending of the message if successful and required. If the file +fails to open, it's hard to know what to do. We cannot write to the Exim +log from here, since we may be running under an unprivileged uid. We don't +want to fail the delivery, since the message has been successfully sent. For +the moment, ignore open failures. Write the log entry as a single write() to a +file opened for appending, in order to avoid interleaving of output from +different processes. The log_buffer can be used exactly as for main log +writing. */ + +if (logfile != NULL) + { + int log_fd = Uopen(logfile, O_WRONLY|O_APPEND|O_CREAT, ob->mode); + if (log_fd >= 0) + { + uschar *ptr = log_buffer; + DEBUG(D_transport) debug_printf("logging message details\n"); + sprintf(CS ptr, "%s\n", tod_stamp(tod_log)); + while(*ptr) ptr++; + if (from != NULL) + { + (void)string_format(ptr, LOG_BUFFER_SIZE - (ptr-log_buffer), + " From: %s\n", from); + while(*ptr) ptr++; + } + if (to != NULL) + { + (void)string_format(ptr, LOG_BUFFER_SIZE - (ptr-log_buffer), + " To: %s\n", to); + while(*ptr) ptr++; + } + if (cc != NULL) + { + (void)string_format(ptr, LOG_BUFFER_SIZE - (ptr-log_buffer), + " Cc: %s\n", cc); + while(*ptr) ptr++; + } + if (bcc != NULL) + { + (void)string_format(ptr, LOG_BUFFER_SIZE - (ptr-log_buffer), + " Bcc: %s\n", bcc); + while(*ptr) ptr++; + } + if (subject != NULL) + { + (void)string_format(ptr, LOG_BUFFER_SIZE - (ptr-log_buffer), + " Subject: %s\n", subject); + while(*ptr) ptr++; + } + if (headers != NULL) + { + (void)string_format(ptr, LOG_BUFFER_SIZE - (ptr-log_buffer), + " %s\n", headers); + while(*ptr) ptr++; + } + write(log_fd, log_buffer, ptr - log_buffer); + close(log_fd); + } + else DEBUG(D_transport) debug_printf("Failed to open log file %s for %s " + "transport: %s\n", logfile, tblock->name, strerror(errno)); + } + +END_OFF: +if (dbm_file != NULL) EXIM_DBCLOSE(dbm_file); +if (cache_fd > 0) close(cache_fd); + +DEBUG(D_transport) debug_printf("%s transport succeeded\n", tblock->name); + +return FALSE; +} + +/* End of transport/autoreply.c */ diff --git a/src/src/transports/autoreply.h b/src/src/transports/autoreply.h new file mode 100644 index 000000000..0ce4a0e44 --- /dev/null +++ b/src/src/transports/autoreply.h @@ -0,0 +1,47 @@ +/* $Cambridge: exim/src/src/transports/autoreply.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *from; + uschar *reply_to; + uschar *to; + uschar *cc; + uschar *bcc; + uschar *subject; + uschar *headers; + uschar *text; + uschar *file; + uschar *logfile; + uschar *oncelog; + uschar *once_repeat; + uschar *never_mail; + int mode; + off_t once_file_size; + BOOL file_expand; + BOOL file_optional; + BOOL return_message; +} autoreply_transport_options_block; + +/* Data for reading the private options. */ + +extern optionlist autoreply_transport_options[]; +extern int autoreply_transport_options_count; + +/* Block containing default values. */ + +extern autoreply_transport_options_block autoreply_transport_option_defaults; + +/* The main and init entry points for the transport */ + +extern BOOL autoreply_transport_entry(transport_instance *, address_item *); +extern void autoreply_transport_init(transport_instance *); + +/* End of transports/autoreply.h */ diff --git a/src/src/transports/lmtp.c b/src/src/transports/lmtp.c new file mode 100644 index 000000000..871f724c0 --- /dev/null +++ b/src/src/transports/lmtp.c @@ -0,0 +1,750 @@ +/* $Cambridge: exim/src/src/transports/lmtp.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "lmtp.h" + +#define PENDING_OK 256 + + +/* Options specific to the lmtp transport. They must be in alphabetic +order (note that "_" comes before the lower case letters). Those starting +with "*" are not settable by the user but are used by the option-reading +software for alternative value types. Some options are stored in the transport +instance block so as to be publicly visible; these are flagged with opt_public. +*/ + +optionlist lmtp_transport_options[] = { + { "batch_id", opt_stringptr | opt_public, + (void *)offsetof(transport_instance, batch_id) }, + { "batch_max", opt_int | opt_public, + (void *)offsetof(transport_instance, batch_max) }, + { "command", opt_stringptr, + (void *)offsetof(lmtp_transport_options_block, cmd) }, + { "socket", opt_stringptr, + (void *)offsetof(lmtp_transport_options_block, skt) }, + { "timeout", opt_time, + (void *)offsetof(lmtp_transport_options_block, timeout) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int lmtp_transport_options_count = + sizeof(lmtp_transport_options)/sizeof(optionlist); + +/* Default private options block for the lmtp transport. */ + +lmtp_transport_options_block lmtp_transport_option_defaults = { + NULL, /* cmd */ + NULL, /* skt */ + 5*60, /* timeout */ + 0 /* options */ +}; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +lmtp_transport_init(transport_instance *tblock) +{ +lmtp_transport_options_block *ob = + (lmtp_transport_options_block *)(tblock->options_block); + +/* Either the command field or the socket field must be set */ + +if ((ob->cmd == NULL) == (ob->skt == NULL)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "one (and only one) of command or socket must be set for the %s transport", + tblock->name); + +/* If a fixed uid field is set, then a gid field must also be set. */ + +if (tblock->uid_set && !tblock->gid_set && tblock->expand_gid == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "user set without group for the %s transport", tblock->name); + +/* Set up the bitwise options for transport_write_message from the various +driver options. Only one of body_only and headers_only can be set. */ + +ob->options |= + (tblock->body_only? topt_no_headers : 0) | + (tblock->headers_only? topt_no_body : 0) | + (tblock->return_path_add? topt_add_return_path : 0) | + (tblock->delivery_date_add? topt_add_delivery_date : 0) | + (tblock->envelope_to_add? topt_add_envelope_to : 0) | + topt_use_crlf | topt_end_dot; +} + + +/************************************************* +* Check an LMTP response * +*************************************************/ + +/* This function is given an errno code and the LMTP response buffer to +analyse. It sets an appropriate message and puts the first digit of the +response code into the yield variable. If no response was actually read, a +suitable digit is chosen. + +Arguments: + errno_value pointer to the errno value + more_errno from the top address for use with ERRNO_FILTER_FAIL + buffer the LMTP response buffer + yield where to put a one-digit LMTP response code + message where to put an errror message + +Returns: TRUE if a "QUIT" command should be sent, else FALSE +*/ + +static BOOL check_response(int *errno_value, int more_errno, uschar *buffer, + int *yield, uschar **message) +{ +*yield = '4'; /* Default setting is to give a temporary error */ + +/* Handle response timeout */ + +if (*errno_value == ETIMEDOUT) + { + *message = string_sprintf("LMTP timeout after %s", big_buffer); + if (transport_count > 0) + *message = string_sprintf("%s (%d bytes written)", *message, + transport_count); + *errno_value = 0; + return FALSE; + } + +/* Handle malformed LMTP response */ + +if (*errno_value == ERRNO_SMTPFORMAT) + { + *message = string_sprintf("Malformed LMTP response after %s: %s", + big_buffer, string_printing(buffer)); + return FALSE; + } + +/* Handle a failed filter process error; can't send QUIT as we mustn't +end the DATA. */ + +if (*errno_value == ERRNO_FILTER_FAIL) + { + *message = string_sprintf("transport filter process failed (%d)", more_errno); + return FALSE; + } + +/* Handle a failed add_headers expansion; can't send QUIT as we mustn't +end the DATA. */ + +if (*errno_value == ERRNO_CHHEADER_FAIL) + { + *message = + string_sprintf("failed to expand headers_add or headers_remove: %s", + expand_string_message); + return FALSE; + } + +/* Handle failure to write a complete data block */ + +if (*errno_value == ERRNO_WRITEINCOMPLETE) + { + *message = string_sprintf("failed to write a data block"); + return FALSE; + } + +/* Handle error responses from the remote process. */ + +if (buffer[0] != 0) + { + uschar *s = string_printing(buffer); + *message = string_sprintf("LMTP error after %s: %s", big_buffer, s); + *yield = buffer[0]; + return TRUE; + } + +/* No data was read. If there is no errno, this must be the EOF (i.e. +connection closed) case, which causes deferral. Otherwise, leave the errno +value to be interpreted. In all cases, we have to assume the connection is now +dead. */ + +if (*errno_value == 0) + { + *errno_value = ERRNO_SMTPCLOSED; + *message = string_sprintf("LMTP connection closed after %s", big_buffer); + } + +return FALSE; +} + + + +/************************************************* +* Write LMTP command * +*************************************************/ + +/* The formatted command is left in big_buffer so that it can be reflected in +any error message. + +Arguments: + fd the fd to write to + format a format, starting with one of + of HELO, MAIL FROM, RCPT TO, DATA, ".", or QUIT. + ... data for the format + +Returns: TRUE if successful, FALSE if not, with errno set +*/ + +static BOOL +lmtp_write_command(int fd, char *format, ...) +{ +int count, rc; +va_list ap; +va_start(ap, format); +if (!string_vformat(big_buffer, big_buffer_size, CS format, ap)) + { + errno = ERRNO_SMTPFORMAT; + return FALSE; + } +va_end(ap); +count = Ustrlen(big_buffer); +DEBUG(D_transport|D_v) debug_printf(" LMTP>> %s", big_buffer); +rc = write(fd, big_buffer, count); +big_buffer[count-2] = 0; /* remove \r\n for debug and error message */ +if (rc > 0) return TRUE; +DEBUG(D_transport) debug_printf("write failed: %s\n", strerror(errno)); +return FALSE; +} + + + + +/************************************************* +* Read LMTP response * +*************************************************/ + +/* This function reads an LMTP response with a timeout, and returns the +response in the given buffer. It also analyzes the first digit of the reply +code and returns FALSE if it is not acceptable. + +FALSE is also returned after a reading error. In this case buffer[0] will be +zero, and the error code will be in errno. + +Arguments: + f a file to read from + buffer where to put the response + size the size of the buffer + okdigit the expected first digit of the response + timeout the timeout to use + +Returns: TRUE if a valid, non-error response was received; else FALSE +*/ + +static BOOL +lmtp_read_response(FILE *f, uschar *buffer, int size, int okdigit, int timeout) +{ +int count; +uschar *ptr = buffer; +uschar *readptr = buffer; + +/* Ensure errno starts out zero */ + +errno = 0; + +/* Loop for handling LMTP responses that do not all come in one line. */ + +for (;;) + { + /* If buffer is too full, something has gone wrong. */ + + if (size < 10) + { + *readptr = 0; + errno = ERRNO_SMTPFORMAT; + return FALSE; + } + + /* Loop to cover the read getting interrupted. */ + + for (;;) + { + char *rc; + int save_errno; + + *readptr = 0; /* In case nothing gets read */ + sigalrm_seen = FALSE; + alarm(timeout); + rc = Ufgets(readptr, size-1, f); + save_errno = errno; + alarm(0); + errno = save_errno; + + if (rc != NULL) break; /* A line has been read */ + + /* Handle timeout; must do this first because it uses EINTR */ + + if (sigalrm_seen) errno = ETIMEDOUT; + + /* If some other interrupt arrived, just retry. We presume this to be rare, + but it can happen (e.g. the SIGUSR1 signal sent by exiwhat causes + read() to exit). */ + + else if (errno == EINTR) + { + DEBUG(D_transport) debug_printf("EINTR while reading LMTP response\n"); + continue; + } + + /* Handle other errors, including EOF; ensure buffer is completely empty. */ + + buffer[0] = 0; + return FALSE; + } + + /* Adjust size in case we have to read another line, and adjust the + count to be the length of the line we are about to inspect. */ + + count = Ustrlen(readptr); + size -= count; + count += readptr - ptr; + + /* See if the final two characters in the buffer are \r\n. If not, we + have to read some more. At least, that is what we should do on a strict + interpretation of the RFC. But accept LF as well, as we do for SMTP. */ + + if (ptr[count-1] != '\n') + { + DEBUG(D_transport) + { + int i; + debug_printf("LMTP input line incomplete in one buffer:\n "); + for (i = 0; i < count; i++) + { + int c = (ptr[i]); + if (mac_isprint(c)) debug_printf("%c", c); else debug_printf("<%d>", c); + } + debug_printf("\n"); + } + readptr = ptr + count; + continue; + } + + /* Remove any whitespace at the end of the buffer. This gets rid of CR, LF + etc. at the end. Show it, if debugging, formatting multi-line responses. */ + + while (count > 0 && isspace(ptr[count-1])) count--; + ptr[count] = 0; + + DEBUG(D_transport|D_v) + { + uschar *s = ptr; + uschar *t = ptr; + while (*t != 0) + { + while (*t != 0 && *t != '\n') t++; + debug_printf(" %s %*s\n", (s == ptr)? "LMTP<<" : " ", + (int)(t-s), s); + if (*t == 0) break; + s = t = t + 1; + } + } + + /* Check the format of the response: it must start with three digits; if + these are followed by a space or end of line, the response is complete. If + they are followed by '-' this is a multi-line response and we must look for + another line until the final line is reached. The only use made of multi-line + responses is to pass them back as error messages. We therefore just + concatenate them all within the buffer, which should be large enough to + accept any reasonable number of lines. A multiline response may already + have been read in one go - hence the loop here. */ + + for(;;) + { + uschar *p; + if (count < 3 || + !isdigit(ptr[0]) || + !isdigit(ptr[1]) || + !isdigit(ptr[2]) || + (ptr[3] != '-' && ptr[3] != ' ' && ptr[3] != 0)) + { + errno = ERRNO_SMTPFORMAT; /* format error */ + return FALSE; + } + + /* If a single-line response, exit the loop */ + + if (ptr[3] != '-') break; + + /* For a multi-line response see if the next line is already read, and if + so, stay in this loop to check it. */ + + p = ptr + 3; + while (*(++p) != 0) + { + if (*p == '\n') + { + ptr = ++p; + break; + } + } + if (*p == 0) break; /* No more lines to check */ + } + + /* End of response. If the last of the lines we are looking at is the final + line, we are done. Otherwise more data has to be read. */ + + if (ptr[3] != '-') break; + + /* Move the reading pointer upwards in the buffer and insert \n in case this + is an error message that subsequently gets printed. Set the scanning pointer + to the reading pointer position. */ + + ptr += count; + *ptr++ = '\n'; + size--; + readptr = ptr; + } + +/* Return a value that depends on the LMTP return code. Ensure that errno is +zero, because the caller of this function looks at errno when FALSE is +returned, to distinguish between an unexpected return code and other errors +such as timeouts, lost connections, etc. */ + +errno = 0; +return buffer[0] == okdigit; +} + + + + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. For setup-errors, this transport +returns FALSE, indicating that the first address has the status for all; in +normal cases it returns TRUE, indicating that each address has its own status +set. */ + +BOOL +lmtp_transport_entry( + transport_instance *tblock, /* data for this instantiation */ + address_item *addrlist) /* address(es) we are working on */ +{ +pid_t pid = 0; +FILE *out; +lmtp_transport_options_block *ob = + (lmtp_transport_options_block *)(tblock->options_block); +struct sockaddr_un sockun; /* don't call this "sun" ! */ +int timeout = ob->timeout; +int fd_in = -1, fd_out = -1; +int code, save_errno; +BOOL send_data; +BOOL yield = FALSE; +address_item *addr; +uschar *sockname = NULL; +uschar **argv; +uschar buffer[256]; + +DEBUG(D_transport) debug_printf("%s transport entered\n", tblock->name); + +/* Initialization ensures that either a command or a socket is specified, but +not both. When a command is specified, call the common function for creating an +argument list and expanding the items. */ + +if (ob->cmd != NULL) + { + DEBUG(D_transport) debug_printf("using command %s\n", ob->cmd); + sprintf(CS buffer, "%.50s transport", tblock->name); + if (!transport_set_up_command(&argv, ob->cmd, TRUE, PANIC, addrlist, buffer, + NULL)) + return FALSE; + } + +/* When a socket is specified, expand the string and create a socket. */ + +else + { + DEBUG(D_transport) debug_printf("using socket %s\n", ob->skt); + sockname = expand_string(ob->skt); + if (sockname == NULL) + { + addrlist->message = string_sprintf("Expansion of \"%s\" (socket setting " + "for %s transport) failed: %s", ob->skt, tblock->name, + expand_string_message); + return FALSE; + } + if ((fd_in = fd_out = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) + { + addrlist->message = string_sprintf( + "Failed to create socket %s for %s transport: %s", + ob->skt, tblock->name, strerror(errno)); + return FALSE; + } + } + +/* If the -N option is set, can't do any more. Presume all has gone well. */ + +if (dont_deliver) + { + DEBUG(D_transport) + debug_printf("*** delivery by %s transport bypassed by -N option", + tblock->name); + addrlist->transport_return = OK; + return FALSE; + } + +/* As this is a local transport, we are already running with the required +uid/gid and current directory. Request that the new process be a process group +leader, so we can kill it and all its children on an error. */ + +if (ob->cmd != NULL) + { + if ((pid = child_open(argv, NULL, 0, &fd_in, &fd_out, TRUE)) < 0) + { + addrlist->message = string_sprintf( + "Failed to create child process for %s transport: %s", tblock->name, + strerror(errno)); + return FALSE; + } + } + +/* For a socket, try to make the connection */ + +else + { + sockun.sun_family = AF_UNIX; + sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1), sockname); + if(connect(fd_out, (struct sockaddr *)(&sockun), sizeof(sockun)) == -1) + { + addrlist->message = string_sprintf( + "Failed to connect to socket %s for %s transport: %s", + sockun.sun_path, tblock->name, strerror(errno)); + return FALSE; + } + } + +/* Make the output we are going to read into a file. */ + +out = fdopen(fd_out, "rb"); + +/* Now we must implement the LMTP protocol. It is like SMTP, except that after +the end of the message, a return code for every accepted RCPT TO is sent. This +allows for message+recipient checks after the message has been received. */ + +/* First thing is to wait for an initial greeting. */ + +Ustrcpy(big_buffer, "initial connection"); +if (!lmtp_read_response(out, buffer, sizeof(buffer), '2', + timeout)) goto RESPONSE_FAILED; + +/* Next, we send a LHLO command, and expect a positive response */ + +if (!lmtp_write_command(fd_in, "%s %s\r\n", "LHLO", + primary_hostname)) goto WRITE_FAILED; + +if (!lmtp_read_response(out, buffer, sizeof(buffer), '2', + timeout)) goto RESPONSE_FAILED; + +/* Now the envelope sender */ + +if (!lmtp_write_command(fd_in, "MAIL FROM:<%s>\r\n", return_path)) + goto WRITE_FAILED; + +if (!lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout)) + goto RESPONSE_FAILED; + +/* Next, we hand over all the recipients. Some may be permanently or +temporarily rejected; others may be accepted, for now. */ + +send_data = FALSE; +for (addr = addrlist; addr != NULL; addr = addr->next) + { + if (!lmtp_write_command(fd_in, "RCPT TO:<%s>\r\n", + transport_rcpt_address(addr, tblock->rcpt_include_affixes))) + goto WRITE_FAILED; + if (lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout)) + { + send_data = TRUE; + addr->transport_return = PENDING_OK; + } + else + { + if (errno != 0 || buffer[0] == 0) goto RESPONSE_FAILED; + addr->message = string_sprintf("LMTP error after %s: %s", big_buffer, + string_printing(buffer)); + if (buffer[0] == '5') addr->transport_return = FAIL; else + { + int bincode = (buffer[1] - '0')*10 + buffer[2] - '0'; + addr->basic_errno = ERRNO_RCPT4XX; + addr->more_errno |= bincode << 8; + } + } + } + +/* Now send the text of the message if there were any good recipients. */ + +if (send_data) + { + BOOL ok; + + if (!lmtp_write_command(fd_in, "DATA\r\n")) goto WRITE_FAILED; + if (!lmtp_read_response(out, buffer, sizeof(buffer), '3', timeout)) + goto RESPONSE_FAILED; + + sigalrm_seen = FALSE; + transport_write_timeout = timeout; + Ustrcpy(big_buffer, "sending data block"); /* For error messages */ + DEBUG(D_transport|D_v) + debug_printf(" LMTP>> writing message and terminating \".\"\n"); + + transport_count = 0; + ok = transport_write_message(addrlist, fd_in, ob->options, 0, + tblock->add_headers, tblock->remove_headers, US".", US"..", + tblock->rewrite_rules, tblock->rewrite_existflags); + + /* Failure can either be some kind of I/O disaster (including timeout), + or the failure of a transport filter or the expansion of added headers. */ + + if (!ok) + { + buffer[0] = 0; /* There hasn't been a response */ + goto RESPONSE_FAILED; + } + + Ustrcpy(big_buffer, "end of data"); /* For error messages */ + + /* We now expect a response for every address that was accepted above, + in the same order. For those that get a response, their status is fixed; + any that are accepted have been handed over, even if later responses crash - + at least, that's how I read RFC 2033. */ + + for (addr = addrlist; addr != NULL; addr = addr->next) + { + if (addr->transport_return != PENDING_OK) continue; + + if (lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout)) + addr->transport_return = OK; + + /* If the response has failed badly, use it for all the remaining pending + addresses and give up. */ + + else if (errno != 0 || buffer[0] == 0) + { + address_item *a; + save_errno = errno; + check_response(&save_errno, addr->more_errno, buffer, &code, + &(addr->message)); + addr->transport_return = (code == '5')? FAIL : DEFER; + for (a = addr->next; a != NULL; a = a->next) + { + if (a->transport_return != PENDING_OK) continue; + a->basic_errno = addr->basic_errno; + a->message = addr->message; + a->transport_return = addr->transport_return; + } + break; + } + + /* Otherwise, it's an LMTP error code return for one address */ + + else + { + addr->message = string_sprintf("LMTP error after %s: %s", big_buffer, + string_printing(buffer)); + addr->transport_return = (buffer[0] == '5')? FAIL : DEFER; + } + } + } + +/* The message transaction has completed successfully - this doesn't mean that +all the addresses have necessarily been transferred, but each has its status +set, so we change the yield to TRUE. */ + +yield = TRUE; +(void) lmtp_write_command(fd_in, "QUIT\r\n"); +(void) lmtp_read_response(out, buffer, sizeof(buffer), '2', 1); + +goto RETURN; + + +/* Come here if any call to read_response, other than a response after the data +phase, failed. Put the error in the top address - this will be replicated +because the yield is still FALSE. Analyse the error, and if if isn't too bad, +send a QUIT command. Wait for the response with a short timeout, so we don't +wind up this process before the far end has had time to read the QUIT. */ + +RESPONSE_FAILED: + +save_errno = errno; +addrlist->message = NULL; + +if (check_response(&save_errno, addrlist->more_errno, + buffer, &code, &(addrlist->message))) + { + (void) lmtp_write_command(fd_in, "QUIT\r\n"); + (void) lmtp_read_response(out, buffer, sizeof(buffer), '2', 1); + } + +addrlist->transport_return = (code == '5')? FAIL : DEFER; +if (code == '4' && save_errno > 0) + addrlist->message = string_sprintf("%s: %s", addrlist->message, + strerror(save_errno)); +goto KILL_AND_RETURN; + +/* Come here if there are errors during writing of a command or the message +itself. This error will be applied to all the addresses. */ + +WRITE_FAILED: + +addrlist->transport_return = PANIC; +addrlist->basic_errno = errno; +if (errno == ERRNO_CHHEADER_FAIL) + addrlist->message = + string_sprintf("Failed to expand headers_add or headers_remove: %s", + expand_string_message); +else if (errno == ERRNO_FILTER_FAIL) + addrlist->message = string_sprintf("Filter process failure"); +else if (errno == ERRNO_WRITEINCOMPLETE) + addrlist->message = string_sprintf("Failed repeatedly to write data"); +else if (errno == ERRNO_SMTPFORMAT) + addrlist->message = US"overlong LMTP command generated"; +else + addrlist->message = string_sprintf("Error %d", errno); + +/* Come here after errors. Kill off the process. */ + +KILL_AND_RETURN: + +if (pid > 0) killpg(pid, SIGKILL); + +/* Come here from all paths after the subprocess is created. Wait for the +process, but with a timeout. */ + +RETURN: + +(void)child_close(pid, timeout); + +if (fd_in >= 0) (void) close(fd_in); +if (fd_out >= 0) (void) fclose(out); + +DEBUG(D_transport) + debug_printf("%s transport yields %d\n", tblock->name, yield); + +return yield; +} + +/* End of transport/lmtp.c */ diff --git a/src/src/transports/lmtp.h b/src/src/transports/lmtp.h new file mode 100644 index 000000000..d6d3c21ca --- /dev/null +++ b/src/src/transports/lmtp.h @@ -0,0 +1,33 @@ +/* $Cambridge: exim/src/src/transports/lmtp.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *cmd; + uschar *skt; + int timeout; + int options; +} lmtp_transport_options_block; + +/* Data for reading the private options. */ + +extern optionlist lmtp_transport_options[]; +extern int lmtp_transport_options_count; + +/* Block containing default values. */ + +extern lmtp_transport_options_block lmtp_transport_option_defaults; + +/* The main and init entry points for the transport */ + +extern BOOL lmtp_transport_entry(transport_instance *, address_item *); +extern void lmtp_transport_init(transport_instance *); + +/* End of transports/lmtp.h */ diff --git a/src/src/transports/pipe.c b/src/src/transports/pipe.c new file mode 100644 index 000000000..f2fe47112 --- /dev/null +++ b/src/src/transports/pipe.c @@ -0,0 +1,1010 @@ +/* $Cambridge: exim/src/src/transports/pipe.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "../exim.h" +#include "pipe.h" + + + +/* Options specific to the pipe transport. They must be in alphabetic +order (note that "_" comes before the lower case letters). Those starting +with "*" are not settable by the user but are used by the option-reading +software for alternative value types. Some options are stored in the transport +instance block so as to be publicly visible; these are flagged with opt_public. +*/ + +optionlist pipe_transport_options[] = { + { "allow_commands", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, allow_commands) }, + { "batch_id", opt_stringptr | opt_public, + (void *)offsetof(transport_instance, batch_id) }, + { "batch_max", opt_int | opt_public, + (void *)offsetof(transport_instance, batch_max) }, + { "check_string", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, check_string) }, + { "command", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, cmd) }, + { "environment", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, environment) }, + { "escape_string", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, escape_string) }, + { "freeze_exec_fail", opt_bool, + (void *)offsetof(pipe_transport_options_block, freeze_exec_fail) }, + { "ignore_status", opt_bool, + (void *)offsetof(pipe_transport_options_block, ignore_status) }, + { "log_defer_output", opt_bool | opt_public, + (void *)offsetof(transport_instance, log_defer_output) }, + { "log_fail_output", opt_bool | opt_public, + (void *)offsetof(transport_instance, log_fail_output) }, + { "log_output", opt_bool | opt_public, + (void *)offsetof(transport_instance, log_output) }, + { "max_output", opt_mkint, + (void *)offsetof(pipe_transport_options_block, max_output) }, + { "message_prefix", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, message_prefix) }, + { "message_suffix", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, message_suffix) }, + { "path", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, path) }, + { "pipe_as_creator", opt_bool | opt_public, + (void *)offsetof(transport_instance, deliver_as_creator) }, + { "restrict_to_path", opt_bool, + (void *)offsetof(pipe_transport_options_block, restrict_to_path) }, + { "return_fail_output",opt_bool | opt_public, + (void *)offsetof(transport_instance, return_fail_output) }, + { "return_output", opt_bool | opt_public, + (void *)offsetof(transport_instance, return_output) }, + { "temp_errors", opt_stringptr, + (void *)offsetof(pipe_transport_options_block, temp_errors) }, + { "timeout", opt_time, + (void *)offsetof(pipe_transport_options_block, timeout) }, + { "umask", opt_octint, + (void *)offsetof(pipe_transport_options_block, umask) }, + { "use_bsmtp", opt_bool, + (void *)offsetof(pipe_transport_options_block, use_bsmtp) }, + { "use_crlf", opt_bool, + (void *)offsetof(pipe_transport_options_block, use_crlf) }, + { "use_shell", opt_bool, + (void *)offsetof(pipe_transport_options_block, use_shell) }, +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int pipe_transport_options_count = + sizeof(pipe_transport_options)/sizeof(optionlist); + +/* Default private options block for the pipe transport. */ + +pipe_transport_options_block pipe_transport_option_defaults = { + NULL, /* cmd */ + NULL, /* allow_commands */ + NULL, /* environment */ + US"/usr/bin", /* path */ + NULL, /* message_prefix (reset in init if not bsmtp) */ + NULL, /* message_suffix (ditto) */ + US mac_expanded_string(EX_TEMPFAIL) ":" /* temp_errors */ + mac_expanded_string(EX_CANTCREAT), + NULL, /* check_string */ + NULL, /* escape_string */ + 022, /* umask */ + 20480, /* max_output */ + 60*60, /* timeout */ + 0, /* options */ + FALSE, /* freeze_exec_fail */ + FALSE, /* ignore_status */ + FALSE, /* restrict_to_path */ + FALSE, /* use_shell */ + FALSE, /* use_bsmtp */ + FALSE /* use_crlf */ +}; + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +pipe_transport_init(transport_instance *tblock) +{ +pipe_transport_options_block *ob = + (pipe_transport_options_block *)(tblock->options_block); + +/* If pipe_as_creator is set, then uid/gid should not be set. */ + +if (tblock->deliver_as_creator && (tblock->uid_set || tblock->gid_set || + tblock->expand_uid != NULL || tblock->expand_gid != NULL)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "both pipe_as_creator and an explicit uid/gid are set for the %s " + "transport", tblock->name); + +/* If a fixed uid field is set, then a gid field must also be set. */ + +if (tblock->uid_set && !tblock->gid_set && tblock->expand_gid == NULL) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "user set without group for the %s transport", tblock->name); + +/* Temp_errors must consist only of digits and colons, but there can be +spaces round the colons, so allow them too. */ + +if (ob->temp_errors != NULL && Ustrcmp(ob->temp_errors, "*") != 0) + { + size_t p = Ustrspn(ob->temp_errors, "0123456789: "); + if (ob->temp_errors[p] != 0) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "temp_errors must be a list of numbers or an asterisk for the %s " + "transport", tblock->name); + } + +/* Only one of return_output/return_fail_output or log_output/log_fail_output +should be set. */ + +if (tblock->return_output && tblock->return_fail_output) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "both return_output and return_fail_output set for %s transport", + tblock->name); + +if (tblock->log_output && tblock->log_fail_output) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "both log_output and log_fail_output set for the %s transport", + tblock->name); + +/* If batch SMTP is set, force the check and escape strings, and arrange that +headers are also escaped. */ + +if (ob->use_bsmtp) + { + ob->check_string = US"."; + ob->escape_string = US".."; + ob->options |= topt_escape_headers; + } + +/* If not batch SMTP, and message_prefix or message_suffix are unset, insert +default values for them. */ + +else + { + if (ob->message_prefix == NULL) ob->message_prefix = + US"From ${if def:return_path{$return_path}{MAILER-DAEMON}} ${tod_bsdinbox}\n"; + if (ob->message_suffix == NULL) ob->message_suffix = US"\n"; + } + +/* The restrict_to_path and use_shell options are incompatible */ + +if (ob->restrict_to_path && ob->use_shell) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "both restrict_to_path and use_shell set for %s transport", + tblock->name); + +/* The allow_commands and use_shell options are incompatible */ + +if (ob->allow_commands && ob->use_shell) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "both allow_commands and use_shell set for %s transport", + tblock->name); + +/* Set up the bitwise options for transport_write_message from the various +driver options. Only one of body_only and headers_only can be set. */ + +ob->options |= + (tblock->body_only? topt_no_headers : 0) | + (tblock->headers_only? topt_no_body : 0) | + (tblock->return_path_add? topt_add_return_path : 0) | + (tblock->delivery_date_add? topt_add_delivery_date : 0) | + (tblock->envelope_to_add? topt_add_envelope_to : 0) | + (ob->use_crlf? topt_use_crlf : 0); +} + + + +/************************************************* +* Set up direct (non-shell) command * +*************************************************/ + +/* This function is called when a command line is to be parsed by the transport +and executed directly, without the use of /bin/sh. + +Arguments: + argvptr pointer to anchor for argv vector + cmd points to the command string + expand_arguments true if expansion is to occur + expand_fail error if expansion fails + addr chain of addresses + tname the transport name + ob the transport options block + +Returns: TRUE if all went well; otherwise an error will be + set in the first address and FALSE returned +*/ + +static BOOL +set_up_direct_command(uschar ***argvptr, uschar *cmd, BOOL expand_arguments, + int expand_fail, address_item *addr, uschar *tname, + pipe_transport_options_block *ob) +{ +BOOL permitted = FALSE; +uschar **argv; +uschar buffer[64]; + +/* Set up "transport " to be put in any error messages, and then +call the common function for creating an argument list and expanding +the items if necessary. If it fails, this function fails (error information +is in the addresses). */ + +sprintf(CS buffer, "%.50s transport", tname); +if (!transport_set_up_command(argvptr, cmd, expand_arguments, expand_fail, + addr, buffer, NULL)) + return FALSE; + +/* Point to the set-up arguments. */ + +argv = *argvptr; + +/* If allow_commands is set, see if the command is in the permitted list. */ + +if (ob->allow_commands != NULL) + { + int sep = 0; + uschar *s, *p; + uschar buffer[256]; + + s = expand_string(ob->allow_commands); + if (s == NULL) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("failed to expand string \"%s\" " + "for %s transport: %s", ob->allow_commands, tname, expand_string_message); + return FALSE; + } + + while ((p = string_nextinlist(&s, &sep, buffer, sizeof(buffer))) != NULL) + { + if (Ustrcmp(p, argv[0]) == 0) { permitted = TRUE; break; } + } + } + +/* If permitted is TRUE it means the command was found in the allowed list, and +no further checks are done. If permitted = FALSE, it either means +allow_commands wasn't set, or that the command didn't match anything in the +list. In both cases, if restrict_to_path is set, we fail if the command +contains any slashes, but if restrict_to_path is not set, we must fail the +command only if allow_commands is set. */ + +if (!permitted) + { + if (ob->restrict_to_path) + { + if (Ustrchr(argv[0], '/') != NULL) + { + addr->transport_return = FAIL; + addr->message = string_sprintf("\"/\" found in \"%s\" (command for %s " + "transport) - failed for security reasons", cmd, tname); + return FALSE; + } + } + + else if (ob->allow_commands != NULL) + { + addr->transport_return = FAIL; + addr->message = string_sprintf("\"%s\" command not permitted by %s " + "transport", argv[0], tname); + return FALSE; + } + } + +/* If the command is not an absolute path, search the PATH directories +for it. */ + +if (argv[0][0] != '/') + { + int sep = 0; + uschar *p; + uschar *listptr = ob->path; + uschar buffer[1024]; + + while ((p = string_nextinlist(&listptr, &sep, buffer, sizeof(buffer))) != NULL) + { + struct stat statbuf; + sprintf(CS big_buffer, "%.256s/%.256s", p, argv[0]); + if (Ustat(big_buffer, &statbuf) == 0) + { + argv[0] = string_copy(big_buffer); + break; + } + } + if (p == NULL) + { + addr->transport_return = FAIL; + addr->message = string_sprintf("\"%s\" command not found for %s transport", + argv[0], tname); + return FALSE; + } + } + +return TRUE; +} + + +/************************************************* +* Set up shell command * +*************************************************/ + +/* This function is called when a command line is to be passed to /bin/sh +without parsing inside the transport. + +Arguments: + argvptr pointer to anchor for argv vector + cmd points to the command string + expand_arguments true if expansion is to occur + expand_fail error if expansion fails + addr chain of addresses + tname the transport name + +Returns: TRUE if all went well; otherwise an error will be + set in the first address and FALSE returned +*/ + +static BOOL +set_up_shell_command(uschar ***argvptr, uschar *cmd, BOOL expand_arguments, + int expand_fail, address_item *addr, uschar *tname) +{ +uschar **argv; + +*argvptr = argv = store_get((4)*sizeof(uschar *)); + +argv[0] = US"/bin/sh"; +argv[1] = US"-c"; + +/* We have to take special action to handle the special "variable" called +$pipe_addresses, which is not recognized by the normal expansion function. */ + +DEBUG(D_transport) + debug_printf("shell pipe command before expansion:\n %s\n", cmd); + +if (expand_arguments) + { + uschar *s = cmd; + uschar *p = Ustrstr(cmd, "pipe_addresses"); + + if (p != NULL && ( + (p > cmd && p[-1] == '$') || + (p > cmd + 1 && p[-2] == '$' && p[-1] == '{' && p[14] == '}'))) + { + address_item *ad; + uschar *q = p + 14; + int size = Ustrlen(cmd) + 64; + int offset; + + if (p[-1] == '{') { q++; p--; } + + s = store_get(size); + offset = p - cmd - 1; + Ustrncpy(s, cmd, offset); + + for (ad = addr; ad != NULL; ad = ad->next) + { + if (ad != addr) string_cat(s, &size, &offset, US" ", 1); + string_cat(s, &size, &offset, ad->address, Ustrlen(ad->address)); + } + + string_cat(s, &size, &offset, q, Ustrlen(q)); + s[offset] = 0; + } + + /* Allow $recipients in the expansion iff it comes from a system filter */ + + enable_dollar_recipients = addr != NULL && + addr->parent != NULL && + Ustrcmp(addr->parent->address, "system-filter") == 0; + argv[2] = expand_string(s); + enable_dollar_recipients = FALSE; + + if (argv[2] == NULL) + { + addr->transport_return = search_find_defer? DEFER : expand_fail; + addr->message = string_sprintf("Expansion of command \"%s\" " + "in %s transport failed: %s", + cmd, tname, expand_string_message); + return FALSE; + } + + DEBUG(D_transport) + debug_printf("shell pipe command after expansion:\n %s\n", argv[2]); + } +else argv[2] = cmd; + +argv[3] = (uschar *)0; +return TRUE; +} + + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. This transport always returns FALSE, +indicating that the status in the first address is the status for all addresses +in a batch. */ + +BOOL +pipe_transport_entry( + transport_instance *tblock, /* data for this instantiation */ + address_item *addr) /* address(es) we are working on */ +{ +pid_t pid, outpid; +int fd_in, fd_out, rc; +int envcount = 0; +int envsep = 0; +int expand_fail; +pipe_transport_options_block *ob = + (pipe_transport_options_block *)(tblock->options_block); +int timeout = ob->timeout; +BOOL written_ok = FALSE; +BOOL expand_arguments; +uschar **argv; +uschar *envp[50]; +uschar *envlist = ob->environment; +uschar *cmd, *ss; +uschar *eol = (ob->use_crlf)? US"\r\n" : US"\n"; + +DEBUG(D_transport) debug_printf("%s transport entered\n", tblock->name); + +/* Set up for the good case */ + +addr->transport_return = OK; +addr->basic_errno = 0; + +/* Pipes are not accepted as general addresses, but they can be generated from +.forward files or alias files. In those cases, the pfr flag is set, and the +command to be obeyed is pointed to by addr->local_part; it starts with the pipe +symbol. In other cases, the command is supplied as one of the pipe transport's +options. */ + +if (testflag(addr, af_pfr) && addr->local_part[0] == '|') + { + cmd = addr->local_part + 1; + while (isspace(*cmd)) cmd++; + expand_arguments = testflag(addr, af_expand_pipe); + expand_fail = FAIL; + } +else + { + cmd = ob->cmd; + expand_arguments = TRUE; + expand_fail = PANIC; + } + +/* If no command has been supplied, we are in trouble. */ + +if (cmd == NULL) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("no command specified for %s transport", + tblock->name); + return FALSE; + } + +/* When a pipe is set up by a filter file, there may be values for $thisaddress +and numerical the variables in existence. These are passed in +addr->pipe_expandn for use here. */ + +if (expand_arguments && addr->pipe_expandn != NULL) + { + uschar **ss = addr->pipe_expandn; + expand_nmax = -1; + if (*ss != NULL) filter_thisaddress = *ss++; + while (*ss != NULL) + { + expand_nstring[++expand_nmax] = *ss; + expand_nlength[expand_nmax] = Ustrlen(*ss++); + } + } + +/* The default way of processing the command is to split it up into arguments +here, and run it directly. This offers some security advantages. However, there +are installations that want by default to run commands under /bin/sh always, so +there is an option to do that. */ + +if (ob->use_shell) + { + if (!set_up_shell_command(&argv, cmd, expand_arguments, expand_fail, addr, + tblock->name)) return FALSE; + } +else if (!set_up_direct_command(&argv, cmd, expand_arguments, expand_fail, addr, + tblock->name, ob)) return FALSE; + +expand_nmax = -1; /* Reset */ +filter_thisaddress = NULL; + +/* Set up the environment for the command. */ + +envp[envcount++] = string_sprintf("LOCAL_PART=%s", deliver_localpart); +envp[envcount++] = string_sprintf("LOGNAME=%s", deliver_localpart); +envp[envcount++] = string_sprintf("USER=%s", deliver_localpart); +envp[envcount++] = string_sprintf("LOCAL_PART_PREFIX=%#s", + deliver_localpart_prefix); +envp[envcount++] = string_sprintf("LOCAL_PART_SUFFIX=%#s", + deliver_localpart_suffix); +envp[envcount++] = string_sprintf("DOMAIN=%s", deliver_domain); +envp[envcount++] = string_sprintf("HOME=%#s", deliver_home); +envp[envcount++] = string_sprintf("MESSAGE_ID=%s", message_id); +envp[envcount++] = string_sprintf("PATH=%s", ob->path); +envp[envcount++] = string_sprintf("RECIPIENT=%#s%#s%#s@%#s", + deliver_localpart_prefix, deliver_localpart, deliver_localpart_suffix, + deliver_domain); +envp[envcount++] = string_sprintf("QUALIFY_DOMAIN=%s", qualify_domain_sender); +envp[envcount++] = string_sprintf("SENDER=%s", sender_address); +envp[envcount++] = US"SHELL=/bin/sh"; + +if (addr->host_list != NULL) + envp[envcount++] = string_sprintf("HOST=%s", addr->host_list->name); + +if (timestamps_utc) envp[envcount++] = US"TZ=UTC"; +else if (timezone_string != NULL && timezone_string[0] != 0) + envp[envcount++] = string_sprintf("TZ=%s", timezone_string); + +/* Add any requested items */ + +if (envlist != NULL) + { + envlist = expand_string(envlist); + if (envlist == NULL) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("failed to expand string \"%s\" " + "for %s transport: %s", ob->environment, tblock->name, + expand_string_message); + return FALSE; + } + } + +while ((ss = string_nextinlist(&envlist, &envsep, big_buffer, big_buffer_size)) + != NULL) + { + if (envcount > sizeof(envp)/sizeof(uschar *) - 2) + { + addr->transport_return = DEFER; + addr->message = string_sprintf("too many environment settings for " + "%s transport", tblock->name); + return FALSE; + } + envp[envcount++] = string_copy(ss); + } + +envp[envcount] = NULL; + +/* If the -N option is set, can't do any more. */ + +if (dont_deliver) + { + DEBUG(D_transport) + debug_printf("*** delivery by %s transport bypassed by -N option", + tblock->name); + return FALSE; + } + + +/* Handling the output from the pipe is tricky. If a file for catching this +output is provided, we could in theory just hand that fd over to the process, +but this isn't very safe because it might loop and carry on writing for +ever (which is exactly what happened in early versions of Exim). Therefore we +use the standard child_open() function, which creates pipes. We can then read +our end of the output pipe and count the number of bytes that come through, +chopping the sub-process if it exceeds some limit. + +However, this means we want to run a sub-process with both its input and output +attached to pipes. We can't handle that easily from a single parent process +using straightforward code such as the transport_write_message() function +because the subprocess might not be reading its input because it is trying to +write to a full output pipe. The complication of redesigning the world to +handle this is too great - simpler just to run another process to do the +reading of the output pipe. */ + + +/* As this is a local transport, we are already running with the required +uid/gid and current directory. Request that the new process be a process group +leader, so we can kill it and all its children on a timeout. */ + +if ((pid = child_open(argv, envp, ob->umask, &fd_in, &fd_out, TRUE)) < 0) + { + addr->transport_return = DEFER; + addr->message = string_sprintf( + "Failed to create child process for %s transport: %s", tblock->name, + strerror(errno)); + return FALSE; + } + +/* Now fork a process to handle the output that comes down the pipe. */ + +if ((outpid = fork()) < 0) + { + addr->basic_errno = errno; + addr->transport_return = DEFER; + addr->message = string_sprintf( + "Failed to create process for handling output in %s transport", + tblock->name); + close(fd_in); + close(fd_out); + return FALSE; + } + +/* This is the code for the output-handling subprocess. Read from the pipe +in chunks, and write to the return file if one is provided. Keep track of +the number of bytes handled. If the limit is exceeded, try to kill the +subprocess group, and in any case close the pipe and exit, which should cause +the subprocess to fail. */ + +if (outpid == 0) + { + int count = 0; + close(fd_in); + set_process_info("reading output from |%s", cmd); + while ((rc = read(fd_out, big_buffer, big_buffer_size)) > 0) + { + if (addr->return_file >= 0) + write(addr->return_file, big_buffer, rc); + count += rc; + if (count > ob->max_output) + { + uschar *message = US"\n\n*** Too much output - remainder discarded ***\n"; + DEBUG(D_transport) debug_printf("Too much output from pipe - killed\n"); + if (addr->return_file >= 0) + write(addr->return_file, message, Ustrlen(message)); + killpg(pid, SIGKILL); + break; + } + } + close(fd_out); + _exit(0); + } + +close(fd_out); /* Not used in this process */ + + +/* Carrying on now with the main parent process. Attempt to write the message +to it down the pipe. It is a fallacy to think that you can detect write errors +when the sub-process fails to read the pipe. The parent process may complete +writing and close the pipe before the sub-process completes. We could sleep a +bit here to let the sub-process get going, but it may still not complete. So we +ignore all writing errors. (When in the test harness, we do do a short sleep so +any debugging output is likely to be in the same order.) */ + +if (running_in_test_harness) millisleep(500); + +DEBUG(D_transport) debug_printf("Writing message to pipe\n"); + +/* Arrange to time out writes if there is a timeout set. */ + +if (timeout > 0) + { + sigalrm_seen = FALSE; + transport_write_timeout = timeout; + } + +/* Reset the counter of bytes written */ + +transport_count = 0; + +/* First write any configured prefix information */ + +if (ob->message_prefix != NULL) + { + uschar *prefix = expand_string(ob->message_prefix); + if (prefix == NULL) + { + addr->transport_return = search_find_defer? DEFER : PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (prefix for %s " + "transport) failed: %s", ob->message_prefix, tblock->name, + expand_string_message); + return FALSE; + } + if (!transport_write_block(fd_in, prefix, Ustrlen(prefix))) + goto END_WRITE; + } + +/* If the use_bsmtp option is set, we need to write SMTP prefix information. +The various different values for batching are handled outside; if there is more +than one address available here, all must be included. Force SMTP dot-handling. +*/ + +if (ob->use_bsmtp) + { + address_item *a; + + if (!transport_write_string(fd_in, "MAIL FROM:<%s>%s", return_path, eol)) + goto END_WRITE; + + for (a = addr; a != NULL; a = a->next) + { + if (!transport_write_string(fd_in, + "RCPT TO:<%s>%s", + transport_rcpt_address(a, tblock->rcpt_include_affixes), + eol)) + goto END_WRITE; + } + + if (!transport_write_string(fd_in, "DATA%s", eol)) goto END_WRITE; + } + +/* Now the actual message - the options were set at initialization time */ + +if (!transport_write_message(addr, fd_in, ob->options, 0, tblock->add_headers, + tblock->remove_headers, ob->check_string, ob->escape_string, + tblock->rewrite_rules, tblock->rewrite_existflags)) + goto END_WRITE; + +/* Now any configured suffix */ + +if (ob->message_suffix != NULL) + { + uschar *suffix = expand_string(ob->message_suffix); + if (suffix == NULL) + { + addr->transport_return = search_find_defer? DEFER : PANIC; + addr->message = string_sprintf("Expansion of \"%s\" (suffix for %s " + "transport) failed: %s", ob->message_suffix, tblock->name, + expand_string_message); + return FALSE; + } + if (!transport_write_block(fd_in, suffix, Ustrlen(suffix))) + goto END_WRITE; + } + +/* If local_smtp, write the terminating dot. */ + +if (ob->use_bsmtp && !transport_write_string(fd_in, ".%s", eol)) + goto END_WRITE; + +/* Flag all writing completed successfully. */ + +written_ok = TRUE; + +/* Come here if there are errors during writing. */ + +END_WRITE: + +/* OK, the writing is now all done. Close the pipe. */ + +(void) close(fd_in); + +/* Handle errors during writing. For timeouts, set the timeout for waiting for +the child process to 1 second. If the process at the far end of the pipe died +without reading all of it, we expect an EPIPE error, which should be ignored. +We used also to ignore WRITEINCOMPLETE but the writing function is now cleverer +at handling OS where the death of a pipe doesn't give EPIPE immediately. See +comments therein. This change made 04-Sep-98. Clean up this code in a year or +so. */ + +if (!written_ok) + { + if (errno == ETIMEDOUT) + timeout = 1; + else if (errno == EPIPE /* || errno == ERRNO_WRITEINCOMPLETE */ ) + { + debug_printf("transport error %s ignored\n", + (errno == EPIPE)? "EPIPE" : "WRITEINCOMPLETE"); + } + else + { + addr->transport_return = PANIC; + addr->basic_errno = errno; + if (errno == ERRNO_CHHEADER_FAIL) + addr->message = + string_sprintf("Failed to expand headers_add or headers_remove: %s", + expand_string_message); + else if (errno == ERRNO_FILTER_FAIL) + addr->message = string_sprintf("Filter process failure"); + else if (errno == ERRNO_WRITEINCOMPLETE) + addr->message = string_sprintf("Failed repeatedly to write data"); + else + addr->message = string_sprintf("Error %d", errno); + return FALSE; + } + } + +/* Wait for the child process to complete and take action if the returned +status is nonzero. The timeout will be just 1 second if any of the writes +above timed out. */ + +if ((rc = child_close(pid, timeout)) != 0) + { + /* The process did not complete in time; kill its process group and fail + the delivery. It appears to be necessary to kill the output process too, as + otherwise it hangs on for some time if the actual pipe process is sleeping. + (At least, that's what I observed on Solaris 2.5.1.) Since we are failing + the delivery, that shouldn't cause any problem. */ + + if (rc == -256) + { + killpg(pid, SIGKILL); + kill(outpid, SIGKILL); + addr->transport_return = FAIL; + addr->message = string_sprintf("pipe delivery process timed out"); + } + + /* Wait() failed. */ + + else if (rc == -257) + { + addr->transport_return = PANIC; + addr->message = string_sprintf("Wait() failed for child process of %s " + "transport: %s", tblock->name, strerror(errno)); + } + + /* Either the process completed, but yielded a non-zero (necessarily + positive) status, or the process was terminated by a signal (rc will contain + the negation of the signal number). Treat killing by signal as failure unless + status is being ignored. */ + + else if (rc < 0) + { + if (!ob->ignore_status) + { + addr->transport_return = FAIL; + addr->message = string_sprintf("Child process of %s transport (running " + "command \"%s\") was terminated by signal %d (%s)", tblock->name, cmd, + -rc, os_strsignal(-rc)); + } + } + + /* For positive values (process terminated with non-zero status), we need a + status code to request deferral. A number of systems contain the following + line in sysexits.h: + + #define EX_TEMPFAIL 75 + + with the description + + EX_TEMPFAIL -- temporary failure, indicating something that + is not really an error. In sendmail, this means + that a mailer (e.g.) could not create a connection, + and the request should be reattempted later. + + Based on this, we use exit code EX_TEMPFAIL as a default to mean "defer" when + not ignoring the returned status. However, there is now an option that + contains a list of temporary codes, with TEMPFAIL and CANTCREAT as defaults. + + Another case that needs special treatment is if execve() failed (typically + the command that was given is a non-existent path). By default this is + treated as just another failure, but if freeze_exec_fail is set, the reaction + is to freeze the message rather than bounce the address. Exim used to signal + this failure with EX_UNAVAILABLE, which is definined in many systems as + + #define EX_UNAVAILABLE 69 + + with the description + + EX_UNAVAILABLE -- A service is unavailable. This can occur + if a support program or file does not exist. This + can also be used as a catchall message when something + you wanted to do doesn't work, but you don't know why. + + However, this can be confused with a command that actually returns 69 because + something *it* wanted is unavailable. At release 4.21, Exim was changed to + use return code 127 instead, because this is what the shell returns when it + is unable to exec a command. We define it as EX_EXECFAILED, and use it in + child.c to signal execve() failure and other unexpected failures such as + setuid() not working - though that won't be the case here because we aren't + changing uid. */ + + else + { + /* Always handle execve() failure specially if requested to */ + + if (ob->freeze_exec_fail && (rc == EX_EXECFAILED)) + { + addr->transport_return = DEFER; + addr->special_action = SPECIAL_FREEZE; + addr->message = string_sprintf("pipe process failed to exec \"%s\"", + cmd); + } + + /* Otherwise take action only if not ignoring status */ + + else if (!ob->ignore_status) + { + uschar *ss; + int size, ptr, i; + + /* If temp_errors is "*" all codes are temporary. Initializion checks + that it's either "*" or a list of numbers. If not "*", scan the list of + temporary failure codes; if any match, the result is DEFER. */ + + if (ob->temp_errors[0] == '*') + addr->transport_return = DEFER; + + else + { + uschar *s = ob->temp_errors; + uschar *p; + uschar buffer[64]; + int sep = 0; + + addr->transport_return = FAIL; + while ((p = string_nextinlist(&s,&sep,buffer,sizeof(buffer))) != NULL) + { + if (rc == Uatoi(p)) { addr->transport_return = DEFER; break; } + } + } + + /* Ensure the message contains the expanded command and arguments. This + doesn't have to be brilliantly efficient - it is an error situation. */ + + addr->message = string_sprintf("Child process of %s transport returned " + "%d", tblock->name, rc); + + ptr = Ustrlen(addr->message); + size = ptr + 1; + + /* If the return code is > 128, it often means that a shell command + was terminated by a signal. */ + + ss = (rc > 128)? + string_sprintf("(could mean shell command ended by signal %d (%s))", + rc-128, os_strsignal(rc-128)) : + US os_strexit(rc); + + if (*ss != 0) + { + addr->message = string_cat(addr->message, &size, &ptr, US" ", 1); + addr->message = string_cat(addr->message, &size, &ptr, + ss, Ustrlen(ss)); + } + + /* Now add the command and arguments */ + + addr->message = string_cat(addr->message, &size, &ptr, + US" from command:", 14); + + for (i = 0; i < sizeof(argv)/sizeof(int *) && argv[i] != NULL; i++) + { + BOOL quote = FALSE; + addr->message = string_cat(addr->message, &size, &ptr, US" ", 1); + if (Ustrpbrk(argv[i], " \t") != NULL) + { + quote = TRUE; + addr->message = string_cat(addr->message, &size, &ptr, US"\"", 1); + } + addr->message = string_cat(addr->message, &size, &ptr, argv[i], + Ustrlen(argv[i])); + if (quote) + addr->message = string_cat(addr->message, &size, &ptr, US"\"", 1); + } + addr->message[ptr] = 0; /* Ensure concatenated string terminated */ + } + } + } + +/* Ensure all subprocesses (in particular, the output handling process) +are complete before we pass this point. */ + +while (wait(&rc) >= 0); + +DEBUG(D_transport) debug_printf("%s transport yielded %d\n", tblock->name, + addr->transport_return); + +/* If there has been a problem, the message in addr->message contains details +of the pipe command. We don't want to expose these to the world, so we set up +something bland to return to the sender. */ + +if (addr->transport_return != OK) + addr->user_message = US"local delivery failed"; + +return FALSE; +} + +/* End of transport/pipe.c */ diff --git a/src/src/transports/pipe.h b/src/src/transports/pipe.h new file mode 100644 index 000000000..d5544afd2 --- /dev/null +++ b/src/src/transports/pipe.h @@ -0,0 +1,48 @@ +/* $Cambridge: exim/src/src/transports/pipe.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *cmd; + uschar *allow_commands; + uschar *environment; + uschar *path; + uschar *message_prefix; + uschar *message_suffix; + uschar *temp_errors; + uschar *check_string; + uschar *escape_string; + int umask; + int max_output; + int timeout; + int options; + BOOL freeze_exec_fail; + BOOL ignore_status; + BOOL restrict_to_path; + BOOL use_shell; + BOOL use_bsmtp; + BOOL use_crlf; +} pipe_transport_options_block; + +/* Data for reading the private options. */ + +extern optionlist pipe_transport_options[]; +extern int pipe_transport_options_count; + +/* Block containing default values. */ + +extern pipe_transport_options_block pipe_transport_option_defaults; + +/* The main and init entry points for the transport */ + +extern BOOL pipe_transport_entry(transport_instance *, address_item *); +extern void pipe_transport_init(transport_instance *); + +/* End of transports/pipe.h */ diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c new file mode 100644 index 000000000..71fca3a10 --- /dev/null +++ b/src/src/transports/smtp.c @@ -0,0 +1,2746 @@ +/* $Cambridge: exim/src/src/transports/smtp.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "smtp.h" + +#define PENDING 256 +#define PENDING_DEFER (PENDING + DEFER) +#define PENDING_OK (PENDING + OK) + + +/* Options specific to the smtp transport. This transport also supports LMTP +over TCP/IP. The options must be in alphabetic order (note that "_" comes +before the lower case letters). Some live in the transport_instance block so as +to be publicly visible; these are flagged with opt_public. */ + +optionlist smtp_transport_options[] = { + { "allow_localhost", opt_bool, + (void *)offsetof(smtp_transport_options_block, allow_localhost) }, + { "authenticated_sender", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, authenticated_sender) }, + { "command_timeout", opt_time, + (void *)offsetof(smtp_transport_options_block, command_timeout) }, + { "connect_timeout", opt_time, + (void *)offsetof(smtp_transport_options_block, connect_timeout) }, + { "connection_max_messages", opt_int | opt_public, + (void *)offsetof(transport_instance, connection_max_messages) }, + { "data_timeout", opt_time, + (void *)offsetof(smtp_transport_options_block, data_timeout) }, + { "delay_after_cutoff", opt_bool, + (void *)offsetof(smtp_transport_options_block, delay_after_cutoff) }, + { "dns_qualify_single", opt_bool, + (void *)offsetof(smtp_transport_options_block, dns_qualify_single) }, + { "dns_search_parents", opt_bool, + (void *)offsetof(smtp_transport_options_block, dns_search_parents) }, + { "fallback_hosts", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, fallback_hosts) }, + { "final_timeout", opt_time, + (void *)offsetof(smtp_transport_options_block, final_timeout) }, + { "gethostbyname", opt_bool, + (void *)offsetof(smtp_transport_options_block, gethostbyname) }, + { "helo_data", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, helo_data) }, + { "hosts", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, hosts) }, + { "hosts_avoid_esmtp", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, hosts_avoid_esmtp) }, + #ifdef SUPPORT_TLS + { "hosts_avoid_tls", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, hosts_avoid_tls) }, + #endif + { "hosts_max_try", opt_int, + (void *)offsetof(smtp_transport_options_block, hosts_max_try) }, + #ifdef SUPPORT_TLS + { "hosts_nopass_tls", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, hosts_nopass_tls) }, + #endif + { "hosts_override", opt_bool, + (void *)offsetof(smtp_transport_options_block, hosts_override) }, + { "hosts_randomize", opt_bool, + (void *)offsetof(smtp_transport_options_block, hosts_randomize) }, + { "hosts_require_auth", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, hosts_require_auth) }, + #ifdef SUPPORT_TLS + { "hosts_require_tls", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, hosts_require_tls) }, + #endif + { "hosts_try_auth", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, hosts_try_auth) }, + { "interface", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, interface) }, + { "keepalive", opt_bool, + (void *)offsetof(smtp_transport_options_block, keepalive) }, + { "max_rcpt", opt_int | opt_public, + (void *)offsetof(transport_instance, max_addresses) }, + { "multi_domain", opt_bool | opt_public, + (void *)offsetof(transport_instance, multi_domain) }, + { "port", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, port) }, + { "protocol", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, protocol) }, + { "retry_include_ip_address", opt_bool, + (void *)offsetof(smtp_transport_options_block, retry_include_ip_address) }, + { "serialize_hosts", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, serialize_hosts) }, + { "size_addition", opt_int, + (void *)offsetof(smtp_transport_options_block, size_addition) } + #ifdef SUPPORT_TLS + ,{ "tls_certificate", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, tls_certificate) }, + { "tls_crl", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, tls_crl) }, + { "tls_privatekey", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, tls_privatekey) }, + { "tls_require_ciphers", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, tls_require_ciphers) }, + { "tls_tempfail_tryclear", opt_bool, + (void *)offsetof(smtp_transport_options_block, tls_tempfail_tryclear) }, + { "tls_verify_certificates", opt_stringptr, + (void *)offsetof(smtp_transport_options_block, tls_verify_certificates) } + #endif +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int smtp_transport_options_count = + sizeof(smtp_transport_options)/sizeof(optionlist); + +/* Default private options block for the smtp transport. */ + +smtp_transport_options_block smtp_transport_option_defaults = { + NULL, /* hosts */ + NULL, /* fallback_hosts */ + NULL, /* hostlist */ + NULL, /* fallback_hostlist */ + NULL, /* authenticated_sender */ + US"$primary_hostname", /* helo_data */ + NULL, /* interface */ + NULL, /* port */ + US"smtp", /* protocol */ + NULL, /* serialize_hosts */ + NULL, /* hosts_try_auth */ + NULL, /* hosts_require_auth */ + NULL, /* hosts_require_tls */ + NULL, /* hosts_avoid_tls */ + NULL, /* hosts_avoid_esmtp */ + NULL, /* hosts_nopass_tls */ + 5*60, /* command_timeout */ + 5*60, /* connect_timeout; shorter system default overrides */ + 5*60, /* data timeout */ + 10*60, /* final timeout */ + 1024, /* size_addition */ + 5, /* hosts_max_try */ + FALSE, /* allow_localhost */ + FALSE, /* gethostbyname */ + TRUE, /* dns_qualify_single */ + FALSE, /* dns_search_parents */ + TRUE, /* delay_after_cutoff */ + FALSE, /* hosts_override */ + FALSE, /* hosts_randomize */ + TRUE, /* keepalive */ + TRUE /* retry_include_ip_address */ + #ifdef SUPPORT_TLS + ,NULL, /* tls_certificate */ + NULL, /* tls_crl */ + NULL, /* tls_privatekey */ + NULL, /* tls_require_ciphers */ + NULL, /* tls_verify_certificates */ + TRUE /* tls_tempfail_tryclear */ + #endif +}; + + +/* Local statics */ + +static uschar *smtp_command; /* Points to last cmd for error messages */ +static uschar *mail_command; /* Points to MAIL cmd for error messages */ + + +/************************************************* +* Setup entry point * +*************************************************/ + +/* This function is called when the transport is about to be used, +but before running it in a sub-process. It is used for two things: + + (1) To set the fallback host list in addresses, when delivering. + (2) To pass back the interface, port, and protocol options, for use during + callout verification. + +Arguments: + tblock pointer to the transport instance block + addrlist list of addresses about to be transported + tf if not NULL, pointer to block in which to return options + errmsg place for error message (not used) + +Returns: OK always (FAIL, DEFER not used) +*/ + +static int +smtp_transport_setup(transport_instance *tblock, address_item *addrlist, + transport_feedback *tf, uschar **errmsg) +{ +smtp_transport_options_block *ob = + (smtp_transport_options_block *)(tblock->options_block); + +errmsg = errmsg; /* Keep picky compilers happy */ + +/* Pass back options if required. This interface is getting very messy. */ + +if (tf != NULL) + { + tf->interface = ob->interface; + tf->port = ob->port; + tf->protocol = ob->protocol; + tf->hosts = ob->hosts; + tf->hosts_override = ob->hosts_override; + tf->hosts_randomize = ob->hosts_randomize; + tf->gethostbyname = ob->gethostbyname; + tf->qualify_single = ob->dns_qualify_single; + tf->search_parents = ob->dns_search_parents; + } + +/* Set the fallback host list for all the addresses that don't have fallback +host lists, provided that the local host wasn't present in the original host +list. */ + +if (!testflag(addrlist, af_local_host_removed)) + { + for (; addrlist != NULL; addrlist = addrlist->next) + if (addrlist->fallback_hosts == NULL) + addrlist->fallback_hosts = ob->fallback_hostlist; + } + +return OK; +} + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. + +Argument: pointer to the transport instance block +Returns: nothing +*/ + +void +smtp_transport_init(transport_instance *tblock) +{ +smtp_transport_options_block *ob = + (smtp_transport_options_block *)(tblock->options_block); + +/* Retry_use_local_part defaults FALSE if unset */ + +if (tblock->retry_use_local_part == TRUE_UNSET) + tblock->retry_use_local_part = FALSE; + +/* Set the default port according to the protocol */ + +if (ob->port == NULL) + ob->port = (strcmpic(ob->protocol, US"lmtp") == 0)? US"lmtp" : US"smtp"; + +/* Set up the setup entry point, to be called before subprocesses for this +transport. */ + +tblock->setup = smtp_transport_setup; + +/* Complain if any of the timeouts are zero. */ + +if (ob->command_timeout <= 0 || ob->data_timeout <= 0 || + ob->final_timeout <= 0) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "command, data, or final timeout value is zero for %s transport", + tblock->name); + +/* If hosts_override is set and there are local hosts, set the global +flag that stops verify from showing router hosts. */ + +if (ob->hosts_override && ob->hosts != NULL) tblock->overrides_hosts = TRUE; + +/* If there are any fallback hosts listed, build a chain of host items +for them, but do not do any lookups at this time. */ + +host_build_hostlist(&(ob->fallback_hostlist), ob->fallback_hosts, FALSE); +} + + + + + +/************************************************* +* Set delivery info into all active addresses * +*************************************************/ + +/* Only addresses whose status is >= PENDING are relevant. A lesser +status means that an address is not currently being processed. + +Arguments: + addrlist points to a chain of addresses + errno_value to put in each address's errno field + msg to put in each address's message field + rc to put in each address's transport_return field + +If errno_value has the special value ERRNO_CONNECTTIMEOUT, ETIMEDOUT is put in +the errno field, and RTEF_CTOUT is ORed into the more_errno field, to indicate +this particular type of timeout. + +Returns: nothing +*/ + +static +void set_errno(address_item *addrlist, int errno_value, uschar *msg, int rc) +{ +address_item *addr; +int orvalue = 0; +if (errno_value == ERRNO_CONNECTTIMEOUT) + { + errno_value = ETIMEDOUT; + orvalue = RTEF_CTOUT; + } +for (addr = addrlist; addr != NULL; addr = addr->next) + { + if (addr->transport_return < PENDING) continue; + addr->basic_errno = errno_value; + addr->more_errno |= orvalue; + if (msg != NULL) addr->message = msg; + addr->transport_return = rc; + } +} + + + +/************************************************* +* Check an SMTP response * +*************************************************/ + +/* This function is given an errno code and the SMTP response buffer +to analyse, together with the host identification for generating messages. It +sets an appropriate message and puts the first digit of the response code into +the yield variable. If no response was actually read, a suitable digit is +chosen. + +Arguments: + host the current host, to get its name for messages + errno_value pointer to the errno value + more_errno from the top address for use with ERRNO_FILTER_FAIL + buffer the SMTP response buffer + yield where to put a one-digit SMTP response code + message where to put an errror message + +Returns: TRUE if an SMTP "QUIT" command should be sent, else FALSE +*/ + +static BOOL check_response(host_item *host, int *errno_value, int more_errno, + uschar *buffer, int *yield, uschar **message) +{ +uschar *pl = US""; + +if (smtp_use_pipelining && + (Ustrcmp(smtp_command, "MAIL") == 0 || + Ustrcmp(smtp_command, "RCPT") == 0 || + Ustrcmp(smtp_command, "DATA") == 0)) + pl = US"pipelined "; + +*yield = '4'; /* Default setting is to give a temporary error */ + +/* Handle response timeout */ + +if (*errno_value == ETIMEDOUT) + { + *message = US string_sprintf("SMTP timeout while connected to %s [%s] " + "after %s%s", host->name, host->address, pl, smtp_command); + if (transport_count > 0) + *message = US string_sprintf("%s (%d bytes written)", *message, + transport_count); + return FALSE; + } + +/* Handle malformed SMTP response */ + +if (*errno_value == ERRNO_SMTPFORMAT) + { + uschar *malfresp = string_printing(buffer); + while (isspace(*malfresp)) malfresp++; + if (*malfresp == 0) + *message = string_sprintf("Malformed SMTP reply (an empty line) from " + "%s [%s] in response to %s%s", host->name, host->address, pl, + smtp_command); + else + *message = string_sprintf("Malformed SMTP reply from %s [%s] in response " + "to %s%s: %s", host->name, host->address, pl, smtp_command, malfresp); + return FALSE; + } + +/* Handle a failed filter process error; can't send QUIT as we mustn't +end the DATA. */ + +if (*errno_value == ERRNO_FILTER_FAIL) + { + *message = US string_sprintf("transport filter process failed (%d)", + more_errno); + return FALSE; + } + +/* Handle a failed add_headers expansion; can't send QUIT as we mustn't +end the DATA. */ + +if (*errno_value == ERRNO_CHHEADER_FAIL) + { + *message = + US string_sprintf("failed to expand headers_add or headers_remove: %s", + expand_string_message); + return FALSE; + } + +/* Handle failure to write a complete data block */ + +if (*errno_value == ERRNO_WRITEINCOMPLETE) + { + *message = US string_sprintf("failed to write a data block"); + return FALSE; + } + +/* Handle error responses from the remote mailer. */ + +if (buffer[0] != 0) + { + uschar *s = string_printing(buffer); + *message = US string_sprintf("SMTP error from remote mailer after %s%s: " + "host %s [%s]: %s", pl, smtp_command, host->name, host->address, s); + *yield = buffer[0]; + return TRUE; + } + +/* No data was read. If there is no errno, this must be the EOF (i.e. +connection closed) case, which causes deferral. Otherwise, put the host's +identity in the message, leaving the errno value to be interpreted as well. In +all cases, we have to assume the connection is now dead. */ + +if (*errno_value == 0) + { + *errno_value = ERRNO_SMTPCLOSED; + *message = US string_sprintf("Remote host %s [%s] closed connection " + "in response to %s%s", host->name, host->address, pl, smtp_command); + } +else *message = US string_sprintf("%s [%s]", host->name, host->address); + +return FALSE; +} + + + +/************************************************* +* Write error message to logs * +*************************************************/ + +/* This writes to the main log and to the message log. + +Arguments: + addr the address item containing error information + host the current host + +Returns: nothing +*/ + +static void +write_logs(address_item *addr, host_item *host) +{ +if (addr->message != NULL) + { + uschar *message = addr->message; + if (addr->basic_errno > 0) + message = string_sprintf("%s: %s", message, strerror(addr->basic_errno)); + log_write(0, LOG_MAIN, "%s", message); + deliver_msglog("%s %s\n", tod_stamp(tod_log), message); + } +else + { + log_write(0, LOG_MAIN, "%s [%s]: %s", + host->name, + host->address, + strerror(addr->basic_errno)); + deliver_msglog("%s %s [%s]: %s\n", + tod_stamp(tod_log), + host->name, + host->address, + strerror(addr->basic_errno)); + } +} + + + +/************************************************* +* Synchronize SMTP responses * +*************************************************/ + +/* This function is called from smtp_deliver() to receive SMTP responses from +the server, and match them up with the commands to which they relate. When +PIPELINING is not in use, this function is called after every command, and is +therefore somewhat over-engineered, but it is simpler to use a single scheme +that works both with and without PIPELINING instead of having two separate sets +of code. + +The set of commands that are buffered up with pipelining may start with MAIL +and may end with DATA; in between are RCPT commands that correspond to the +addresses whose status is PENDING_DEFER. All other commands (STARTTLS, AUTH, +etc.) are never buffered. + +Errors after MAIL or DATA abort the whole process leaving the response in the +buffer. After MAIL, pending responses are flushed, and the original command is +re-instated in big_buffer for error messages. For RCPT commands, the remote is +permitted to reject some recipient addresses while accepting others. However +certain errors clearly abort the whole process. Set the value in +transport_return to PENDING_OK if the address is accepted. If there is a +subsequent general error, it will get reset accordingly. If not, it will get +converted to OK at the end. + +Arguments: + addrlist the complete address list + include_affixes TRUE if affixes include in RCPT + sync_addr ptr to the ptr of the one to start scanning at (updated) + host the host we are connected to + count the number of responses to read + pending_MAIL true if the first response is for MAIL + pending_DATA 0 if last command sent was not DATA + +1 if previously had a good recipient + -1 if not previously had a good recipient + inblock incoming SMTP block + timeout timeout value + buffer buffer for reading response + buffsize size of buffer + +Returns: 3 if at least one address had 2xx and one had 5xx + 2 if at least one address had 5xx but none had 2xx + 1 if at least one host had a 2xx response, but none had 5xx + 0 no address had 2xx or 5xx but no errors (all 4xx, or just DATA) + -1 timeout while reading RCPT response + -2 I/O or other non-response error for RCPT + -3 DATA or MAIL failed - errno and buffer set +*/ + +static int +sync_responses(address_item *addrlist, BOOL include_affixes, + address_item **sync_addr, host_item *host, int count, BOOL pending_MAIL, + int pending_DATA, smtp_inblock *inblock, int timeout, uschar *buffer, + int buffsize) +{ +address_item *addr = *sync_addr; +int yield = 0; + +/* Handle the response for a MAIL command. On error, reinstate the original +command in big_buffer for error message use, and flush any further pending +responses before returning, except after I/O errors and timeouts. */ + +if (pending_MAIL) + { + count--; + if (!smtp_read_response(inblock, buffer, buffsize, '2', timeout)) + { + Ustrcpy(big_buffer, mail_command); /* Fits, because it came from there! */ + if (errno == 0 && buffer[0] != 0) + { + uschar flushbuffer[4096]; + while (count-- > 0) + { + if (!smtp_read_response(inblock, flushbuffer, sizeof(flushbuffer), + '2', timeout) + && (errno != 0 || flushbuffer[0] == 0)) + break; + } + } + return -3; + } + } + +if (pending_DATA) count--; /* Number of RCPT responses to come */ + +/* Read and handle the required number of RCPT responses, matching each one up +with an address by scanning for the next address whose status is PENDING_DEFER. +*/ + +while (count-- > 0) + { + while (addr->transport_return != PENDING_DEFER) addr = addr->next; + + /* The address was accepted */ + + if (smtp_read_response(inblock, buffer, buffsize, '2', timeout)) + { + yield |= 1; + addr->transport_return = PENDING_OK; + + /* If af_dr_retry_exists is set, there was a routing delay on this address; + ensure that any address-specific retry record is expunged. */ + + if (testflag(addr, af_dr_retry_exists)) + retry_add_item(addr, addr->address_retry_key, rf_delete); + } + + /* Timeout while reading the response */ + + else if (errno == ETIMEDOUT) + { + int save_errno = errno; + uschar *message = string_sprintf("SMTP timeout while connected to %s [%s] " + "after RCPT TO:<%s>", host->name, host->address, + transport_rcpt_address(addr, include_affixes)); + set_errno(addrlist, save_errno, message, DEFER); + retry_add_item(addr, addr->address_retry_key, 0); + host->update_waiting = FALSE; + return -1; + } + + /* Handle other errors in obtaining an SMTP response by returning -1. This + will cause all the addresses to be deferred. Restore the SMTP command in + big_buffer for which we are checking the response, so the error message + makes sense. */ + + else if (errno != 0 || buffer[0] == 0) + { + string_format(big_buffer, big_buffer_size, "RCPT TO:<%s>", + transport_rcpt_address(addr, include_affixes)); + return -2; + } + + /* Handle SMTP permanent and temporary response codes. */ + + else + { + addr->message = + string_sprintf("SMTP error from remote mailer after RCPT TO:<%s>: " + "host %s [%s]: %s", transport_rcpt_address(addr, include_affixes), + host->name, host->address, string_printing(buffer)); + deliver_msglog("%s %s\n", tod_stamp(tod_log), addr->message); + + /* The response was 5xx */ + + if (buffer[0] == '5') + { + addr->transport_return = FAIL; + yield |= 2; + } + + /* The response was 4xx */ + + else + { + int bincode = (buffer[1] - '0')*10 + buffer[2] - '0'; + + addr->transport_return = DEFER; + addr->basic_errno = ERRNO_RCPT4XX; + addr->more_errno |= bincode << 8; + + /* Log temporary errors if there are more hosts to be tried. */ + + if (host->next != NULL) log_write(0, LOG_MAIN, "%s", addr->message); + + /* Do not put this message on the list of those waiting for this host, + as otherwise it is likely to be tried too often. */ + + host->update_waiting = FALSE; + + /* Add a retry item for the address so that it doesn't get tried + again too soon. */ + + retry_add_item(addr, addr->address_retry_key, 0); + } + } + } /* Loop for next RCPT response */ + +/* Update where to start at for the next block of responses, unless we +have already handled all the addresses. */ + +if (addr != NULL) *sync_addr = addr->next; + +/* Handle a response to DATA. If we have not had any good recipients, either +previously or in this block, the response is ignored. */ + +if (pending_DATA != 0 && + !smtp_read_response(inblock, buffer, buffsize, '3', timeout)) + { + int code; + uschar *msg; + if (pending_DATA > 0 || (yield & 1) != 0) return -3; + (void)check_response(host, &errno, 0, buffer, &code, &msg); + DEBUG(D_transport) debug_printf("%s\nerror for DATA ignored: pipelining " + "is in use and there were no good recipients\n", msg); + } + +/* All responses read and handled; MAIL (if present) received 2xx and DATA (if +present) received 3xx. If any RCPTs were handled and yielded anything other +than 4xx, yield will be set non-zero. */ + +return yield; +} + + + +/************************************************* +* Deliver address list to given host * +*************************************************/ + +/* If continue_hostname is not null, we get here only when continuing to +deliver down an existing channel. The channel was passed as the standard +input. + +Otherwise, we have to make a connection to the remote host, and do the +initial protocol exchange. + +When running as an MUA wrapper, if the sender or any recipient is rejected, +temporarily or permanently, we force failure for all recipients. + +Arguments: + addrlist chain of potential addresses to deliver; only those whose + transport_return field is set to PENDING_DEFER are currently + being processed; others should be skipped - they have either + been delivered to an earlier host or IP address, or been + failed by one of them. + host host to deliver to + host_af AF_INET or AF_INET6 + port TCP/IP port to use, in host byte order + interface interface to bind to, or NULL + tblock transport instance block + copy_host TRUE if host set in addr->host_used must be copied, because + it is specific to this call of the transport + message_defer set TRUE if yield is OK, but all addresses were deferred + because of a non-recipient, non-host failure, that is, a + 4xx response to MAIL FROM, DATA, or ".". This is a defer + that is specific to the message. + suppress_tls if TRUE, don't attempt a TLS connection - this is set for + a second attempt after TLS initialization fails + +Returns: OK - the connection was made and the delivery attempted; + the result for each address is in its data block. + DEFER - the connection could not be made, or something failed + while setting up the SMTP session, or there was a + non-message-specific error, such as a timeout. + ERROR - a filter command is specified for this transport, + and there was a problem setting it up; OR helo_data + or add_headers or authenticated_sender is specified + for this transport, and the string failed to expand +*/ + +static int +smtp_deliver(address_item *addrlist, host_item *host, int host_af, int port, + uschar *interface, transport_instance *tblock, BOOL copy_host, + BOOL *message_defer, BOOL suppress_tls) +{ +address_item *addr; +address_item *sync_addr; +address_item *first_addr = addrlist; +int yield = OK; +int address_count; +int save_errno; +int rc; +time_t start_delivery_time = time(NULL); +smtp_transport_options_block *ob = + (smtp_transport_options_block *)(tblock->options_block); +BOOL lmtp = strcmpic(ob->protocol, US"lmtp") == 0; +BOOL ok = FALSE; +BOOL send_rset = TRUE; +BOOL send_quit = TRUE; +BOOL setting_up = TRUE; +BOOL completed_address = FALSE; +BOOL esmtp = TRUE; +BOOL pending_MAIL; +smtp_inblock inblock; +smtp_outblock outblock; +int max_rcpt = tblock->max_addresses; +uschar *local_authenticated_sender = authenticated_sender; +uschar *helo_data; +uschar *message = NULL; +uschar new_message_id[MESSAGE_ID_LENGTH + 1]; +uschar *p; +uschar buffer[4096]; +uschar inbuffer[4096]; +uschar outbuffer[1024]; + +suppress_tls = suppress_tls; /* stop compiler warning when no TLS support */ + +*message_defer = FALSE; +smtp_command = US"initial connection"; +if (max_rcpt == 0) max_rcpt = 999999; + +/* Set up the buffer for reading SMTP response packets. */ + +inblock.buffer = inbuffer; +inblock.buffersize = sizeof(inbuffer); +inblock.ptr = inbuffer; +inblock.ptrend = inbuffer; + +/* Set up the buffer for holding SMTP commands while pipelining */ + +outblock.buffer = outbuffer; +outblock.buffersize = sizeof(outbuffer); +outblock.ptr = outbuffer; +outblock.cmd_count = 0; +outblock.authenticating = FALSE; + +/* Expand the greeting message */ + +helo_data = expand_string(ob->helo_data); +if (helo_data == NULL) + { + uschar *message = string_sprintf("failed to expand helo_data: %s", + expand_string_message); + set_errno(addrlist, 0, message, DEFER); + return ERROR; + } + +/* If an authenticated_sender override has been specified for this transport +instance, expand it. If the expansion is forced to fail, and there was already +an authenticated_sender for this message, the original value will be used. +Other expansion failures are serious. An empty result is ignored, but there is +otherwise no check - this feature is expected to be used with LMTP and other +cases where non-standard addresses (e.g. without domains) might be required. */ + +if (ob->authenticated_sender != NULL) + { + uschar *new = expand_string(ob->authenticated_sender); + if (new == NULL) + { + if (!expand_string_forcedfail) + { + uschar *message = string_sprintf("failed to expand " + "authenticated_sender: %s", expand_string_message); + set_errno(addrlist, 0, message, DEFER); + return ERROR; + } + } + else if (new[0] != 0) local_authenticated_sender = new; + } + +/* Make a connection to the host if this isn't a continued delivery, and handle +the initial interaction and HELO/EHLO/LHLO. Connect timeout errors are handled +specially so they can be identified for retries. */ + +if (continue_hostname == NULL) + { + inblock.sock = outblock.sock = + smtp_connect(host, host_af, port, interface, ob->connect_timeout, + ob->keepalive); + if (inblock.sock < 0) + { + set_errno(addrlist, (errno == ETIMEDOUT)? ERRNO_CONNECTTIMEOUT : errno, + NULL, DEFER); + return DEFER; + } + + /* The first thing is to wait for an initial OK response. The dreaded "goto" + is nevertheless a reasonably clean way of programming this kind of logic, + where you want to escape on any error. */ + + if (!smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout)) goto RESPONSE_FAILED; + +/** Debugging without sending a message +addrlist->transport_return = DEFER; +goto SEND_QUIT; +**/ + + /* Errors that occur after this point follow an SMTP command, which is + left in big_buffer by smtp_write_command() for use in error messages. */ + + smtp_command = big_buffer; + + /* Tell the remote who we are... + + February 1998: A convention has evolved that ESMTP-speaking MTAs include the + string "ESMTP" in their greeting lines, so make Exim send EHLO if the + greeting is of this form. The assumption was that the far end supports it + properly... but experience shows that there are some that give 5xx responses, + even though the banner includes "ESMTP" (there's a bloody-minded one that + says "ESMTP not spoken here"). Cope with that case. + + September 2000: Time has passed, and it seems reasonable now to always send + EHLO at the start. It is also convenient to make the change while installing + the TLS stuff. + + July 2003: Joachim Wieland met a broken server that advertises "PIPELINING" + but times out after sending MAIL FROM, RCPT TO and DATA all together. There + would be no way to send out the mails, so there is now a host list + "hosts_avoid_esmtp" that disables ESMTP for special hosts and solves the + PIPELINING problem as well. Maybe it can also be useful to cure other + problems with broken servers. + + Exim originally sent "Helo" at this point and ran for nearly a year that way. + Then somebody tried it with a Microsoft mailer... It seems that all other + mailers use upper case for some reason (the RFC is quite clear about case + independence) so, for peace of mind, I gave in. */ + + esmtp = verify_check_this_host(&(ob->hosts_avoid_esmtp), NULL, + host->name, host->address, NULL) != OK; + + if (esmtp) + { + if (smtp_write_command(&outblock, FALSE, "%s %s\r\n", + lmtp? "LHLO" : "EHLO", helo_data) < 0) + goto SEND_FAILED; + if (!smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout)) + { + if (errno != 0 || buffer[0] == 0 || lmtp) goto RESPONSE_FAILED; + esmtp = FALSE; + } + } + else + { + DEBUG(D_transport) + debug_printf("not sending EHLO (host matches hosts_avoid_esmtp)\n"); + } + + if (!esmtp) + { + if (smtp_write_command(&outblock, FALSE, "HELO %s\r\n", helo_data) < 0) + goto SEND_FAILED; + if (!smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout)) goto RESPONSE_FAILED; + } + + /* Set tls_offered if the response to EHLO specifies support for STARTTLS. */ + + #ifdef SUPPORT_TLS + tls_offered = esmtp && + pcre_exec(regex_STARTTLS, NULL, CS buffer, Ustrlen(buffer), 0, + PCRE_EOPT, NULL, 0) >= 0; + #endif + } + +/* For continuing deliveries down the same channel, the socket is the standard +input, and we don't need to redo EHLO here (but may need to do so for TLS - see +below). Set up the pointer to where subsequent commands will be left, for +error messages. Note that smtp_use_size and smtp_use_pipelining will have been +set from the command line if they were set in the process that passed the +connection on. */ + +else + { + inblock.sock = outblock.sock = fileno(stdin); + smtp_command = big_buffer; + } + +/* If TLS is available on this connection, whether continued or not, attempt to +start up a TLS session, unless the host is in hosts_avoid_tls. If successful, +send another EHLO - the server may give a different answer in secure mode. We +use a separate buffer for reading the response to STARTTLS so that if it is +negative, the original EHLO data is available for subsequent analysis, should +the client not be required to use TLS. If the response is bad, copy the buffer +for error analysis. */ + +#ifdef SUPPORT_TLS +if (tls_offered && !suppress_tls && + verify_check_this_host(&(ob->hosts_avoid_tls), NULL, host->name, + host->address, NULL) != OK) + { + uschar buffer2[4096]; + if (smtp_write_command(&outblock, FALSE, "STARTTLS\r\n") < 0) + goto SEND_FAILED; + + /* If there is an I/O error, transmission of this message is deferred. If + there is a temporary rejection of STARRTLS and tls_tempfail_tryclear is + false, we also defer. However, if there is a temporary rejection of STARTTLS + and tls_tempfail_tryclear is true, or if there is an outright rejection of + STARTTLS, we carry on. This means we will try to send the message in clear, + unless the host is in hosts_require_tls (tested below). */ + + if (!smtp_read_response(&inblock, buffer2, sizeof(buffer2), '2', + ob->command_timeout)) + { + Ustrncpy(buffer, buffer2, sizeof(buffer)); + if (errno != 0 || buffer2[0] == 0 || + (buffer2[0] == '4' && !ob->tls_tempfail_tryclear)) + goto RESPONSE_FAILED; + } + + /* STARTTLS accepted: try to negotiate a TLS session. */ + + else + { + int rc = tls_client_start(inblock.sock, host, addrlist, + NULL, /* No DH param */ + ob->tls_certificate, + ob->tls_privatekey, + ob->tls_verify_certificates, + ob->tls_crl, + ob->tls_require_ciphers, + ob->command_timeout); + + /* TLS negotiation failed; give an error. From outside, this function may + be called again to try in clear on a new connection, if the options permit + it for this host. */ + + if (rc != OK) + { + save_errno = ERRNO_TLSFAILURE; + message = US"failure while setting up TLS session"; + send_quit = FALSE; + goto TLS_FAILED; + } + + /* TLS session is set up */ + + for (addr = addrlist; addr != NULL; addr = addr->next) + { + addr->cipher = tls_cipher; + addr->peerdn = tls_peerdn; + } + } + } + +/* If we started TLS, redo the EHLO/LHLO exchange over the secure channel. */ + +if (tls_active >= 0) + { + if (smtp_write_command(&outblock, FALSE, "%s %s\r\n", lmtp? "LHLO" : "EHLO", + helo_data) < 0) + goto SEND_FAILED; + if (!smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout)) + goto RESPONSE_FAILED; + } + +/* If the host is required to use a secure channel, ensure that we +have one. */ + +else if (verify_check_this_host(&(ob->hosts_require_tls), NULL, host->name, + host->address, NULL) == OK) + { + save_errno = ERRNO_TLSREQUIRED; + message = string_sprintf("a TLS session is required for %s [%s], but %s", + host->name, host->address, + tls_offered? "an attempt to start TLS failed" : + "the server did not offer TLS support"); + goto TLS_FAILED; + } +#endif + +/* If TLS is active, we have just started it up and re-done the EHLO command, +so its response needs to be analyzed. If TLS is not active and this is a +continued session down a previously-used socket, we haven't just done EHLO, so +we skip this. */ + +if (continue_hostname == NULL + #ifdef SUPPORT_TLS + || tls_active >= 0 + #endif + ) + { + int require_auth; + uschar *fail_reason = US"server did not advertise AUTH support"; + + /* If the response to EHLO specified support for the SIZE parameter, note + this, provided size_addition is non-negative. */ + + smtp_use_size = esmtp && ob->size_addition >= 0 && + pcre_exec(regex_SIZE, NULL, CS buffer, Ustrlen(CS buffer), 0, + PCRE_EOPT, NULL, 0) >= 0; + + /* Note whether the server supports PIPELINING. If hosts_avoid_esmtp matched + the current host, esmtp will be false, so PIPELINING can never be used. */ + + smtp_use_pipelining = esmtp && + pcre_exec(regex_PIPELINING, NULL, CS buffer, Ustrlen(CS buffer), 0, + PCRE_EOPT, NULL, 0) >= 0; + + DEBUG(D_transport) debug_printf("%susing PIPELINING\n", + smtp_use_pipelining? "" : "not "); + + /* Note if the response to EHLO specifies support for the AUTH extension. + If it has, check that this host is one we want to authenticate to, and do + the business. The host name and address must be available when the + authenticator's client driver is running. */ + + smtp_authenticated = FALSE; + require_auth = verify_check_this_host(&(ob->hosts_require_auth), NULL, + host->name, host->address, NULL); + + if (esmtp && regex_match_and_setup(regex_AUTH, buffer, 0, -1)) + { + uschar *names = string_copyn(expand_nstring[1], expand_nlength[1]); + expand_nmax = -1; /* reset */ + + /* Must not do this check until after we have saved the result of the + regex match above. */ + + if (require_auth == OK || + verify_check_this_host(&(ob->hosts_try_auth), NULL, host->name, + host->address, NULL) == OK) + { + auth_instance *au; + fail_reason = US"no common mechanisms were found"; + + DEBUG(D_transport) debug_printf("scanning authentication mechanisms\n"); + + /* Scan the configured authenticators looking for one which is configured + for use as a client and whose name matches an authentication mechanism + supported by the server. If one is found, attempt to authenticate by + calling its client function. */ + + for (au = auths; !smtp_authenticated && au != NULL; au = au->next) + { + uschar *p = names; + if (!au->client) continue; + + /* Loop to scan supported server mechanisms */ + + while (*p != 0) + { + int rc; + int len = Ustrlen(au->public_name); + while (isspace(*p)) p++; + + if (strncmpic(au->public_name, p, len) != 0 || + (p[len] != 0 && !isspace(p[len]))) + { + while (*p != 0 && !isspace(*p)) p++; + continue; + } + + /* Found data for a listed mechanism. Call its client entry. Set + a flag in the outblock so that data is overwritten after sending so + that reflections don't show it. */ + + fail_reason = US"authentication attempt(s) failed"; + outblock.authenticating = TRUE; + rc = (au->info->clientcode)(au, &inblock, &outblock, + ob->command_timeout, buffer, sizeof(buffer)); + outblock.authenticating = FALSE; + DEBUG(D_transport) debug_printf("%s authenticator yielded %d\n", + au->name, rc); + + /* A temporary authentication failure must hold up delivery to + this host. After a permanent authentication failure, we carry on + to try other authentication methods. If all fail hard, try to + deliver the message unauthenticated unless require_auth was set. */ + + switch(rc) + { + case OK: + smtp_authenticated = TRUE; /* stops the outer loop */ + break; + + /* Failure after writing a command */ + + case FAIL_SEND: + goto SEND_FAILED; + + /* Failure after reading a response */ + + case FAIL: + if (errno != 0 || buffer[0] != '5') goto RESPONSE_FAILED; + log_write(0, LOG_MAIN, "%s authenticator failed H=%s [%s] %s", + au->name, host->name, host->address, buffer); + break; + + /* Failure by some other means. In effect, the authenticator + decided it wasn't prepared to handle this case. Typically this + is the result of "fail" in an expansion string. Do we need to + log anything here? */ + + case CANCELLED: + break; + + /* Internal problem, message in buffer. */ + + case ERROR: + yield = ERROR; + set_errno(addrlist, 0, string_copy(buffer), DEFER); + goto SEND_QUIT; + } + + break; /* If not authenticated, try next authenticator */ + } /* Loop for scanning supported server mechanisms */ + } /* Loop for further authenticators */ + } + } + + /* If we haven't authenticated, but are required to, give up. */ + + if (require_auth == OK && !smtp_authenticated) + { + yield = DEFER; + set_errno(addrlist, ERRNO_AUTHFAIL, + string_sprintf("authentication required but %s", fail_reason), DEFER); + goto SEND_QUIT; + } + } + +/* The setting up of the SMTP call is now complete. Any subsequent errors are +message-specific. */ + +setting_up = FALSE; + +/* If there is a filter command specified for this transport, we can now +set it up. This cannot be done until the identify of the host is known. */ + +if (tblock->filter_command != NULL) + { + BOOL rc; + uschar buffer[64]; + sprintf(CS buffer, "%.50s transport", tblock->name); + rc = transport_set_up_command(&transport_filter_argv, tblock->filter_command, + TRUE, DEFER, addrlist, buffer, NULL); + + /* On failure, copy the error to all addresses, abandon the SMTP call, and + yield ERROR. */ + + if (!rc) + { + set_errno(addrlist->next, addrlist->basic_errno, addrlist->message, DEFER); + yield = ERROR; + goto SEND_QUIT; + } + } + + +/* For messages that have more than the maximum number of envelope recipients, +we want to send several transactions down the same SMTP connection. (See +comments in deliver.c as to how this reconciles, heuristically, with +remote_max_parallel.) This optimization was added to Exim after the following +code was already working. The simplest way to put it in without disturbing the +code was to use a goto to jump back to this point when there is another +transaction to handle. */ + +SEND_MESSAGE: +sync_addr = first_addr; +address_count = 0; +ok = FALSE; +send_rset = TRUE; +completed_address = FALSE; + + +/* Initiate a message transfer. If we know the receiving MTA supports the SIZE +qualification, send it, adding something to the message size to allow for +imprecision and things that get added en route. Exim keeps the number of lines +in a message, so we can give an accurate value for the original message, but we +need some additional to handle added headers. (Double "." characters don't get +included in the count.) */ + +p = buffer; +*p = 0; + +if (smtp_use_size) + { + sprintf(CS p, " SIZE=%d", message_size+message_linecount+ob->size_addition); + while (*p) p++; + } + +/* Add the authenticated sender address if present */ + +if (smtp_authenticated && local_authenticated_sender != NULL) + { + string_format(p, sizeof(buffer) - (p-buffer), " AUTH=%s", + auth_xtextencode(local_authenticated_sender, + Ustrlen(local_authenticated_sender))); + } + +/* From here until we send the DATA command, we can make use of PIPELINING +if the server host supports it. The code has to be able to check the responses +at any point, for when the buffer fills up, so we write it totally generally. +When PIPELINING is off, each command written reports that it has flushed the +buffer. */ + +pending_MAIL = TRUE; /* The block starts with MAIL */ + +rc = smtp_write_command(&outblock, smtp_use_pipelining, + "MAIL FROM:<%s>%s\r\n", return_path, buffer); +mail_command = string_copy(big_buffer); /* Save for later error message */ + +switch(rc) + { + case -1: /* Transmission error */ + goto SEND_FAILED; + + case +1: /* Block was sent */ + if (!smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout)) goto RESPONSE_FAILED; + pending_MAIL = FALSE; + break; + } + +/* Pass over all the relevant recipient addresses for this host, which are the +ones that have status PENDING_DEFER. If we are using PIPELINING, we can send +several before we have to read the responses for those seen so far. This +checking is done by a subroutine because it also needs to be done at the end. +Send only up to max_rcpt addresses at a time, leaving first_addr pointing to +the next one if not all are sent. + +In the MUA wrapper situation, we want to flush the PIPELINING buffer for the +last address because we want to abort if any recipients have any kind of +problem, temporary or permanent. We know that all recipient addresses will have +the PENDING_DEFER status, because only one attempt is ever made, and we know +that max_rcpt will be large, so all addresses will be done at once. */ + +for (addr = first_addr; + address_count < max_rcpt && addr != NULL; + addr = addr->next) + { + int count; + BOOL no_flush; + + if (addr->transport_return != PENDING_DEFER) continue; + + address_count++; + no_flush = smtp_use_pipelining && (!mua_wrapper || addr->next != NULL); + + /* Now send the RCPT command, and process outstanding responses when + necessary. After a timeout on RCPT, we just end the function, leaving the + yield as OK, because this error can often mean that there is a problem with + just one address, so we don't want to delay the host. */ + + count = smtp_write_command(&outblock, no_flush, "RCPT TO:<%s>\r\n", + transport_rcpt_address(addr, tblock->rcpt_include_affixes)); + if (count < 0) goto SEND_FAILED; + if (count > 0) + { + switch(sync_responses(first_addr, tblock->rcpt_include_affixes, + &sync_addr, host, count, pending_MAIL, 0, &inblock, + ob->command_timeout, buffer, sizeof(buffer))) + { + case 3: ok = TRUE; /* 2xx & 5xx => OK & progress made */ + case 2: completed_address = TRUE; /* 5xx (only) => progress made */ + break; + + case 1: ok = TRUE; /* 2xx (only) => OK, but if LMTP, */ + if (!lmtp) completed_address = TRUE; /* can't tell about progress yet */ + case 0: /* No 2xx or 5xx, but no probs */ + break; + + case -1: goto END_OFF; /* Timeout on RCPT */ + default: goto RESPONSE_FAILED; /* I/O error, or any MAIL error */ + } + pending_MAIL = FALSE; /* Dealt with MAIL */ + } + } /* Loop for next address */ + +/* If we are an MUA wrapper, abort if any RCPTs were rejected, either +permanently or temporarily. We should have flushed and synced after the last +RCPT. */ + +if (mua_wrapper) + { + address_item *badaddr; + for (badaddr = first_addr; badaddr != NULL; badaddr = badaddr->next) + { + if (badaddr->transport_return != PENDING_OK) break; + } + if (badaddr != NULL) + { + set_errno(addrlist, 0, badaddr->message, FAIL); + ok = FALSE; + } + } + +/* If ok is TRUE, we know we have got at least one good recipient, and must now +send DATA, but if it is FALSE (in the normal, non-wrapper case), we may still +have a good recipient buffered up if we are pipelining. We don't want to waste +time sending DATA needlessly, so we only send it if either ok is TRUE or if we +are pipelining. The responses are all handled by sync_responses(). */ + +if (ok || (smtp_use_pipelining && !mua_wrapper)) + { + int count = smtp_write_command(&outblock, FALSE, "DATA\r\n"); + if (count < 0) goto SEND_FAILED; + switch(sync_responses(first_addr, tblock->rcpt_include_affixes, &sync_addr, + host, count, pending_MAIL, ok? +1 : -1, &inblock, + ob->command_timeout, buffer, sizeof(buffer))) + { + case 3: ok = TRUE; /* 2xx & 5xx => OK & progress made */ + case 2: completed_address = TRUE; /* 5xx (only) => progress made */ + break; + + case 1: ok = TRUE; /* 2xx (only) => OK, but if LMTP, */ + if (!lmtp) completed_address = TRUE; /* can't tell about progress yet */ + case 0: break; /* No 2xx or 5xx, but no probs */ + + case -1: goto END_OFF; /* Timeout on RCPT */ + default: goto RESPONSE_FAILED; /* I/O error, or any MAIL/DATA error */ + } + } + +/* Save the first address of the next batch. */ + +first_addr = addr; + +/* If there were no good recipients (but otherwise there have been no +problems), just set ok TRUE, since we have handled address-specific errors +already. Otherwise, it's OK to send the message. Use the check/escape mechanism +for handling the SMTP dot-handling protocol, flagging to apply to headers as +well as body. Set the appropriate timeout value to be used for each chunk. +(Haven't been able to make it work using select() for writing yet.) */ + +if (!ok) ok = TRUE; else + { + sigalrm_seen = FALSE; + transport_write_timeout = ob->data_timeout; + smtp_command = US"sending data block"; /* For error messages */ + DEBUG(D_transport|D_v) + debug_printf(" SMTP>> writing message and terminating \".\"\n"); + transport_count = 0; + ok = transport_write_message(addrlist, inblock.sock, + topt_use_crlf | topt_end_dot | topt_escape_headers | + (tblock->body_only? topt_no_headers : 0) | + (tblock->headers_only? topt_no_body : 0) | + (tblock->return_path_add? topt_add_return_path : 0) | + (tblock->delivery_date_add? topt_add_delivery_date : 0) | + (tblock->envelope_to_add? topt_add_envelope_to : 0), + 0, /* No size limit */ + tblock->add_headers, tblock->remove_headers, + US".", US"..", /* Escaping strings */ + tblock->rewrite_rules, tblock->rewrite_existflags); + + /* transport_write_message() uses write() because it is called from other + places to write to non-sockets. This means that under some OS (e.g. Solaris) + it can exit with "Broken pipe" as its error. This really means that the + socket got closed at the far end. */ + + transport_write_timeout = 0; /* for subsequent transports */ + + /* Failure can either be some kind of I/O disaster (including timeout), + or the failure of a transport filter or the expansion of added headers. */ + + if (!ok) + { + buffer[0] = 0; /* There hasn't been a response */ + goto RESPONSE_FAILED; + } + + /* We used to send the terminating "." explicitly here, but because of + buffering effects at both ends of TCP/IP connections, you don't gain + anything by keeping it separate, so it might as well go in the final + data buffer for efficiency. This is now done by setting the topt_end_dot + flag above. */ + + smtp_command = US"end of data"; + + /* For SMTP, we now read a single response that applies to the whole message. + If it is OK, then all the addresses have been delivered. */ + + if (!lmtp) ok = smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->final_timeout); + + /* For LMTP, we get back a response for every RCPT command that we sent; + some may be accepted and some rejected. For those that get a response, their + status is fixed; any that are accepted have been handed over, even if later + responses crash - at least, that's how I read RFC 2033. + + If all went well, mark the recipient addresses as completed, record which + host/IPaddress they were delivered to, and cut out RSET when sending another + message down the same channel. Write the completed addresses to the journal + now so that they are recorded in case there is a crash of hardware or + software before the spool gets updated. Also record the final SMTP + confirmation if needed (for SMTP only). */ + + if (ok) + { + int flag = '='; + int delivery_time = (int)(time(NULL) - start_delivery_time); + int len; + host_item *thost; + uschar *conf = NULL; + send_rset = FALSE; + + /* Make a copy of the host if it is local to this invocation + of the transport. */ + + if (copy_host) + { + thost = store_get(sizeof(host_item)); + *thost = *host; + thost->name = string_copy(host->name); + thost->address = string_copy(host->address); + } + else thost = host; + + /* Set up confirmation if needed - applies only to SMTP */ + + if ((log_extra_selector & LX_smtp_confirmation) != 0 && !lmtp) + { + uschar *s = string_printing(buffer); + conf = (s == buffer)? (uschar *)string_copy(s) : s; + } + + /* Process all transported addresses - for LMTP, read a status for + each one. */ + + for (addr = addrlist; addr != first_addr; addr = addr->next) + { + if (addr->transport_return != PENDING_OK) continue; + + /* LMTP - if the response fails badly (e.g. timeout), use it for all the + remaining addresses. Otherwise, it's a return code for just the one + address. */ + + if (lmtp) + { + if (!smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->final_timeout)) + { + if (errno != 0 || buffer[0] == 0) goto RESPONSE_FAILED; + addr->message = string_sprintf("LMTP error after %s: %s", + big_buffer, string_printing(buffer)); + addr->transport_return = (buffer[0] == '5')? FAIL : DEFER; + continue; + } + completed_address = TRUE; /* NOW we can set this flag */ + } + + /* SMTP, or success return from LMTP for this address. Pass back the + actual port used. */ + + addr->transport_return = OK; + addr->more_errno = delivery_time; + thost->port = port; + addr->host_used = thost; + addr->special_action = flag; + addr->message = conf; + flag = '-'; + + /* Update the journal. For homonymic addresses, use the base address plus + the transport name. See lots of comments in deliver.c about the reasons + for the complications when homonyms are involved. Just carry on after + write error, as it may prove possible to update the spool file later. */ + + if (testflag(addr, af_homonym)) + sprintf(CS buffer, "%.500s/%s\n", addr->unique + 3, tblock->name); + else + sprintf(CS buffer, "%.500s\n", addr->unique); + + DEBUG(D_deliver) debug_printf("journalling %s", buffer); + len = Ustrlen(CS buffer); + if (write(journal_fd, buffer, len) != len) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to write journal for " + "%s: %s", buffer, strerror(errno)); + } + + /* Ensure the journal file is pushed out to disk. */ + + if (fsync(journal_fd) < 0) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to fsync journal: %s", + strerror(errno)); + } + } + + +/* Handle general (not specific to one address) failures here. The value of ok +is used to skip over this code on the falling through case. A timeout causes a +deferral. Other errors may defer or fail according to the response code, and +may set up a special errno value, e.g. after connection chopped, which is +assumed if errno == 0 and there is no text in the buffer. If control reaches +here during the setting up phase (i.e. before MAIL FROM) then always defer, as +the problem is not related to this specific message. */ + +if (!ok) + { + int code; + + RESPONSE_FAILED: + save_errno = errno; + message = NULL; + send_quit = check_response(host, &save_errno, addrlist->more_errno, + buffer, &code, &message); + goto FAILED; + + SEND_FAILED: + save_errno = errno; + code = '4'; + message = US string_sprintf("send() to %s [%s] failed: %s", + host->name, host->address, strerror(save_errno)); + send_quit = FALSE; + goto FAILED; + + /* This label is jumped to directly when a TLS negotiation has failed, + or was not done for a host for which it is required. Values will be set + in message and save_errno, and setting_up will always be true. Treat as + a temporary error. */ + + #ifdef SUPPORT_TLS + TLS_FAILED: + code = '4'; + #endif + + /* If the failure happened while setting up the call, see if the failure was + a 5xx response (this will either be on connection, or following HELO - a 5xx + after EHLO causes it to try HELO). If so, fail all addresses, as this host is + never going to accept them. For other errors during setting up (timeouts or + whatever), defer all addresses, and yield DEFER, so that the host is not + tried again for a while. */ + + FAILED: + ok = FALSE; /* For when reached by GOTO */ + + if (setting_up) + { + if (code == '5') + { + set_errno(addrlist, save_errno, message, FAIL); + } + else + { + set_errno(addrlist, save_errno, message, DEFER); + yield = DEFER; + } + } + + /* If there was an I/O error or timeout or other transportation error, + indicated by errno being non-zero, defer all addresses and yield DEFER, + except for the case of failed add_headers expansion, or a transport filter + failure, when the yield should be ERROR, to stop it trying other hosts. + + However, handle timeouts after MAIL FROM or "." and loss of connection after + "." specially. They can indicate a problem with the sender address or with + the contents of the message rather than a real error on the connection. + Therefore, treat these cases in the same way as a 4xx response. + + The following condition tests for NOT these special cases. */ + + else if (save_errno != 0 && + (save_errno != ETIMEDOUT || + (Ustrncmp(smtp_command,"MAIL",4) != 0 && + Ustrncmp(smtp_command,"end ",4) != 0)) && + (save_errno != ERRNO_SMTPCLOSED || + Ustrncmp(smtp_command,"end ",4) != 0)) + { + yield = (save_errno == ERRNO_CHHEADER_FAIL || + save_errno == ERRNO_FILTER_FAIL)? ERROR : DEFER; + set_errno(addrlist, save_errno, message, DEFER); + } + + /* Otherwise we have a message-specific error response from the remote + host. This is one of + (a) negative response or timeout after "mail from" + (b) negative response after "data" + (c) negative response or timeout or dropped connection after "." + It won't be a negative response or timeout after "rcpt to", as that is dealt + with separately above. The action in all cases is to set an appropriate + error code for all the addresses, but to leave yield set to OK because + the host itself has not failed. [It might in practice have failed for a + timeout after MAIL FROM, or "." but if so, we'll discover that at the next + delivery attempt.] For a temporary error, set the message_defer flag, and + write to the logs for information if this is not the last host. The error for + the last host will be logged as part of the address's log line. */ + + else + { + if (mua_wrapper) code = '5'; /* Force hard failure in wrapper mode */ + + set_errno(addrlist, save_errno, message, (code == '5')? FAIL : DEFER); + + /* If there's an errno, the message contains just the identity of + the host. */ + + if (code != '5') /* Anything other than 5 is treated as temporary */ + { + if (save_errno > 0) + message = US string_sprintf("%s: %s", message, strerror(save_errno)); + if (host->next != NULL) log_write(0, LOG_MAIN, "%s", message); + deliver_msglog("%s %s\n", tod_stamp(tod_log), message); + *message_defer = TRUE; + } + } + } + + +/* If all has gone well, send_quit will be set TRUE, implying we can end the +SMTP session tidily. However, if there were too many addresses to send in one +message (indicated by first_addr being non-NULL) we want to carry on with the +rest of them. Also, it is desirable to send more than one message down the SMTP +connection if there are several waiting, provided we haven't already sent so +many as to hit the configured limit. The function transport_check_waiting looks +for a waiting message and returns its id. Then transport_pass_socket tries to +set up a continued delivery by passing the socket on to another process. The +variable send_rset is FALSE if a message has just been successfully transfered. + +If we are already sending down a continued channel, there may be further +addresses not yet delivered that are aimed at the same host, but which have not +been passed in this run of the transport. In this case, continue_more will be +true, and all we should do is send RSET if necessary, and return, leaving the +channel open. + +However, if no address was disposed of, i.e. all addresses got 4xx errors, we +do not want to continue with other messages down the same channel, because that +can lead to looping between two or more messages, all with the same, +temporarily failing address(es). [The retry information isn't updated yet, so +new processes keep on trying.] We probably also don't want to try more of this +message's addresses either. + +If we have started a TLS session, we have to end it before passing the +connection to a new process. However, not all servers can handle this (Exim +can), so we do not pass such a connection on if the host matches +hosts_nopass_tls. */ + +DEBUG(D_transport) + debug_printf("ok=%d send_quit=%d send_rset=%d continue_more=%d " + "yield=%d first_address is %sNULL\n", ok, send_quit, send_rset, + continue_more, yield, (first_addr == NULL)? "":"not "); + +if (completed_address && ok && send_quit) + { + BOOL more; + if (first_addr != NULL || continue_more || + ( + (tls_active < 0 || + verify_check_this_host(&(ob->hosts_nopass_tls), NULL, host->name, + host->address, NULL) != OK) + && + transport_check_waiting(tblock->name, host->name, + tblock->connection_max_messages, new_message_id, &more) + )) + { + uschar *msg; + + if (send_rset) + { + if (! (ok = smtp_write_command(&outblock, FALSE, "RSET\r\n") >= 0)) + { + msg = US string_sprintf("send() to %s [%s] failed: %s", host->name, + host->address, strerror(save_errno)); + send_quit = FALSE; + } + else if (! (ok = smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout))) + { + int code; + send_quit = check_response(host, &errno, 0, buffer, &code, &msg); + if (!send_quit) + { + DEBUG(D_transport) debug_printf("%s\n", msg); + } + } + } + + /* Either RSET was not needed, or it succeeded */ + + if (ok) + { + if (first_addr != NULL) /* More addresses still to be sent */ + { /* in this run of the transport */ + continue_sequence++; /* Causes * in logging */ + goto SEND_MESSAGE; + } + if (continue_more) return yield; /* More addresses for another run */ + + /* Pass the socket to a new Exim process. Before doing so, we must shut + down TLS. Not all MTAs allow for the continuation of the SMTP session + when TLS is shut down. We test for this by sending a new EHLO. If we + don't get a good response, we don't attempt to pass the socket on. */ + + #ifdef SUPPORT_TLS + if (tls_active >= 0) + { + tls_close(TRUE); + ok = smtp_write_command(&outblock,FALSE,"EHLO %s\r\n",helo_data) >= 0 && + smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout); + } + #endif + + /* If the socket is successfully passed, we musn't send QUIT (or + indeed anything!) from here. */ + + if (ok && transport_pass_socket(tblock->name, host->name, host->address, + new_message_id, inblock.sock)) + { + send_quit = FALSE; + } + } + + /* If RSET failed and there are addresses left, they get deferred. */ + + else set_errno(first_addr, errno, msg, DEFER); + } + } + +/* End off tidily with QUIT unless the connection has died or the socket has +been passed to another process. There has been discussion on the net about what +to do after sending QUIT. The wording of the RFC suggests that it is necessary +to wait for a response, but on the other hand, there isn't anything one can do +with an error response, other than log it. Exim used to do that. However, +further discussion suggested that it is positively advantageous not to wait for +the response, but to close the session immediately. This is supposed to move +the TCP/IP TIME_WAIT state from the server to the client, thereby removing some +load from the server. (Hosts that are both servers and clients may not see much +difference, of course.) Further discussion indicated that this was safe to do +on Unix systems which have decent implementations of TCP/IP that leave the +connection around for a while (TIME_WAIT) after the application has gone away. +This enables the response sent by the server to be properly ACKed rather than +timed out, as can happen on broken TCP/IP implementations on other OS. + +This change is being made on 31-Jul-98. After over a year of trouble-free +operation, the old commented-out code was removed on 17-Sep-99. */ + +SEND_QUIT: +if (send_quit) (void)smtp_write_command(&outblock, FALSE, "QUIT\r\n"); + +END_OFF: + +#ifdef SUPPORT_TLS +tls_close(TRUE); +#endif + +/* Close the socket, and return the appropriate value, first setting +continue_transport and continue_hostname NULL to prevent any other addresses +that may include the host from trying to re-use a continuation socket. This +works because the NULL setting is passed back to the calling process, and +remote_max_parallel is forced to 1 when delivering over an existing connection, + +If all went well and continue_more is set, we shouldn't actually get here if +there are further addresses, as the return above will be taken. However, +writing RSET might have failed, or there may be other addresses whose hosts are +specified in the transports, and therefore not visible at top level, in which +case continue_more won't get set. */ + +close(inblock.sock); +continue_transport = NULL; +continue_hostname = NULL; +return yield; +} + + + + +/************************************************* +* Closedown entry point * +*************************************************/ + +/* This function is called when exim is passed an open smtp channel +from another incarnation, but the message which it has been asked +to deliver no longer exists. The channel is on stdin. + +We might do fancy things like looking for another message to send down +the channel, but if the one we sought has gone, it has probably been +delivered by some other process that itself will seek further messages, +so just close down our connection. + +Argument: pointer to the transport instance block +Returns: nothing +*/ + +void +smtp_transport_closedown(transport_instance *tblock) +{ +smtp_transport_options_block *ob = + (smtp_transport_options_block *)(tblock->options_block); +smtp_inblock inblock; +smtp_outblock outblock; +uschar buffer[256]; +uschar inbuffer[4096]; +uschar outbuffer[16]; + +inblock.sock = fileno(stdin); +inblock.buffer = inbuffer; +inblock.buffersize = sizeof(inbuffer); +inblock.ptr = inbuffer; +inblock.ptrend = inbuffer; + +outblock.sock = inblock.sock; +outblock.buffersize = sizeof(outbuffer); +outblock.buffer = outbuffer; +outblock.ptr = outbuffer; +outblock.cmd_count = 0; +outblock.authenticating = FALSE; + +(void)smtp_write_command(&outblock, FALSE, "QUIT\r\n"); +(void)smtp_read_response(&inblock, buffer, sizeof(buffer), '2', + ob->command_timeout); +close(inblock.sock); +} + + + +/************************************************* +* Prepare addresses for delivery * +*************************************************/ + +/* This function is called to flush out error settings from previous delivery +attempts to other hosts. It also records whether we got here via an MX record +or not in the more_errno field of the address. We are interested only in +addresses that are still marked DEFER - others may have got delivered to a +previously considered IP address. Set their status to PENDING_DEFER to indicate +which ones are relevant this time. + +Arguments: + addrlist the list of addresses + host the host we are delivering to + +Returns: the first address for this delivery +*/ + +static address_item * +prepare_addresses(address_item *addrlist, host_item *host) +{ +address_item *first_addr = NULL; +address_item *addr; +for (addr = addrlist; addr != NULL; addr = addr->next) + { + if (addr->transport_return != DEFER) continue; + if (first_addr == NULL) first_addr = addr; + addr->transport_return = PENDING_DEFER; + addr->basic_errno = 0; + addr->more_errno = (host->mx >= 0)? 'M' : 'A'; + addr->message = NULL; + #ifdef SUPPORT_TLS + addr->cipher = NULL; + addr->peerdn = NULL; + #endif + } +return first_addr; +} + + + +/************************************************* +* Main entry point * +*************************************************/ + +/* See local README for interface details. As this is a remote transport, it is +given a chain of addresses to be delivered in one connection, if possible. It +always returns TRUE, indicating that each address has its own independent +status set, except if there is a setting up problem, in which case it returns +FALSE. */ + +BOOL +smtp_transport_entry( + transport_instance *tblock, /* data for this instantiation */ + address_item *addrlist) /* addresses we are working on */ +{ +int cutoff_retry; +int port; +int hosts_defer = 0; +int hosts_fail = 0; +int hosts_looked_up = 0; +int hosts_retry = 0; +int hosts_serial = 0; +int hosts_total = 0; +address_item *addr; +BOOL expired = TRUE; +BOOL continuing = continue_hostname != NULL; +uschar *expanded_hosts = NULL; +uschar *pistring; +uschar *tid = string_sprintf("%s transport", tblock->name); +smtp_transport_options_block *ob = + (smtp_transport_options_block *)(tblock->options_block); +host_item *hostlist = addrlist->host_list; +host_item *host = NULL; + +DEBUG(D_transport) + { + debug_printf("%s transport entered\n", tblock->name); + for (addr = addrlist; addr != NULL; addr = addr->next) + debug_printf(" %s\n", addr->address); + if (continuing) debug_printf("already connected to %s [%s]\n", + continue_hostname, continue_host_address); + } + +/* If a host list is not defined for the addresses - they must all have the +same one in order to be passed to a single transport - or if the transport has +a host list with hosts_override set, use the host list supplied with the +transport. It is an error for this not to exist. */ + +if (hostlist == NULL || (ob->hosts_override && ob->hosts != NULL)) + { + if (ob->hosts == NULL) + { + addrlist->message = string_sprintf("%s transport called with no hosts set", + tblock->name); + addrlist->transport_return = PANIC; + return FALSE; /* Only top address has status */ + } + + DEBUG(D_transport) debug_printf("using the transport's hosts: %s\n", + ob->hosts); + + /* If the transport's host list contains no '$' characters, and we are not + randomizing, it is fixed and therefore a chain of hosts can be built once + and for all, and remembered for subsequent use by other calls to this + transport. If, on the other hand, the host list does contain '$', or we are + randomizing its order, we have to rebuild it each time. In the fixed case, + as the hosts string will never be used again, it doesn't matter that we + replace all the : characters with zeros. */ + + if (ob->hostlist == NULL) + { + uschar *s = ob->hosts; + + if (Ustrchr(s, '$') != NULL) + { + expanded_hosts = expand_string(s); + if (expanded_hosts == NULL) + { + addrlist->message = string_sprintf("failed to expand list of hosts " + "\"%s\" in %s transport: %s", s, tblock->name, expand_string_message); + addrlist->transport_return = search_find_defer? DEFER : PANIC; + return FALSE; /* Only top address has status */ + } + DEBUG(D_transport) debug_printf("expanded list of hosts \"%s\" to " + "\"%s\"\n", s, expanded_hosts); + s = expanded_hosts; + } + else + if (ob->hosts_randomize) s = expanded_hosts = string_copy(s); + + host_build_hostlist(&hostlist, s, ob->hosts_randomize); + + /* If there was no expansion of hosts, save the host list for + next time. */ + + if (expanded_hosts == NULL) ob->hostlist = hostlist; + } + + /* This is not the first time this transport has been run in this delivery; + the host list was built previously. */ + + else hostlist = ob->hostlist; + } + +/* The host list was supplied with the address. If hosts_randomize is set, we +must sort it into a random order if it did not come from MX records and has not +already been randomized (but don't bother if continuing down an existing +connection). */ + +else if (ob->hosts_randomize && hostlist->mx == MX_NONE && !continuing) + { + host_item *newlist = NULL; + while (hostlist != NULL) + { + host_item *h = hostlist; + hostlist = hostlist->next; + + h->sort_key = random_number(100); + + if (newlist == NULL) + { + h->next = NULL; + newlist = h; + } + else if (h->sort_key < newlist->sort_key) + { + h->next = newlist; + newlist = h; + } + else + { + host_item *hh = newlist; + while (hh->next != NULL) + { + if (h->sort_key < hh->next->sort_key) break; + hh = hh->next; + } + h->next = hh->next; + hh->next = h; + } + } + + hostlist = addrlist->host_list = newlist; + } + + +/* Sort out the port. Set up a string for adding to the retry key if the port +number is not the standard SMTP port. */ + +if (!smtp_get_port(ob->port, addrlist, &port, tid)) return FALSE; +pistring = string_sprintf(":%d", port); +if (Ustrcmp(pistring, ":25") == 0) pistring = US""; + + +/* For each host-plus-IP-address on the list: + +. If this is a continued delivery and the host isn't the one with the + current connection, skip. + +. If the status is unusable (i.e. previously failed or retry checked), skip. + +. If no IP address set, get the address, either by turning the name into + an address, calling gethostbyname if gethostbyname is on, or by calling + the DNS. The DNS may yield multiple addresses, in which case insert the + extra ones into the list. + +. Get the retry data if not previously obtained for this address and set the + field which remembers the state of this address. Skip if the retry time is + not reached. If not, remember whether retry data was found. The retry string + contains both the name and the IP address. + +. Scan the list of addresses and mark those whose status is DEFER as + PENDING_DEFER. These are the only ones that will be processed in this cycle + of the hosts loop. + +. Make a delivery attempt - addresses marked PENDING_DEFER will be tried. + Some addresses may be successfully delivered, others may fail, and yet + others may get temporary errors and so get marked DEFER. + +. The return from the delivery attempt is OK if a connection was made and a + valid SMTP dialogue was completed. Otherwise it is DEFER. + +. If OK, add a "remove" retry item for this host/IPaddress, if any. + +. If fail to connect, or other defer state, add a retry item. + +. If there are any addresses whose status is still DEFER, carry on to the + next host/IPaddress, unless we have tried the number of hosts given + by hosts_max_try; otherwise return. + +If we get to the end of the list, all hosts have deferred at least one address, +or not reached their retry times. If delay_after_cutoff is unset, it requests a +delivery attempt to those hosts whose last try was before the arrival time of +the current message. To cope with this, we have to go round the loop a second +time. After that, set the status and error data for any addresses that haven't +had it set already. */ + +for (cutoff_retry = 0; expired && + cutoff_retry < ((ob->delay_after_cutoff)? 1 : 2); + cutoff_retry++) + { + host_item *nexthost = NULL; + int unexpired_hosts_tried = 0; + + for (host = hostlist; + host != NULL && unexpired_hosts_tried < ob->hosts_max_try; + host = nexthost) + { + int rc; + int host_af; + uschar *rs; + BOOL serialized = FALSE; + BOOL host_is_expired = FALSE; + BOOL message_defer = FALSE; + BOOL ifchanges = FALSE; + BOOL some_deferred = FALSE; + address_item *first_addr = NULL; + uschar *interface = NULL; + uschar *retry_host_key = NULL; + uschar *retry_message_key = NULL; + uschar *serialize_key = NULL; + + /* Default next host is next host. :-) But this can vary if the + hosts_max_try limit is hit (see below). */ + + nexthost = host->next; + + /* Set the flag requesting that this host be added to the waiting + database if the delivery fails temporarily or if we are running with + queue_smtp or a 2-stage queue run. This gets unset for certain + kinds of error, typically those that are specific to the message. */ + + host->update_waiting = TRUE; + + /* If the address hasn't yet been obtained from the host name, look it up + now, unless the host is already marked as unusable. If it is marked as + unusable, it means that the router was unable to find its IP address (in + the DNS or wherever) OR we are in the 2nd time round the cutoff loop, and + the lookup failed last time. We don't get this far if *all* MX records + point to non-existent hosts; that is treated as a hard error. + + We can just skip this host entirely. When the hosts came from the router, + the address will timeout based on the other host(s); when the address is + looked up below, there is an explicit retry record added. + + Note that we mustn't skip unusable hosts if the address is not unset; they + may be needed as expired hosts on the 2nd time round the cutoff loop. */ + + if (host->address == NULL) + { + uschar *canonical_name; + + if (host->status >= hstatus_unusable) + { + DEBUG(D_transport) debug_printf("%s has no address and is unusable - skipping\n", + host->name); + continue; + } + + DEBUG(D_transport) debug_printf("getting address for %s\n", host->name); + + hosts_looked_up++; + + /* Find by name if so configured, or if it's an IP address. We don't + just copy the IP address, because we need the test-for-local to happen. */ + + if (ob->gethostbyname || string_is_ip_address(host->name, NULL)) + rc = host_find_byname(host, NULL, &canonical_name, TRUE); + else + { + int flags = HOST_FIND_BY_A; + if (ob->dns_qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE; + if (ob->dns_search_parents) flags |= HOST_FIND_SEARCH_PARENTS; + rc = host_find_bydns(host, NULL, flags, NULL, NULL, NULL, + &canonical_name, NULL); + } + + /* Failure to find the host at this time (usually DNS temporary failure) + is really a kind of routing failure rather than a transport failure. + Therefore we add a retry item of the routing kind, not to stop us trying + to look this name up here again, but to ensure the address gets timed + out if the failures go on long enough. A complete failure at this point + commonly points to a configuration error, but the best action is still + to carry on for the next host. */ + + if (rc == HOST_FIND_AGAIN || rc == HOST_FIND_FAILED) + { + retry_add_item(addrlist, string_sprintf("R:%s", host->name), 0); + expired = FALSE; + if (rc == HOST_FIND_AGAIN) hosts_defer++; else hosts_fail++; + DEBUG(D_transport) debug_printf("rc = %s for %s\n", (rc == HOST_FIND_AGAIN)? + "HOST_FIND_AGAIN" : "HOST_FIND_FAILED", host->name); + host->status = hstatus_unusable; + + for (addr = addrlist; addr != NULL; addr = addr->next) + { + if (addr->transport_return != DEFER) continue; + addr->basic_errno = ERRNO_UNKNOWNHOST; + addr->message = + string_sprintf("failed to lookup IP address for %s", host->name); + } + continue; + } + + /* If the host is actually the local host, we may have a problem, or + there may be some cunning configuration going on. In the problem case, + log things and give up. The default transport status is already DEFER. */ + + if (rc == HOST_FOUND_LOCAL && !ob->allow_localhost) + { + for (addr = addrlist; addr != NULL; addr = addr->next) + { + addr->basic_errno = 0; + addr->message = string_sprintf("%s transport found host %s to be " + "local", tblock->name, host->name); + } + goto END_TRANSPORT; + } + } /* End of block for IP address lookup */ + + /* If this is a continued delivery, we are interested only in the host + which matches the name of the existing open channel. The check is put + here after the local host lookup, in case the name gets expanded as a + result of the lookup. Set expired FALSE, to save the outer loop executing + twice. */ + + if (continuing && (Ustrcmp(continue_hostname, host->name) != 0 || + Ustrcmp(continue_host_address, host->address) != 0)) + { + expired = FALSE; + continue; /* With next host */ + } + + /* If queue_smtp is set (-odqs or the first part of a 2-stage run), or the + domain is in queue_smtp_domains, we don't actually want to attempt any + deliveries. When doing a queue run, queue_smtp_domains is always unset. If + there is a lookup defer in queue_smtp_domains, proceed as if the domain + were not in it. We don't want to hold up all SMTP deliveries! Except when + doing a two-stage queue run, don't do this if forcing. */ + + if ((!deliver_force || queue_2stage) && (queue_smtp || + match_isinlist(addrlist->domain, &queue_smtp_domains, 0, NULL, NULL, + MCL_DOMAIN, TRUE, NULL) == OK)) + { + expired = FALSE; + for (addr = addrlist; addr != NULL; addr = addr->next) + { + if (addr->transport_return != DEFER) continue; + addr->message = US"domain matches queue_smtp_domains, or -odqs set"; + } + continue; /* With next host */ + } + + /* Count hosts being considered - purely for an intelligent comment + if none are usable. */ + + hosts_total++; + + /* Set $host and $host address now in case they are needed for the + interface expansion or the serialize_hosts check; they remain set if an + actual delivery happens. */ + + deliver_host = host->name; + deliver_host_address = host->address; + + /* Select IPv4 or IPv6, and choose an outgoing interface. If the interface + string changes upon expansion, we must add it to the key that is used for + retries, because connections to the same host from a different interface + should be treated separately. */ + + host_af = (Ustrchr(host->address, ':') == NULL)? AF_INET : AF_INET6; + if (!smtp_get_interface(ob->interface, host_af, addrlist, &ifchanges, + &interface, tid)) + return FALSE; + if (ifchanges) pistring = string_sprintf("%s/%s", pistring, interface); + + /* The first time round the outer loop, check the status of the host by + inspecting the retry data. The second time round, we are interested only + in expired hosts that haven't been tried since this message arrived. */ + + if (cutoff_retry == 0) + { + /* Ensure the status of the address is set by checking retry data if + necessary. There maybe host-specific retry data (applicable to all + messages) and also data for retries of a specific message at this host. + If either of these retry records are actually read, the keys used are + returned to save recomputing them later. */ + + host_is_expired = retry_check_address(addrlist->domain, host, pistring, + ob->retry_include_ip_address, &retry_host_key, &retry_message_key); + + DEBUG(D_transport) debug_printf("%s [%s]%s status = %s\n", host->name, + (host->address == NULL)? US"" : host->address, pistring, + (host->status == hstatus_usable)? "usable" : + (host->status == hstatus_unusable)? "unusable" : + (host->status == hstatus_unusable_expired)? "unusable (expired)" : "?"); + + /* Skip this address if not usable at this time, noting if it wasn't + actually expired, both locally and in the address. */ + + switch (host->status) + { + case hstatus_unusable: + expired = FALSE; + setflag(addrlist, af_retry_skipped); + /* Fall through */ + + case hstatus_unusable_expired: + switch (host->why) + { + case hwhy_retry: hosts_retry++; break; + case hwhy_failed: hosts_fail++; break; + case hwhy_deferred: hosts_defer++; break; + } + + /* If there was a retry message key, implying that previously there + was a message-specific defer, we don't want to update the list of + messages waiting for this host. */ + + if (retry_message_key != NULL) host->update_waiting = FALSE; + continue; /* With the next host or IP address */ + } + } + + /* Second time round the loop: if the address is set but expired, and + the message is newer than the last try, let it through. */ + + else + { + if (host->address == NULL || + host->status != hstatus_unusable_expired || + host->last_try > received_time) + continue; + DEBUG(D_transport) + debug_printf("trying expired host %s [%s]%s\n", + host->name, host->address, pistring); + host_is_expired = TRUE; + } + + /* Setting "expired=FALSE" doesn't actually mean not all hosts are expired; + it remains TRUE only if all hosts are expired and none are actually tried. + */ + + expired = FALSE; + + /* If this host is listed as one to which access must be serialized, + see if another Exim process has a connection to it, and if so, skip + this host. If not, update the database to record our connection to it + and remember this for later deletion. Do not do any of this if we are + sending the message down a pre-existing connection. */ + + if (!continuing && + verify_check_this_host(&(ob->serialize_hosts), NULL, host->name, + host->address, NULL) == OK) + { + serialize_key = string_sprintf("host-serialize-%s", host->name); + if (!enq_start(serialize_key)) + { + DEBUG(D_transport) + debug_printf("skipping host %s because another Exim process " + "is connected to it\n", host->name); + hosts_serial++; + continue; + } + serialized = TRUE; + } + + /* OK, we have an IP address that is not waiting for its retry time to + arrive (it might be expired) OR (second time round the loop) we have an + expired host that hasn't been tried since the message arrived. Have a go + at delivering the message to it. First prepare the addresses by flushing + out the result of previous attempts, and finding the first address that + is still to be delivered. */ + + first_addr = prepare_addresses(addrlist, host); + + DEBUG(D_transport) debug_printf("delivering %s to %s [%s] (%s%s)\n", + message_id, host->name, host->address, addrlist->address, + (addrlist->next == NULL)? "" : ", ..."); + + set_process_info("delivering %s to %s [%s] (%s%s)", + message_id, host->name, host->address, addrlist->address, + (addrlist->next == NULL)? "" : ", ..."); + + /* This is not for real; don't do the delivery. If there are + any remaining hosts, list them. */ + + if (dont_deliver) + { + host_item *host2; + set_errno(addrlist, 0, NULL, OK); + for (addr = addrlist; addr != NULL; addr = addr->next) + { + addr->host_used = host; + addr->special_action = '*'; + addr->message = US"delivery bypassed by -N option"; + } + DEBUG(D_transport) + { + debug_printf("*** delivery by %s transport bypassed by -N option\n" + "*** host and remaining hosts:\n", tblock->name); + for (host2 = host; host2 != NULL; host2 = host2->next) + debug_printf(" %s [%s]\n", host2->name, + (host2->address == NULL)? US"unset" : host2->address); + } + rc = OK; + } + + /* This is for real. If the host is expired, we don't count it for + hosts_max_retry. This ensures that all hosts must expire before an address + is timed out. Otherwise, if we are about to hit the hosts_max_retry limit, + check to see if there is a subsequent hosts with a different MX value. If + so, make that the next host, and don't count this one. This is a heuristic + to make sure that different MXs do get tried. With a normal kind of retry + rule, they would get tried anyway when the earlier hosts were delayed, but + if the domain has a "retry every time" type of rule - as is often used for + the the very large ISPs, that won't happen. */ + + else + { + if (!host_is_expired && ++unexpired_hosts_tried >= ob->hosts_max_try) + { + host_item *h; + DEBUG(D_transport) + debug_printf("hosts_max_try limit reached with this host\n"); + for (h = host; h != NULL; h = h->next) + if (h->mx != host->mx) break; + if (h != NULL) + { + nexthost = h; + unexpired_hosts_tried--; + DEBUG(D_transport) debug_printf("however, a higher MX host exists " + "and will be tried\n"); + } + } + + /* Attempt the delivery. */ + + rc = smtp_deliver(addrlist, host, host_af, port, interface, tblock, + expanded_hosts != NULL, &message_defer, FALSE); + + /* Yield is one of: + OK => connection made, each address contains its result; + message_defer is set for message-specific defers (when all + recipients are marked defer) + DEFER => there was a non-message-specific delivery problem; + ERROR => there was a problem setting up the arguments for a filter, + or there was a problem with expanding added headers + */ + + /* If the result is not OK, there was a non-message-specific problem. + If the result is DEFER, we need to write to the logs saying what happened + for this particular host, except in the case of authentication and TLS + failures, where the log has already been written. If all hosts defer a + general message is written at the end. */ + + if (rc == DEFER && first_addr->basic_errno != ERRNO_AUTHFAIL && + first_addr->basic_errno != ERRNO_TLSFAILURE) + write_logs(first_addr, host); + + /* If STARTTLS was accepted, but there was a failure in setting up the + TLS session (usually a certificate screwup), and the host is not in + hosts_require_tls, and tls_tempfail_tryclear is true, try again, with + TLS forcibly turned off. We have to start from scratch with a new SMTP + connection. That's why the retry is done from here, not from within + smtp_deliver(). [Rejections of STARTTLS itself don't screw up the + session, so the in-clear transmission after those errors, if permitted, + happens inside smtp_deliver().] */ + + #ifdef SUPPORT_TLS + if (rc == DEFER && first_addr->basic_errno == ERRNO_TLSFAILURE && + ob->tls_tempfail_tryclear && + verify_check_this_host(&(ob->hosts_require_tls), NULL, host->name, + host->address, NULL) != OK) + { + log_write(0, LOG_MAIN, "TLS session failure: delivering unencrypted " + "to %s [%s] (not in hosts_require_tls)", host->name, host->address); + first_addr = prepare_addresses(addrlist, host); + rc = smtp_deliver(addrlist, host, host_af, port, interface, tblock, + expanded_hosts != NULL, &message_defer, TRUE); + if (rc == DEFER && first_addr->basic_errno != ERRNO_AUTHFAIL) + write_logs(first_addr, host); + } + #endif + } + + /* Delivery attempt finished */ + + rs = (rc == OK)? US"OK" : (rc == DEFER)? US"DEFER" : (rc == ERROR)? + US"ERROR" : US"?"; + + set_process_info("delivering %s: just tried %s [%s] for %s%s: result %s", + message_id, host->name, host->address, addrlist->address, + (addrlist->next == NULL)? "" : " (& others)", rs); + + /* Release serialization if set up */ + + if (serialized) enq_end(serialize_key); + + /* If the result is DEFER, or if a host retry record is known to exist, we + need to add an item to the retry chain for updating the retry database + at the end of delivery. We only need to add the item to the top address, + of course. Also, if DEFER, we mark the IP address unusable so as to skip it + for any other delivery attempts using the same address. (It is copied into + the unusable tree at the outer level, so even if different address blocks + contain the same address, it still won't get tried again.) */ + + if (rc == DEFER || retry_host_key != NULL) + { + int delete_flag = (rc != DEFER)? rf_delete : 0; + if (retry_host_key == NULL) + { + retry_host_key = ob->retry_include_ip_address? + string_sprintf("T:%S:%s%s", host->name, host->address, pistring) : + string_sprintf("T:%S%s", host->name, pistring); + } + + /* If a delivery of another message over an existing SMTP connection + yields DEFER, we do NOT set up retry data for the host. This covers the + case when there are delays in routing the addresses in the second message + that are so long that the server times out. This is alleviated by not + routing addresses that previously had routing defers when handling an + existing connection, but even so, this case may occur (e.g. if a + previously happily routed address starts giving routing defers). If the + host is genuinely down, another non-continued message delivery will + notice it soon enough. */ + + if (delete_flag != 0 || !continuing) + retry_add_item(first_addr, retry_host_key, rf_host | delete_flag); + + /* We may have tried an expired host, if its retry time has come; ensure + the status reflects the expiry for the benefit of any other addresses. */ + + if (rc == DEFER) + { + host->status = (host_is_expired)? + hstatus_unusable_expired : hstatus_unusable; + host->why = hwhy_deferred; + } + } + + /* If message_defer is set (host was OK, but every recipient got deferred + because of some message-specific problem), or if that had happened + previously so that a message retry key exists, add an appropriate item + to the retry chain. Note that if there was a message defer but now there is + a host defer, the message defer record gets deleted. That seems perfectly + reasonable. Also, stop the message from being remembered as waiting + for this host. */ + + if (message_defer || retry_message_key != NULL) + { + int delete_flag = message_defer? 0 : rf_delete; + if (retry_message_key == NULL) + { + retry_message_key = ob->retry_include_ip_address? + string_sprintf("T:%S:%s%s:%s", host->name, host->address, pistring, + message_id) : + string_sprintf("T:%S%s:%s", host->name, pistring, message_id); + } + retry_add_item(addrlist, retry_message_key, + rf_message | rf_host | delete_flag); + host->update_waiting = FALSE; + } + + /* Any return other than DEFER (that is, OK or ERROR) means that the + addresses have got their final statuses filled in for this host. In the OK + case, see if any of them are deferred. */ + + if (rc == OK) + { + for (addr = addrlist; addr != NULL; addr = addr->next) + { + if (addr->transport_return == DEFER) + { + some_deferred = TRUE; + break; + } + } + } + + /* If no addresses deferred or the result was ERROR, return. We do this for + ERROR because a failing filter set-up or add_headers expansion is likely to + fail for any host we try. */ + + if (rc == ERROR || (rc == OK && !some_deferred)) + { + DEBUG(D_transport) debug_printf("Leaving %s transport\n", tblock->name); + return TRUE; /* Each address has its status */ + } + + /* If the result was DEFER or some individual addresses deferred, let + the loop run to try other hosts with the deferred addresses, except for the + case when we were trying to deliver down an existing channel and failed. + Don't try any other hosts in this case. */ + + if (continuing) break; + + /* If the whole delivery, or some individual addresses, were deferred and + there are more hosts that could be tried, do not count this host towards + the hosts_max_try limit if the age of the message is greater than the + maximum retry time for this host. This means we may try try all hosts, + ignoring the limit, when messages have been around for some time. This is + important because if we don't try all hosts, the address will never time + out. */ + + if ((rc == DEFER || some_deferred) && nexthost != NULL) + { + BOOL timedout; + retry_config *retry = retry_find_config(host->name, NULL, 0, 0); + + if (retry != NULL && retry->rules != NULL) + { + retry_rule *last_rule; + for (last_rule = retry->rules; + last_rule->next != NULL; + last_rule = last_rule->next); + timedout = time(NULL) - received_time > last_rule->timeout; + } + else timedout = TRUE; /* No rule => timed out */ + + if (timedout) + { + unexpired_hosts_tried--; + DEBUG(D_transport) debug_printf("temporary delivery error(s) override " + "hosts_max_try (message older than host's retry time)\n"); + } + } + } /* End of loop for trying multiple hosts. */ + + /* This is the end of the loop that repeats iff expired is TRUE and + ob->delay_after_cutoff is FALSE. The second time round we will + try those hosts that haven't been tried since the message arrived. */ + + DEBUG(D_transport) + { + debug_printf("all IP addresses skipped or deferred at least one address\n"); + if (expired && !ob->delay_after_cutoff && cutoff_retry == 0) + debug_printf("retrying IP addresses not tried since message arrived\n"); + } + } + + +/* Get here if all IP addresses are skipped or defer at least one address. In +MUA wrapper mode, this will happen only for connection or other non-message- +specific failures. Force the delivery status for all addresses to FAIL. */ + +if (mua_wrapper) + { + for (addr = addrlist; addr != NULL; addr = addr->next) + addr->transport_return = FAIL; + goto END_TRANSPORT; + } + +/* In the normal, non-wrapper case, add a standard message to each deferred +address if there hasn't been an error, that is, if it hasn't actually been +tried this time. The variable "expired" will be FALSE if any deliveries were +actually tried, or if there was at least one host that was not expired. That +is, it is TRUE only if no deliveries were tried and all hosts were expired. If +a delivery has been tried, an error code will be set, and the failing of the +message is handled by the retry code later. + +If queue_smtp is set, or this transport was called to send a subsequent message +down an existing TCP/IP connection, and something caused the host not to be +found, we end up here, but can detect these cases and handle them specially. */ + +for (addr = addrlist; addr != NULL; addr = addr->next) + { + /* If host is not NULL, it means that we stopped processing the host list + because of hosts_max_try. This means we need to behave as if some hosts were + skipped because their retry time had not come. Specifically, this prevents + the address from timing out. */ + + if (host != NULL) + { + DEBUG(D_transport) + debug_printf("hosts_max_try limit caused some hosts to be skipped\n"); + setflag(addr, af_retry_skipped); + } + + if (queue_smtp) /* no deliveries attempted */ + { + addr->transport_return = DEFER; + addr->basic_errno = 0; + addr->message = US"SMTP delivery explicitly queued"; + } + + else if (addr->transport_return == DEFER && + (addr->basic_errno == ERRNO_UNKNOWNERROR || addr->basic_errno == 0) && + addr->message == NULL) + { + addr->basic_errno = ERRNO_HRETRY; + if (continue_hostname != NULL) + { + addr->message = US"no host found for existing SMTP connection"; + } + else if (expired) + { + addr->message = (ob->delay_after_cutoff)? + US"retry time not reached for any host after a long failure period" : + US"all hosts have been failing for a long time and were last tried " + "after this message arrived"; + + /* If we are already using fallback hosts, or there are no fallback hosts + defined, convert the result to FAIL to cause a bounce. */ + + if (addr->host_list == addr->fallback_hosts || + addr->fallback_hosts == NULL) + addr->transport_return = FAIL; + } + else + { + if (hosts_retry == hosts_total) + addr->message = US"retry time not reached for any host"; + else if (hosts_fail == hosts_total) + addr->message = US"all host address lookups failed permanently"; + else if (hosts_defer == hosts_total) + addr->message = US"all host address lookups failed temporarily"; + else if (hosts_serial == hosts_total) + addr->message = US"connection limit reached for all hosts"; + else if (hosts_fail+hosts_defer == hosts_total) + addr->message = US"all host address lookups failed"; + else addr->message = US"some host address lookups failed and retry time " + "not reached for other hosts or connection limit reached"; + } + } + } + +/* Update the database which keeps information about which messages are waiting +for which hosts to become available. Each host in the list has a flag which is +set if the data is to be updated. For some message-specific errors, the flag is +turned off because we don't want follow-on deliveries in those cases. */ + +transport_update_waiting(hostlist, tblock->name); + +END_TRANSPORT: + +DEBUG(D_transport) debug_printf("Leaving %s transport\n", tblock->name); + +return TRUE; /* Each address has its status */ +} + +/* End of transport/smtp.c */ diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h new file mode 100644 index 000000000..bff681cad --- /dev/null +++ b/src/src/transports/smtp.h @@ -0,0 +1,69 @@ +/* $Cambridge: exim/src/src/transports/smtp.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options and other private data. */ + +typedef struct { + uschar *hosts; + uschar *fallback_hosts; + host_item *hostlist; + host_item *fallback_hostlist; + uschar *authenticated_sender; + uschar *helo_data; + uschar *interface; + uschar *port; + uschar *protocol; + uschar *serialize_hosts; + uschar *hosts_try_auth; + uschar *hosts_require_auth; + uschar *hosts_require_tls; + uschar *hosts_avoid_tls; + uschar *hosts_avoid_esmtp; + uschar *hosts_nopass_tls; + int command_timeout; + int connect_timeout; + int data_timeout; + int final_timeout; + int size_addition; + int hosts_max_try; + BOOL allow_localhost; + BOOL gethostbyname; + BOOL dns_qualify_single; + BOOL dns_search_parents; + BOOL delay_after_cutoff; + BOOL hosts_override; + BOOL hosts_randomize; + BOOL keepalive; + BOOL retry_include_ip_address; + #ifdef SUPPORT_TLS + uschar *tls_certificate; + uschar *tls_crl; + uschar *tls_privatekey; + uschar *tls_require_ciphers; + uschar *tls_verify_certificates; + BOOL tls_tempfail_tryclear; + #endif +} smtp_transport_options_block; + +/* Data for reading the private options. */ + +extern optionlist smtp_transport_options[]; +extern int smtp_transport_options_count; + +/* Block containing default values. */ + +extern smtp_transport_options_block smtp_transport_option_defaults; + +/* The main, init, and closedown entry points for the transport */ + +extern BOOL smtp_transport_entry(transport_instance *, address_item *); +extern void smtp_transport_init(transport_instance *); +extern void smtp_transport_closedown(transport_instance *); + +/* End of transports/smtp.h */ diff --git a/src/src/transports/tf_maildir.c b/src/src/transports/tf_maildir.c new file mode 100644 index 000000000..bf0aa1572 --- /dev/null +++ b/src/src/transports/tf_maildir.c @@ -0,0 +1,539 @@ +/* $Cambridge: exim/src/src/transports/tf_maildir.c,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Functions in support of the use of maildirsize files for handling quotas in +maildir directories. Some of the rules are a bit baroque: + +http://www.inter7.com/courierimap/README.maildirquota.html + +We try to follow most of that, except that the directories to skip for quota +calculations are not hard wired in, but are supplied as a regex. */ + + +#include "../exim.h" +#include "appendfile.h" +#include "tf_maildir.h" + +#define MAX_FILE_SIZE 5120 + + + +/************************************************* +* Ensure maildir directories exist * +*************************************************/ + +/* This function is called at the start of a maildir delivery, to ensure that +all the relevant directories exist. + +Argument: + path the base directory name + addr the address item (for setting an error message) + create_directory true if we are allowed to create missing directories + dirmode the mode for created directories + +Returns: TRUE on success; FALSE on failure +*/ + +BOOL maildir_ensure_directories(uschar *path, address_item *addr, + BOOL create_directory, int dirmode) +{ +int i; +struct stat statbuf; +char *subdirs[] = { "/tmp", "/new", "/cur" }; + +DEBUG(D_transport) + debug_printf("ensuring maildir directories exist in %s\n", path); + +/* First ensure that the path we have is a directory; if it does not exist, +create it. Then make sure the tmp, new & cur subdirs of the maildir are +there. If not, fail which aborts the delivery (even though the cur subdir is +not actually needed for delivery). Handle all 4 directory tests/creates in a +loop so that code can be shared. */ + +for (i = 0; i < 4; i++) + { + int j; + uschar *dir, *mdir; + + if (i == 0) + { + mdir = US""; + dir = path; + } + else + { + mdir = US subdirs[i-1]; + dir = mdir + 1; + } + + /* Check an existing path is a directory. This is inside a loop because + there is a potential race condition when creating the directory - some + other process may get there first. Give up after trying several times, + though. */ + + for (j = 0; j < 10; j++) + { + if (Ustat(dir, &statbuf) == 0) + { + if (S_ISDIR(statbuf.st_mode)) break; /* out of the race loop */ + addr->message = string_sprintf("%s%s is not a directory", path, + mdir); + addr->basic_errno = ERRNO_NOTDIRECTORY; + return FALSE; + } + + /* Try to make if non-existent and configured to do so */ + + if (errno == ENOENT && create_directory) + { + if (!directory_make(NULL, dir, dirmode, FALSE)) + { + if (errno == EEXIST) continue; /* repeat the race loop */ + addr->message = string_sprintf("cannot create %s%s", path, mdir); + addr->basic_errno = errno; + return FALSE; + } + DEBUG(D_transport) + debug_printf("created directory %s%s\n", path, mdir); + break; /* out of the race loop */ + } + + /* stat() error other than ENOENT, or ENOENT and not creatable */ + + addr->message = string_sprintf("stat() error for %s%s: %s", path, mdir, + strerror(errno)); + addr->basic_errno = errno; + return FALSE; + } + + /* If we went round the loop 10 times, the directory was flickering in + and out of existence like someone in a malfunctioning Star Trek + transporter. */ + + if (j >= 10) + { + addr->message = string_sprintf("existence of %s%s unclear\n", path, + mdir); + addr->basic_errno = errno; + addr->special_action = SPECIAL_FREEZE; + return FALSE; + } + + /* First time through the directories loop, cd to the main directory */ + + if (i == 0 && Uchdir(path) != 0) + { + addr->message = string_sprintf ("cannot chdir to %s", path); + addr->basic_errno = errno; + return FALSE; + } + } + +return TRUE; /* All directories exist */ +} + + + + +/************************************************* +* Update maildirsizefile for new file * +*************************************************/ + +/* This function is called to add a new line to the file, recording the length +of the newly added message. There isn't much we can do on failure... + +Arguments: + fd the open file descriptor + size the size of the message + +Returns: nothing +*/ + +void +maildir_record_length(int fd, int size) +{ +int len; +uschar buffer[256]; +sprintf(CS buffer, "%d 1\n", size); +len = Ustrlen(buffer); +(void)lseek(fd, 0, SEEK_END); +(void)write(fd, buffer, len); +DEBUG(D_transport) + debug_printf("added '%.*s' to maildirsize file\n", len-1, buffer); +} + + + +/************************************************* +* Find the size of a maildir * +*************************************************/ + +/* This function is called when we have to recalculate the size of a maildir by +scanning all the files and directories therein. There are rules and conventions +about which files or directories are included. We support this by the use of a +regex to match directories that are to be included. + +Maildirs can only be one level deep. However, this function recurses, so it +might cope with deeper nestings. We use the existing check_dir_size() function +to add up the sizes of the files in a directory that contains messages. + +The function returns the most recent timestamp encountered. It can also be run +in a dummy mode in which it does not scan for sizes, but just returns the +timestamp. + +Arguments: + path the path to the maildir + filecount where to store the count of messages + latest where to store the latest timestamp encountered + regex a regex for getting files sizes from file names + dir_regex a regex for matching directories to be included + timestamp_only don't actually compute any sizes + +Returns: the sum of the sizes of the messages +*/ + +int +maildir_compute_size(uschar *path, int *filecount, time_t *latest, + const pcre *regex, const pcre *dir_regex, BOOL timestamp_only) +{ +DIR *dir; +int sum = 0; +struct dirent *ent; +struct stat statbuf; + +dir = opendir(CS path); +if (dir == NULL) return 0; + +while ((ent = readdir(dir)) != NULL) + { + uschar *name = US ent->d_name; + uschar buffer[1024]; + + if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue; + + /* We are normally supplied with a regex for choosing which directories to + scan. We do the regex match first, because that avoids a stat() for names + we aren't interested in. */ + + if (dir_regex != NULL && + pcre_exec(dir_regex, NULL, CS name, Ustrlen(name), 0, 0, NULL, 0) < 0) + { + DEBUG(D_transport) + debug_printf("skipping %s/%s: dir_regex does not match\n", path, name); + continue; + } + + /* The name is OK; stat it. */ + + if (!string_format(buffer, sizeof(buffer), "%s/%s", path, name)) + { + DEBUG(D_transport) + debug_printf("maildir_compute_size: name too long: dir=%s name=%s\n", + path, name); + continue; + } + + if (Ustat(buffer, &statbuf) < 0) + { + DEBUG(D_transport) + debug_printf("maildir_compute_size: stat error %d for %s: %s\n", errno, + buffer, strerror(errno)); + continue; + } + + if ((statbuf.st_mode & S_IFMT) != S_IFDIR) + { + DEBUG(D_transport) + debug_printf("skipping %s/%s: not a directory\n", path, name); + continue; + } + + /* Keep the latest timestamp encountered */ + + if (statbuf.st_mtime > *latest) *latest = statbuf.st_mtime; + + /* If this is a maildir folder, call this function recursively. */ + + if (name[0] == '.') + { + sum += maildir_compute_size(buffer, filecount, latest, regex, dir_regex, + timestamp_only); + } + + /* Otherwise it must be a folder that contains messages (e.g. new or cur), so + we need to get its size, unless all we are interested in is the timestamp. */ + + else if (!timestamp_only) + { + sum += check_dir_size(buffer, filecount, regex); + } + } + +closedir(dir); +DEBUG(D_transport) + { + if (timestamp_only) + debug_printf("maildir_compute_size (timestamp_only): %ld\n", + (long int) *latest); + else + debug_printf("maildir_compute_size: path=%s\n sum=%d filecount=%d " + "timestamp=%ld\n", path, sum, *filecount, (long int) *latest); + } +return sum; +} + + + +/************************************************* +* Create or update maildirsizefile * +*************************************************/ + +/* This function is called before a delivery if the option to use +maildirsizefile is enabled. Its function is to create the file if it does not +exist, or to update it if that is necessary. + +The logic in this function follows the rules that are described in + + http://www.inter7.com/courierimap/README.maildirquota.html + +Or, at least, it is supposed to! + +Arguments: + path the path to the maildir directory; this is already backed-up + to the parent if the delivery diretory is a maildirfolder + ob the appendfile options block + regex a compiled regex for getting a file's size from its name + dir_regex a compiled regex for selecting maildir directories + returned_size where to return the current size of the maildir, even if + the maildirsizefile is removed because of a race + +Returns: >=0 a file descriptor for an open maildirsize file + -1 there was an error opening or accessing the file + -2 the file was removed because of a race +*/ + +int +maildir_ensure_sizefile(uschar *path, appendfile_transport_options_block *ob, + const pcre *regex, const pcre *dir_regex, int *returned_size, + int *returned_filecount) +{ +int count, fd; +int cached_quota = 0; +int cached_quota_filecount = 0; +int size = 0; +int filecount = 0; +int linecount = 0; +uschar *filename; +uschar buffer[MAX_FILE_SIZE]; +uschar *ptr = buffer; +uschar *endptr; + +/* Try a few times to open or create the file, in case another process is doing +the same thing. */ + +filename = string_sprintf("%s/maildirsize", path); + +DEBUG(D_transport) debug_printf("looking for maildirsize in %s\n", path); +fd = Uopen(filename, O_RDWR|O_APPEND, 0); +if (fd < 0) + { + if (errno != ENOENT) return -1; + DEBUG(D_transport) + debug_printf("%s does not exist: recalculating\n", filename); + goto RECALCULATE; + } + +/* The file has been successfully opened. Check that the cached quota value is +still correct, and that the size of the file is still small enough. If so, +compute the maildir size from the file. */ + +count = read(fd, buffer, sizeof(buffer)); +if (count >= sizeof(buffer)) + { + DEBUG(D_transport) + debug_printf("maildirsize file too big (%d): recalculating\n", count); + goto RECALCULATE; + } +buffer[count] = 0; /* Ensure string terminated */ + +/* Read the quota parameters from the first line of the data. */ + +DEBUG(D_transport) + debug_printf("reading quota parameters from maildirsize data\n"); + +for (;;) + { + long int n = Ustrtol(ptr, &endptr, 10); + + /* Only two data items are currently defined; ignore any others that + may be present. The spec is for a number followed by a letter. Anything + else we reject and recalculate. */ + + if (*endptr == 'S') cached_quota = n; + else if (*endptr == 'C') cached_quota_filecount = n; + if (!isalpha(*endptr++)) + { + DEBUG(D_transport) + debug_printf("quota parameter number not followed by letter in " + "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer), + buffer); + goto RECALCULATE; + } + if (*endptr == '\n' || *endptr == 0) break; + if (*endptr++ != ',') + { + DEBUG(D_transport) + debug_printf("quota parameter not followed by comma in " + "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer), + buffer); + goto RECALCULATE; + } + ptr = endptr; + } + +/* Check the cached values against the current settings */ + +if (cached_quota != ob->quota_value || + cached_quota_filecount != ob->quota_filecount_value) + { + DEBUG(D_transport) + debug_printf("cached quota is out of date: recalculating\n" + " quota=%d cached_quota=%d filecount_quota=%d " + "cached_quota_filecount=%d\n", ob->quota_value, cached_quota, + ob->quota_filecount_value, cached_quota_filecount); + goto RECALCULATE; + } + +/* Quota values agree; parse the rest of the data to get the sizes. At this +stage, *endptr points either to 0 or to '\n'. */ + +DEBUG(D_transport) + debug_printf("computing maildir size from maildirsize data\n"); + +while (*endptr++ == '\n') + { + if (*endptr == 0) break; + linecount++; + ptr = endptr; + size += Ustrtol(ptr, &endptr, 10); + if (*endptr != ' ') break; + ptr = endptr + 1; + filecount += Ustrtol(ptr, &endptr, 10); + } + +/* If *endptr is zero, we have successfully parsed the file, and we now have +the size of the mailbox as cached in the file. The "rules" say that if this +value indicates that the mailbox is over quota, we must recalculate if there is +more than one entry in the file, or if the file is older than 15 minutes. */ + +if (*endptr == 0) + { + if (ob->quota_value > 0 && + (size + (ob->quota_is_inclusive? message_size : 0) > ob->quota_value || + (ob->quota_filecount_value > 0 && + filecount + (ob->quota_is_inclusive ? 1:0) > + ob->quota_filecount_value) + )) + { + struct stat statbuf; + if (linecount > 1) + { + DEBUG(D_transport) debug_printf("over quota and maildirsizefile has " + "more than 1 entry: recalculating\n"); + goto RECALCULATE; + } + + if (fstat(fd, &statbuf) < 0) goto RECALCULATE; /* Should never occur */ + + if (time(NULL) - statbuf.st_mtime > 15*60) + { + DEBUG(D_transport) debug_printf("over quota and maildirsize is older " + "than 15 minutes: recalculating\n"); + goto RECALCULATE; + } + } + } + + +/* If *endptr is not zero, there was a syntax error in the file. */ + +else + { + int len; + time_t old_latest, new_latest; + uschar *tempname; + struct timeval tv; + + DEBUG(D_transport) + { + uschar *p = endptr; + while (p > buffer && p[-1] != '\n') p--; + endptr[1] = 0; + + debug_printf("error in maildirsizefile: unexpected character %d in " + "line %d (starting '%s'): recalculating\n", + *endptr, linecount + 1, string_printing(p)); + } + + /* Either there is no file, or the quota value has changed, or the file has + got too big, or there was some format error in the file. Recalculate the size + and write new contents to a temporary file; then rename it. After any + error, just return -1 as the file descriptor. */ + + RECALCULATE: + + if (fd >= 0) close(fd); + old_latest = 0; + filecount = 0; + size = maildir_compute_size(path, &filecount, &old_latest, regex, dir_regex, + FALSE); + + (void)gettimeofday(&tv, NULL); + tempname = string_sprintf("%s/tmp/%lu.H%luP%lu.%s", path, tv.tv_sec, + tv.tv_usec, getpid(), primary_hostname); + + fd = Uopen(tempname, O_RDWR|O_CREAT|O_EXCL, 0600); + if (fd >= 0) + { + (void)sprintf(CS buffer, "%dS,%dC\n%d %d\n", ob->quota_value, + ob->quota_filecount_value, size, filecount); + len = Ustrlen(buffer); + if (write(fd, buffer, len) != len || Urename(tempname, filename) < 0) + { + close(fd); + fd = -1; + } + } + + /* If any of the directories have been modified since the last timestamp we + saw, we have to junk this maildirsize file. */ + + DEBUG(D_transport) debug_printf("checking subdirectory timestamps\n"); + new_latest = 0; + (void)maildir_compute_size(path, NULL, &new_latest , NULL, dir_regex, TRUE); + if (new_latest > old_latest) + { + DEBUG(D_transport) debug_printf("abandoning maildirsize because of " + "a later subdirectory modification\n"); + (void)Uunlink(filename); + close(fd); + fd = -1; + } + } + +/* Return the sizes and the file descriptor, if any */ + +DEBUG(D_transport) debug_printf("returning maildir size=%d filecount=%d\n", + size, filecount); +*returned_size = size; +*returned_filecount = filecount; +return fd; +} + +/* End of tf_maildir.c */ diff --git a/src/src/transports/tf_maildir.h b/src/src/transports/tf_maildir.h new file mode 100644 index 000000000..dc3b726f7 --- /dev/null +++ b/src/src/transports/tf_maildir.h @@ -0,0 +1,21 @@ +/* $Cambridge: exim/src/src/transports/tf_maildir.h,v 1.1 2004/10/07 13:10:02 ph10 Exp $ */ + +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Header file for the functions that are used to support the use of +maildirsize files for quota handling in maildir directories. */ + +extern int maildir_compute_size(uschar *, int *, time_t *, const pcre *, + const pcre *, BOOL); +extern BOOL maildir_ensure_directories(uschar *, address_item *, BOOL, int); +extern int maildir_ensure_sizefile(uschar *, + appendfile_transport_options_block *, const pcre *, const pcre *, + int *, int *); +extern void maildir_record_length(int, int); + +/* End of tf_maildir.h */