sympa-6.2.24/0000755000175000017500000000000013216651447011703 5ustar rackerackesympa-6.2.24/src/0000755000175000017500000000000013216651447012472 5ustar rackerackesympa-6.2.24/src/Sympa/0000755000175000017500000000000013216651447013563 5ustar rackerackesympa-6.2.24/src/Sympa/Extractor.pm0000644000175000017500000000221013216651447016067 0ustar rackerackepackage Sympa::Extractor; use strict; use base qw(Locale::Maketext::Extract::Plugin::Base); our $VERSION = '0.1'; =head1 NAME Sympa::Extractor - Sympa plugin for Locale::Maketext::Extract =head1 SYNOPSIS $plugin = Sympa::Extractor->new( $lexicon # A Locale::Maketext::Extract object @file_types # Optionally specify a list of recognised file types ) $plugin->extract($filename,$filecontents); =head1 DESCRIPTION Extracts strings to localise from List.pm and scenarios files =head1 VALID FORMATS gettext_id entries from List.pm, and title.gettext entries from scenarios are extracted. =head1 KNOWN FILE TYPES =over 4 =item All file types =back =cut sub file_types { return qw( * ); } sub extract { my $self = shift; local $_ = shift; my $count = 1; foreach my $line (split(/\n/, $_)) { # scenarios if ($line =~ /^title.gettext\s+(.+)$/) { $self->add_entry($1, $count, ''); } # List.pm if ($line =~ /'gettext_id'\s+=>\s+(["'])(.+)\1/) { $self->add_entry($2, $count, ''); } $count++; } } 1; sympa-6.2.24/src/smtpc/0000755000175000017500000000000013216651447013620 5ustar rackerackesympa-6.2.24/src/smtpc/utf8.c0000644000175000017500000001103613216651447014653 0ustar rackeracke/* $Id$ */ /* * Sympa - SYsteme de Multi-Postage Automatique * * Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel * Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, * 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER * * 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, see . */ /* * utf8_check was originally taken from UTF8.xs in Unicode-UTF8 module by * Christian Hansen distributed under Perl 5 License: * . * * Copyright 2011-2012 by Christian Hansen. */ #include "config.h" #include #include #if SIZEOF_UNSIGNED_INT >= 4 typedef unsigned int unichar_t; #elif SIZEOF_UNSIGNED_LONG >= 4 typedef unsigned long unichar_t; #else #error "Integral types on your system are too short" #endif static const unsigned char utf8_sequence_len[0x100] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x00-0x0F */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x10-0x1F */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x20-0x2F */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x30-0x3F */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x40-0x4F */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x50-0x5F */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x60-0x6F */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x70-0x7F */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x80-0x8F */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x90-0x9F */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xA0-0xAF */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xB0-0xBF */ 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0xC0-0xCF */ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0xD0-0xDF */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, /* 0xE0-0xEF */ 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xF0-0xFF */ }; /** Check string * Check if the string consists of valid UTF-8 sequence. * @param[in] s Buffer. * @param[in] len Length of buffer. * @returns If the buffer contains only ASCII characters, -1. * Else if the buffer contains non-ASCII sequence not forming valid UTF-8, * index of the first position such sequence appears. * Otherwise, length of the buffer. */ ssize_t utf8_check(const unsigned char *s, const size_t len) { const unsigned char *p = s; const unsigned char *e = s + len; const unsigned char *e4 = e - 4; unichar_t v; int is_asciionly = 1; /* Added to check if non-ASCII is included. */ while (p < e4) { while (p < e4 && *p < 0x80) p++; check: switch (utf8_sequence_len[*p]) { case 0: goto done; case 1: p += 1; break; case 2: /* 110xxxxx 10xxxxxx */ if ((p[1] & 0xC0) != 0x80) goto done; p += 2; is_asciionly = 0; break; case 3: v = ((unichar_t) p[0] << 16) | ((unichar_t) p[1] << 8) | ((unichar_t) p[2]); /* 1110xxxx 10xxxxxx 10xxxxxx */ if ((v & 0x00F0C0C0) != 0x00E08080 || /* Non-shortest form */ v < 0x00E0A080 || /* Surrogates U+D800..U+DFFF */ (v & 0x00EFA080) == 0x00EDA080 || /* Non-characters U+FDD0..U+FDEF, U+FFFE..U+FFFF */ (v >= 0x00EFB790 && (v <= 0x00EFB7AF || v >= 0x00EFBFBE))) goto done; p += 3; is_asciionly = 0; break; case 4: v = ((unichar_t) p[0] << 24) | ((unichar_t) p[1] << 16) | ((unichar_t) p[2] << 8) | ((unichar_t) p[3]); /* 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ if ((v & 0xF8C0C0C0) != 0xF0808080 || /* Non-shortest form */ v < 0xF0908080 || /* Greater than U+10FFFF */ v > 0xF48FBFBF || /* Non-characters U+nFFFE..U+nFFFF on plane 1-16 */ (v & 0x000FBFBE) == 0x000FBFBE) goto done; p += 4; is_asciionly = 0; break; } } if (p < e && p + utf8_sequence_len[*p] <= e) goto check; done: if (p == e && is_asciionly) return -1; else return p - s; } sympa-6.2.24/src/smtpc/sockstr.c0000644000175000017500000003603413216651447015462 0ustar rackeracke/* $Id$ */ /* * Sympa - SYsteme de Multi-Postage Automatique * * Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel * Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, * 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER * * 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, see . */ /* * sockstr.c was originally written by IKEDA Soji * as a part of smtpc utility for Sympa project. * * 2015-05-17 IKEDA Soji: Initial checkin to source repository. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "sockstr.h" /** Constructor * Creats new instance of sockstr object. * @param[in] nodename Hostname of the server. Default is "localhost". * @param[in] servname Port number or service name. Default is "smtp". * @returns New instance. * If error occurred, sets errno and returns NULL. */ sockstr_t *sockstr_new(char *nodename, char *servname, char *path) { sockstr_t *self; self = (sockstr_t *) malloc(sizeof(sockstr_t)); if (self == NULL) return NULL; self->_errstr[0] = '\0'; self->_sock = -1; self->_bufcnt = 0; self->_bufptr = self->_buf; self->timeout = 300; if (path != NULL && *path) { self->path = strdup(path); if (self->path == NULL) { free(self); return NULL; } self->nodename = NULL; self->servname = NULL; } else { self->path = NULL; if (!nodename || !*nodename) nodename = "localhost"; if (!servname || !*servname) servname = "25"; self->nodename = strdup(nodename); self->servname = strdup(servname); if (self->nodename == NULL || self->servname == NULL) { if (self->nodename != NULL) free(self->nodename); if (self->servname != NULL) free(self->servname); free(self); return NULL; } } return self; } /** Destructor * Unallocate memory for the instance. * @retuns None. */ void sockstr_destroy(sockstr_t * self) { if (self == NULL) return; if (0 <= self->_sock) { shutdown(self->_sock, SHUT_RDWR); close(self->_sock); } if (self->nodename != NULL) free(self->nodename); if (self->servname != NULL) free(self->servname); if (self->path != NULL) free(self->path); free(self); } static void sockstr_set_error(sockstr_t * self, int errnum, const char *errstr) { self->_errnum = errnum; if (errstr == NULL) { char *buf; buf = strerror(errnum); if (buf == NULL) snprintf(self->_errstr, SOCKSTR_ERRSIZE, "(%d) Error", errnum); else snprintf(self->_errstr, SOCKSTR_ERRSIZE, "(%d) %s", errnum, buf); } else snprintf(self->_errstr, SOCKSTR_ERRSIZE, "(%d) %s", errnum, errstr); } /** Last error * Gets error by the last operation. * @returns String, or if the last operation was success, NULL. */ char *sockstr_errstr(sockstr_t * self) { return self->_errstr; } static int _connect_socket(sockstr_t * self, int sock, struct sockaddr *ai_addr, socklen_t ai_addrlen, int blocking) { long flags; flags = fcntl(sock, F_GETFL, NULL); if (flags < 0 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) { sockstr_set_error(self, errno, NULL); return -1; } if (connect(sock, ai_addr, ai_addrlen) < 0) { if (errno == EINPROGRESS) { struct timeval tv; fd_set rfd, wfd; int rc, errnum; socklen_t errlen; do { tv.tv_sec = self->timeout; tv.tv_usec = 0; FD_ZERO(&rfd); FD_SET(sock, &rfd); wfd = rfd; rc = select(sock + 1, &rfd, &wfd, NULL, &tv); } while (rc < 0 && errno == EINTR); if (rc == 0) { sockstr_set_error(self, ETIMEDOUT, NULL); return -1; } else if (FD_ISSET(sock, &rfd) || FD_ISSET(sock, &wfd)) { errlen = sizeof(errnum); getsockopt(sock, SOL_SOCKET, SO_ERROR, (void *) &errnum, &errlen); if (errnum) { sockstr_set_error(self, errnum, NULL); return -1; } } else { sockstr_set_error(self, errno, NULL); return -1; } } else { sockstr_set_error(self, errno, NULL); return -1; } } if (blocking) { if (fcntl(sock, F_SETFL, flags) < 0) { sockstr_set_error(self, errno, NULL); return -1; } } return 0; } int socktcp_connect(sockstr_t * self) { struct addrinfo hints, *ai0, *ai; int errnum; int sock = -1; if (0 <= self->_sock) { sockstr_set_error(self, EISCONN, NULL); return -1; } memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; errnum = getaddrinfo(self->nodename, self->servname, &hints, &ai0); if (errnum) { sockstr_set_error(self, errnum, gai_strerror(errnum)); return -1; } for (ai = ai0; ai != NULL; ai = ai->ai_next) { sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if (sock < 0) { sockstr_set_error(self, errno, NULL); continue; } if (_connect_socket(self, sock, ai->ai_addr, ai->ai_addrlen, 0) == 0) break; close(sock); sock = -1; } freeaddrinfo(ai0); if (sock < 0) return -1; self->_sock = sock; return 0; } int sockunix_connect(sockstr_t * self) { struct sockaddr_un sa_un; /* The name "sun" messes Solaris. */ size_t sunlen; int sock = -1; if (0 <= self->_sock) { sockstr_set_error(self, EISCONN, NULL); return -1; } if (self->path == NULL || self->path[0] == '\0') { sockstr_set_error(self, EINVAL, NULL); return -1; } sunlen = strlen(self->path); if (sizeof(sa_un.sun_path) < sunlen + 1) { sockstr_set_error(self, ENAMETOOLONG, NULL); return -1; } memset(&sa_un, 0, sizeof(sa_un)); sa_un.sun_family = PF_UNIX; memcpy(sa_un.sun_path, self->path, sunlen + 1); /* I don't know any platforms need to set .sun_len member. */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { sockstr_set_error(self, errno, NULL); return -1; } if (_connect_socket (self, sock, (struct sockaddr *) &sa_un, sizeof(sa_un), 0) < 0) { close(sock); return -1; } self->_sock = sock; return 0; } /** Connect * Connects to the host. * @returns 1 if success, otherwise 0. * Description of error can be got by sockstr_errstr(). */ int sockstr_client_connect(sockstr_t * self) { if (self->path != NULL) return sockunix_connect(self); else return socktcp_connect(self); } static ssize_t sockstr_read(sockstr_t * self, char *buf, size_t count) { int cnt; while (self->_bufcnt <= 0) { self->_bufcnt = read(self->_sock, self->_buf, sizeof(self->_buf)); if (self->_bufcnt < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { int rc; struct timeval tv; fd_set wfd; do { tv.tv_sec = self->timeout; tv.tv_usec = 0; FD_ZERO(&wfd); FD_SET(self->_sock, &wfd); rc = select(self->_sock + 1, NULL, &wfd, NULL, &tv); } while (rc < 0 && errno == EINTR); if (rc < 0) return -1; else if (rc == 0) { errno = ETIMEDOUT; return -1; } } else if (errno != EINTR) return -1; } else if (self->_bufcnt == 0) return 0; else self->_bufptr = self->_buf; } cnt = count; if (self->_bufcnt < count) cnt = self->_bufcnt; memcpy(buf, self->_bufptr, cnt); self->_bufptr += cnt; self->_bufcnt -= cnt; return cnt; } #define SOCKSTR_MIN_BUFSIZ (4) #define SOCKSTR_DEFAULT_BUFSIZ (128) /** Read one line * Read one line termined by a newline (LF) from peer. * @param[in,out] lineptr Pointer to buffer provided by user. * @param[in,out] n Pointer to allocated size of buffer. * @param[in] omitnul If true value is specified, ignores NUL octets (\0) in * input. * @returns Size of read data, 0 if socket is no longer readalbe * or -1 on failure. * lineptr and n may be changed. */ ssize_t sockstr_getline(sockstr_t * self, char **lineptr, size_t * n, int omitnul) { ssize_t rs; char chr, *p, *newbuf; size_t len, newsiz; if (self->_sock < 0 || lineptr == NULL || n == NULL) { sockstr_set_error(self, EINVAL, NULL); return -1; } p = *lineptr; while (1) { rs = sockstr_read(self, &chr, 1); if (rs == 1) { len = p - *lineptr; if (*lineptr == NULL || *n < len + 2) { if (*lineptr == NULL || *n < SOCKSTR_MIN_BUFSIZ) newsiz = SOCKSTR_DEFAULT_BUFSIZ; else newsiz = *n << 1; if (*lineptr == NULL) newbuf = malloc(newsiz); else newbuf = realloc(*lineptr, newsiz); if (newbuf == NULL) { sockstr_set_error(self, errno, NULL); return -1; } *lineptr = newbuf; *n = newsiz; p = *lineptr + len; } if (!omitnul || chr != '\0') *p++ = chr; if (chr == '\n') break; } else if (rs == 0) { /* Disconnected. */ if (p == *lineptr) { /* EOF */ sockstr_set_error(self, ECONNRESET, NULL); return 0; } break; } else { sockstr_set_error(self, errno, NULL); return -1; } } *p = '\0'; return p - *lineptr; } /* Get status line(s) * Read (one or more) status line(s) from peer. * @param[in,out] lineptr Pointer to buffer provided by user. * @oaram[in,out] n Pointer to allocated size of buffer. * @returns Size of read data, 0 if socket is no longer readalbe * or -1 on failure. * lineptr and n may be changed. * NUL octets (\0) in input are ignored. */ ssize_t sockstr_getstatus(sockstr_t * self, char **lineptr, size_t * n) { ssize_t rs; char *buf = NULL, *newbuf; size_t bufsiz = 0, newsiz, len = 0; if (self->_sock < 0 || lineptr == NULL || n == NULL) { sockstr_set_error(self, EINVAL, NULL); return -1; } while (1) { rs = sockstr_getline(self, &buf, &bufsiz, 1); if (rs < 0) { if (buf != NULL) free(buf); return -1; } else if (rs == 0) { /* Disconnected. */ if (len == 0) { /* EOF */ sockstr_set_error(self, ECONNRESET, NULL); if (buf != NULL) free(buf); return 0; } break; } else { if (*lineptr == NULL || *n < len + rs + 1) { if (*lineptr == NULL || *n < SOCKSTR_DEFAULT_BUFSIZ) newsiz = SOCKSTR_DEFAULT_BUFSIZ; else newsiz = *n; while (newsiz < len + rs + 1) newsiz <<= 1; if (*lineptr == NULL) newbuf = malloc(newsiz); else newbuf = realloc(*lineptr, newsiz); if (newbuf == NULL) { sockstr_set_error(self, errno, NULL); if (buf != NULL) free(buf); return -1; } *n = newsiz; *lineptr = newbuf; } memcpy(*lineptr + len, buf, rs + 1); len += rs; if (3 <= rs && '2' <= buf[0] && buf[0] <= '5' && '0' <= buf[1] && buf[1] <= '9' && '0' <= buf[2] && buf[2] <= '9') { if (buf[3] != '-') break; } else { sockstr_set_error(self, EINVAL, NULL); if (buf != NULL) free(buf); return -1; } } } if (buf != NULL) free(buf); return len; } static ssize_t sockstr_write(sockstr_t * self, void *buf, size_t count) { size_t leftlen = count; ssize_t rs; char *p = buf; while (leftlen > 0) { rs = write(self->_sock, p, leftlen); if (rs < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { int rc; struct timeval tv; fd_set rfd; do { tv.tv_sec = self->timeout; tv.tv_usec = 0; FD_ZERO(&rfd); FD_SET(self->_sock, &rfd); rc = select(self->_sock + 1, &rfd, NULL, NULL, &tv); } while (rc < 0 && errno == EINTR); if (rc < 0) return -1; else if (rc == 0) { errno = ETIMEDOUT; return -1; } } else if (errno != EINTR) return -1; } else { leftlen -= rs; p += rs; } } return count; } /** Write formatted string * Formats string according to format and write to peer. * @param[in] format Format. * @param[in] ap Arguments fed to vsnprintf(3). * @returns Number of octets written. */ ssize_t sockstr_vprintf(sockstr_t * self, const char *format, va_list ap) { int rc; ssize_t rs; char *buf, *newbuf; va_list ap_again; buf = malloc(SOCKSTR_DEFAULT_BUFSIZ); if (buf == NULL) { sockstr_set_error(self, errno, NULL); return -1; } va_copy(ap_again, ap); rc = vsnprintf(buf, SOCKSTR_DEFAULT_BUFSIZ, format, ap); if (rc < 0) { sockstr_set_error(self, errno, NULL); va_end(ap_again); free(buf); return -1; } else if (SOCKSTR_DEFAULT_BUFSIZ < rc + 1) { newbuf = realloc(buf, rc + 1); if (newbuf == NULL) { sockstr_set_error(self, errno, NULL); va_end(ap_again); free(buf); return -1; } buf = newbuf; rc = vsnprintf(buf, rc + 1, format, ap_again); if (rc < 0) { sockstr_set_error(self, errno, NULL); va_end(ap_again); free(buf); return -1; } } va_end(ap_again); rs = sockstr_write(self, buf, rc); if (rs < 0) sockstr_set_error(self, errno, NULL); free(buf); return rs; } /** @todo doc * */ ssize_t sockstr_printf(sockstr_t * self, const char *format, ...) { va_list ap; ssize_t rs; va_start(ap, format); rs = sockstr_vprintf(self, format, ap); va_end(ap); return rs; } /** Write data to peer * Writes data to peer. * @param[in] buf Buffer including data. * @param[in] count Size of data. * @param[in] delim Delimiter appended to output. * @param[in] fixnewline Whether newlines will be canonicalized or not. * @param[in] fixdot Fix lines beginning with dot (.). * @returns Number of octets written. */ ssize_t sockstr_putdata(sockstr_t * self, void *buf, size_t count, char *delim, int fixnewline, int fixdot) { char *p, *q, *end; ssize_t rs, len, linelen; p = q = buf; end = buf + count; len = 0; while (p < end) { if (fixdot && *p == '.') { rs = sockstr_write(self, ".", 1); if (rs < 0) { sockstr_set_error(self, errno, NULL); return -1; } else len += rs; } while (q < end) if (*(q++) == '\n') break; if (fixnewline) { if ((p + 1 == q && q[-1] == '\n') || q[-1] == '\r' || (p + 1 < q && q[-2] != '\r' && q[-1] == '\n')) linelen = q - p - 1; else if (q[-1] != '\n') linelen = q - p; else linelen = q - p - 2; } else linelen = q - p; rs = sockstr_write(self, p, linelen); if (rs < 0) { sockstr_set_error(self, errno, NULL); return -1; } else len += rs; if (fixnewline) { rs = sockstr_write(self, "\r\n", 2); if (rs < 0) { sockstr_set_error(self, errno, NULL); return -1; } else len += rs; } p = q; } if (delim != NULL && *delim != '\0') { rs = sockstr_write(self, delim, strlen(delim)); if (rs < 0) { sockstr_set_error(self, errno, NULL); return -1; } else len += rs; } return len; } sympa-6.2.24/src/smtpc/configure.ac0000644000175000017500000000225513216651447016112 0ustar rackeracke# -*- Autoconf -*- # $Id$ AC_PREREQ([2.59]) AC_INIT([smtpc], [0.3], [sympa-developpers@listes.renater.fr]) AM_INIT_AUTOMAKE([foreign -Wall -Werror 1.9 tar-pax]) m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) AC_CONFIG_HEADERS([config.h]) # Check options # If you bundle me in other package, use --disable-smtpc not to build me. AC_ARG_ENABLE( smtpc, [], [case "$enableval" in yes) smtpc=true;; no) smtpc=false;; *) smtpc=true;; esac], [smtpc=true] ) # Checks for programs. AC_PROG_CC if test x$smtpc = xtrue; then # Checks for libraries. # Checks for header files. AC_CHECK_HEADERS([fcntl.h netdb.h stdlib.h string.h sys/socket.h unistd.h]) # Checks for typedefs, structures, and compiler characteristics. AC_TYPE_SIZE_T AC_TYPE_SSIZE_T AC_CHECK_SIZEOF([unsigned int]) AC_CHECK_SIZEOF([unsigned long]) # Checks for library functions. AC_FUNC_MALLOC AC_FUNC_REALLOC AC_CHECK_FUNCS([memset select socket strdup strerror]) AC_FUNC_SNPRINTF AC_CHECK_FUNC([getaddrinfo], [], [AC_CHECK_LIB([socket], [getaddrinfo], [LIBS="-lsocket -lnsl $LIBS"], [], [-lnsl])]) fi AC_CONFIG_FILES([Makefile]) AC_OUTPUT sympa-6.2.24/src/smtpc/sockstr.h0000644000175000017500000000357213216651447015470 0ustar rackeracke/* $Id$ */ /* * Sympa - SYsteme de Multi-Postage Automatique * * Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel * Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, * 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER * * 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, see . */ #include #include #include #define SOCKSTR_ERRSIZE (128) #define SOCKSTR_BUFSIZE (8192) typedef struct { char *nodename; char *servname; char *path; int timeout; int _sock; int _errnum; char _errstr[SOCKSTR_ERRSIZE]; ssize_t _bufcnt; char *_bufptr; char _buf[SOCKSTR_BUFSIZE]; } sockstr_t; extern sockstr_t *sockstr_new(char *, char *, char *); extern void sockstr_destroy(sockstr_t *); extern char *sockstr_errstr(sockstr_t *); extern int sockstr_client_connect(sockstr_t *); extern ssize_t sockstr_getline(sockstr_t *, char **, size_t *, int); extern ssize_t sockstr_getstatus(sockstr_t *, char **, size_t *); extern ssize_t sockstr_vprintf(sockstr_t *, const char *, va_list); extern ssize_t sockstr_printf(sockstr_t *, const char *, ...); extern ssize_t sockstr_putdata(sockstr_t *, void *, size_t, char *, int, int); sympa-6.2.24/src/smtpc/smtpc.c0000644000175000017500000005101213216651447015111 0ustar rackeracke/* $Id$ */ /* * Sympa - SYsteme de Multi-Postage Automatique * * Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel * Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, * 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER * Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level * directory of this distribution and at * . * * 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, see . */ /* * smtpc was originally written by IKEDA Soji * for Sympa project. * * 2015-05-17 IKEDA Soji: Initial checkin to source repository. */ #include "config.h" #include #include #include #include #include #include #include "sockstr.h" #include "utf8.h" #define SMTPC_BUFSIZ (8192) #define SMTPC_ERR_SOCKET (255) #define SMTPC_ERR_PROTOCOL (254) #define SMTPC_ERR_SUBMISSION (253) #define SMTPC_ERR_UNKNOWN (252) #define SMTPC_7BIT (0) #define SMTPC_8BIT (1) #define SMTPC_UTF8 (2) #define SMTPC_PROTO_TCP (1) #define SMTPC_PROTO_UNIX (1 << 1) #define SMTPC_PROTO_ESMTP (1 << 2) #define SMTPC_PROTO_LMTP (1 << 3) #define SMTPC_EXT_8BITMIME (1) #define SMTPC_EXT_AUTH (1 << 1) #define SMTPC_EXT_DSN (1 << 2) #define SMTPC_EXT_PIPELINING (1 << 3) #define SMTPC_EXT_SIZE (1 << 4) #define SMTPC_EXT_SMTPUTF8 (1 << 5) #define SMTPC_EXT_STARTTLS (1 << 6) #define SMTPC_NOTIFY_NEVER (1) #define SMTPC_NOTIFY_SUCCESS (1 << 1) #define SMTPC_NOTIFY_FAILURE (1 << 2) #define SMTPC_NOTIFY_DELAY (1 << 3) static char buf[SMTPC_BUFSIZ]; static sockstr_t *sockstr; static struct { int dump; int verbose; unsigned int protocol; char *myname; char *nodename; char *servname; char *path; char *sender; unsigned int notify; char *envid; int smtputf8; } options = { 0, 0, 0, NULL, NULL, NULL, NULL, NULL, 0, NULL, 0}; static struct { char **recips; int recipnum; char *buf; size_t buflen; size_t size; int envfeature; int headfeature; int bodyfeature; } message = { NULL, 0, NULL, 0, 0, 0, 0}; static struct { char *buf; size_t buflen; unsigned long extensions; } server = { NULL, 0, 0}; static char *encode_xtext(unsigned char *str) { unsigned char *p; char *encbuf, *q; size_t enclen = 0; p = str; while (*p != '\0') { if (*p == '+' || *p == '=') enclen += 3; else if (33 <= *p && *p <= 126) enclen++; else enclen += 3; p++; } encbuf = malloc(enclen + 1); if (encbuf == NULL) return NULL; p = str; q = encbuf; while (*p != '\0') { if (*p == '+' || *p == '=') q += sprintf(q, "+%02X", (unsigned int) *p); else if (33 <= *p && *p <= 126) *q++ = *p; else q += sprintf(q, "+%02X", (unsigned int) *p); p++; } *q = '\0'; return encbuf; } static char *encode_unitext(unsigned char *str) { unsigned char *p; char *encbuf, *q; size_t enclen = 0; p = str; while (*p != '\0') { if (*p == '\\' || *p == '+' || *p == '=') enclen += 6; else if ((33 <= *p && *p <= 126) || 128 <= *p) enclen++; else enclen += 6; p++; } encbuf = malloc(enclen + 1); if (encbuf == NULL) return NULL; p = str; q = encbuf; while (*p != '\0') { if (*p == '\\' || *p == '+' || *p == '=') q += sprintf(q, "\\x{%02X}", (unsigned int) *p); else if ((33 <= *p && *p <= 126) || 128 <= *p) *q++ = *p; else q += sprintf(q, "\\x{%02X}", (unsigned int) *p); p++; } *q = '\0'; return encbuf; } static int parse_options(int *argcptr, char ***argvptr) { int argc = *argcptr; char **argv = *argvptr; size_t i; char *arg, *p; options.dump = 0; options.verbose = 0; options.protocol = 0; options.myname = "localhost"; options.nodename = NULL; options.servname = NULL; options.sender = NULL; options.notify = 0; options.envid = NULL; options.smtputf8 = 0; for (i = 1; i < argc && argv[i] != NULL; i++) { arg = argv[i]; if (arg[0] != '-') break; else if (arg[0] == '-' && arg[1] == '-') { if (arg[2] == '\0') { i++; break; } else if (strcmp(arg, "--dump") == 0) options.dump++; else if (strcmp(arg, "--esmtp") == 0 && i + 1 < argc) { if (options.protocol != 0) { fprintf(stderr, "Multiple servers are specified\n"); return -1; } options.protocol = SMTPC_PROTO_TCP | SMTPC_PROTO_ESMTP; arg = argv[++i]; if (arg[0] == '[') { p = options.nodename = arg + 1; while (*p != '\0' && *p != ']') p++; if (*p == ']' && options.nodename < p) *p++ = '\0'; else { fprintf(stderr, "Malformed host \"%s\"\n", arg); return -1; } if (*p == ':' && *(++p) != '\0') options.servname = p; else if (*p != '\0') { fprintf(stderr, "Malformed port \"%s\"\n", p); return -1; } else options.servname = "25"; } else { p = options.nodename = arg; while (*p != '\0' && *p != ':') p++; if (*p == ':' && options.nodename < p) *p++ = '\0'; if (*p != '\0') options.servname = p; else options.servname = "25"; } } else if (strcmp(arg, "--iam") == 0 && i + 1 < argc) options.myname = argv[++i]; else if (strcmp(arg, "--lmtp") == 0 && i + 1 < argc) { if (options.protocol != 0) { fprintf(stderr, "Multiple servers are specified\n"); return -1; } options.protocol = SMTPC_PROTO_LMTP; arg = argv[++i]; if (arg[0] == '/') { options.protocol |= SMTPC_PROTO_UNIX; options.path = arg; } else if (arg[0] == '[') { options.protocol |= SMTPC_PROTO_TCP; p = options.nodename = arg + 1; while (*p != '\0' && *p != ']') p++; if (*p == ']' && options.nodename < p) *p++ = '\0'; else { fprintf(stderr, "Malformed host \"%s\"\n", arg); return -1; } if (*p == ':' && *(++p) != '\0') options.servname = p; else if (*p != '\0') { fprintf(stderr, "Malformed port \"%s\"\n", p); return -1; } else options.servname = "24"; } else { options.protocol |= SMTPC_PROTO_TCP; p = options.nodename = arg; while (*p != '\0' && *p != ':') p++; if (*p == ':' && options.nodename < p) *p++ = '\0'; if (*p != '\0') options.servname = p; else options.servname = "24"; } } else if (strcmp(arg, "--smtputf8") == 0) options.smtputf8 = 1; else if (strcmp(arg, "--verbose") == 0) options.verbose++; } switch (arg[1]) { case 'f': if (options.sender != NULL) { fprintf(stderr, "Multiple senders are specified\n"); return -1; } if (arg[2] == '\0' && i + 1 < argc) options.sender = argv[++i]; else if (arg[2] != '\0') options.sender = arg + 2; else goto parse_options_novalue; if (strcmp(options.sender, "<>") == 0) options.sender += 2; break; case 'N': if (arg[2] == '\0' && i + 1 < argc) p = argv[++i]; else if (arg[2] != '\0') p = arg + 2; else goto parse_options_novalue; while (*p != '\0') { char word[29], *wp; wp = word; while (*p == '\t' || *p == ' ' || *p == ',') p++; if (*p == '\0') break; while (*p != '\0' && *p != '\t' && *p != ' ' && *p != ',' && wp - word + 1 < sizeof(word)) if ('a' <= *p && *p <= 'z') *wp++ = *p++ + ('A' - 'a'); else *wp++ = *p++; *wp = '\0'; if (strcmp(word, "NEVER") == 0) { options.notify |= SMTPC_NOTIFY_NEVER; } else if (strcmp(word, "SUCCESS") == 0) options.notify |= SMTPC_NOTIFY_SUCCESS; else if (strcmp(word, "FAILURE") == 0) options.notify |= SMTPC_NOTIFY_FAILURE; else if (strcmp(word, "DELAY") == 0) options.notify |= SMTPC_NOTIFY_DELAY; else { fprintf(stderr, "Unknown NOTIFY keyword \"%s\"\n", word); return -1; } if (options.notify & SMTPC_NOTIFY_NEVER && options.notify & ~SMTPC_NOTIFY_NEVER) { fprintf(stderr, "NEVER keyword must not appear with other keywords\n"); return -1; } } break; case 'V': if (arg[2] == '\0' && i + 1 < argc) options.envid = argv[++i]; else if (arg[2] != '\0') options.envid = arg + 2; else goto parse_options_novalue; p = options.envid; while (*p != '\0') if (32 <= *p && *p <= 126) p++; else { fprintf(stderr, "ENVID contains illegal character \\x%02X\n", *p); return -1; } break; default: break; parse_options_novalue: fprintf(stderr, "No value for option \"%s\"\n", arg); return -1; } } if ((options.protocol & (SMTPC_PROTO_ESMTP | SMTPC_PROTO_LMTP)) == 0) { fprintf(stderr, "Either --esmtp or --lmtp option must be given\n"); return -1; } if ((options.protocol & SMTPC_PROTO_TCP && options.nodename == NULL) || (options.protocol & SMTPC_PROTO_UNIX && options.path == NULL)) { fprintf(stderr, "Nodename nor path is not specified\n"); return -1; } if (options.sender == NULL) { fprintf(stderr, "Envelope sender is not specified\n"); return -1; } *argcptr -= i; *argvptr += i; return 0; } static int check_utf8_address(char *addrbuf) { size_t len; ssize_t rs; len = strlen(addrbuf); if (len == 0) return SMTPC_7BIT; rs = utf8_check((unsigned char *) addrbuf, len); if (rs < 0) return SMTPC_7BIT; else if (rs < len) return SMTPC_8BIT; else return SMTPC_UTF8; } static int read_envelope(char *sender, size_t recipnum, char **recips) { char **pp, **end; if (recipnum <= 0) { fprintf(stderr, "No recipients are specified\n"); return -1; } message.recipnum = recipnum; message.recips = recips; /* * Check feature of sender. */ message.envfeature = check_utf8_address(sender); /* * Check feature of recipients. */ end = recips + recipnum; for (pp = recips; message.envfeature != SMTPC_8BIT && pp < end && *pp != NULL; pp++) switch (check_utf8_address(*pp)) { case SMTPC_8BIT: message.envfeature = SMTPC_8BIT; break; case SMTPC_UTF8: message.envfeature = SMTPC_UTF8; break; default: break; } return 0; } static ssize_t read_message(void) { size_t cr; ssize_t rs; char *newbuf, *p, *end; while (1) { rs = fread(buf, 1, SMTPC_BUFSIZ, stdin); if (rs == 0) break; if (message.buf == NULL) { message.buf = malloc(rs + 1); if (message.buf == NULL) return -1; } else { newbuf = realloc(message.buf, message.buflen + rs + 1); if (newbuf == NULL) return -1; message.buf = newbuf; } memcpy(message.buf + message.buflen, buf, rs); message.buflen += rs; message.buf[message.buflen] = '\0'; if (rs < SMTPC_BUFSIZ) break; } if (feof(stdin)) { if (fclose(stdin) != 0) return -1; } else { fclose(stdin); return -1; } /* * Check message features: * - Feature of message header. * - Feature of message body. * - Estimated size of the message considering newlines. */ cr = 0; message.headfeature = SMTPC_7BIT; message.bodyfeature = SMTPC_7BIT; if (0 < message.buflen) { end = message.buf + message.buflen; for (p = message.buf; p < end; p++) if (*p == '\n') { if (p == message.buf || p[-1] != '\r') cr++; if (p[1] == '\n' || (p[1] == '\r' && p[2] == '\n')) { p++; break; } } rs = utf8_check((unsigned char *) message.buf, p - message.buf); if (rs < 0) message.headfeature = SMTPC_7BIT; else if (rs < p - message.buf) message.headfeature = SMTPC_8BIT; else message.headfeature = SMTPC_UTF8; for (; p < end; p++) if (*p & 0x80) { message.bodyfeature = SMTPC_8BIT; p++; break; } else if (*p == '\n' && p[-1] != '\r') cr++; for (; p < end; p++) if (*p == '\n' && p[-1] != '\r') cr++; if (end[-1] == '\r') cr++; else if (end[-1] != '\n') cr += 2; } else cr = 2; message.size = message.buflen + cr; return message.buflen; } static int dialog(int timeout, const char *format, ...) { va_list ap; ssize_t rs; sockstr->timeout = timeout; if (format != NULL && *format != '\0') { if (options.dump) { fprintf(stderr, "C: "); va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); } va_start(ap, format); rs = sockstr_vprintf(sockstr, format, ap); va_end(ap); if (rs < 0) return -1; } rs = sockstr_getstatus(sockstr, &server.buf, &server.buflen); if (rs <= 0) return -1; if (options.dump) fprintf(stderr, "%s", server.buf); return server.buf[0]; } static int datasend(int timeout) { ssize_t rs; sockstr->timeout = timeout; if (options.dump) fprintf(stderr, "C: (MESSAGE)\r\nC: .\r\n"); if (sockstr_putdata (sockstr, message.buf, message.buflen, ".\r\n", 1, 1) < 0) return -1; rs = sockstr_getstatus(sockstr, &server.buf, &server.buflen); if (rs <= 0) return -1; if (options.dump) fprintf(stderr, "%s", server.buf); return server.buf[0]; } static void parse_extensions(void) { char *p = server.buf; unsigned long extensions = 0L; char word[512], *wp; while (*p != '\n' && *p != '\0') p++; if (*p == '\n') p++; while (*p != '\0') { if (p[0] && p[1] && p[2] && p[3]) { p += 4; while (*p == '\t' || *p == ' ' || *p == '-') p++; if (*p == '\0') break; wp = word; while (wp - word + 1 < sizeof(word) && (*p == '-' || ('0' <= *p && *p <= '9') || ('A' <= *p && *p <= 'Z') || ('a' <= *p && *p <= 'z'))) if ('a' <= *p && *p <= 'z') *wp++ = *p++ + ('A' - 'a'); else *wp++ = *p++; *wp = '\0'; if (strcmp(word, "8BITMIME") == 0) extensions |= SMTPC_EXT_8BITMIME; else if (strcmp(word, "AUTH") == 0) extensions |= SMTPC_EXT_AUTH; else if (strcmp(word, "DSN") == 0) extensions |= SMTPC_EXT_DSN; else if (strcmp(word, "PIPELINING") == 0) extensions |= SMTPC_EXT_PIPELINING; else if (strcmp(word, "SIZE") == 0) extensions |= SMTPC_EXT_SIZE; else if (strcmp(word, "SMTPUTF8") == 0) extensions |= SMTPC_EXT_SMTPUTF8; else if (strcmp(word, "STARTTLS") == 0) extensions |= SMTPC_EXT_STARTTLS; } while (*p != '\n' && *p != '\0') p++; if (*p == '\n') p++; } server.extensions = extensions; } static ssize_t transaction(void) { ssize_t sent = 0; char *hello; char *ext_8bitmime, ext_envid[108], ext_notify[37], ext_orcpt[508], ext_size[27], *ext_smtputf8; int i; ext_8bitmime = ""; *ext_envid = '\0'; *ext_notify = '\0'; *ext_orcpt = '\0'; *ext_size = '\0'; ext_smtputf8 = ""; if (options.protocol & SMTPC_PROTO_ESMTP) hello = "EHLO"; else if (options.protocol & SMTPC_PROTO_LMTP) hello = "LHLO"; else return SMTPC_ERR_UNKNOWN; switch (dialog(300, "%s %s\r\n", hello, options.myname)) { case '2': break; case '4': case '5': return 0; case -1: return SMTPC_ERR_SOCKET; default: return SMTPC_ERR_PROTOCOL; } parse_extensions(); if (server.extensions & SMTPC_EXT_8BITMIME && (message.headfeature != SMTPC_7BIT || message.bodyfeature != SMTPC_7BIT)) ext_8bitmime = " BODY=8BITMIME"; if (server.extensions & SMTPC_EXT_DSN) { if (options.envid != NULL && options.envid[0] != '\0') { char *encbuf; encbuf = encode_xtext((unsigned char *) options.envid); if (encbuf == NULL) { perror("transaction"); return SMTPC_ERR_UNKNOWN; } snprintf(ext_envid, sizeof(ext_envid), " ENVID=%s", encbuf); free(encbuf); } if (options.notify) { unsigned int mask; for (mask = 1; mask < (1 << 4); mask <<= 1) { if (options.notify & mask) { if (*ext_notify == '\0') strcat(ext_notify, " NOTIFY="); else strcat(ext_notify, ","); switch (mask) { case SMTPC_NOTIFY_NEVER: strcat(ext_notify, "NEVER"); break; case SMTPC_NOTIFY_SUCCESS: strcat(ext_notify, "SUCCESS"); break; case SMTPC_NOTIFY_FAILURE: strcat(ext_notify, "FAILURE"); break; case SMTPC_NOTIFY_DELAY: strcat(ext_notify, "DELAY"); break; } } } /* for (mask ...) */ } /* if (options.notify & mask) */ } if (server.extensions & SMTPC_EXT_SIZE) snprintf(ext_size, sizeof(ext_size), " SIZE=%lu", (unsigned long) message.size); if (server.extensions & SMTPC_EXT_SMTPUTF8 && options.smtputf8 && ((message.envfeature == SMTPC_UTF8 && message.headfeature != SMTPC_8BIT) || (message.envfeature != SMTPC_8BIT && message.headfeature == SMTPC_UTF8))) ext_smtputf8 = " SMTPUTF8"; switch (dialog(300, "MAIL FROM:<%s>%s%s%s%s\r\n", options.sender, ext_8bitmime, ext_envid, ext_size, ext_smtputf8)) { case '2': break; case '4': case '5': return 0; case -1: return SMTPC_ERR_SOCKET; default: return SMTPC_ERR_PROTOCOL; } for (i = 0; i < message.recipnum; i++) { if (server.extensions & SMTPC_EXT_DSN) { char *encbuf; if (*ext_smtputf8) { encbuf = encode_unitext((unsigned char *) message.recips[i]); if (encbuf == NULL) { perror("transaction"); return SMTPC_ERR_UNKNOWN; } snprintf(ext_orcpt, sizeof(ext_orcpt), " ORCPT=utf-8;%s", encbuf); } else { encbuf = encode_xtext((unsigned char *) message.recips[i]); if (encbuf == NULL) { perror("transaction"); return SMTPC_ERR_UNKNOWN; } snprintf(ext_orcpt, sizeof(ext_orcpt), " ORCPT=rfc822;%s", encbuf); } free(encbuf); } switch (dialog (300, "RCPT TO:<%s>%s%s\r\n", message.recips[i], ext_notify, ext_orcpt)) { case '2': sent++; break; case '4': case '5': return 0; case -1: return SMTPC_ERR_SOCKET; default: return SMTPC_ERR_PROTOCOL; } } /* for (i ...) */ switch (dialog(120, "DATA\r\n")) { case '3': break; case '4': case '5': return 0; case -1: return SMTPC_ERR_SOCKET; default: return SMTPC_ERR_PROTOCOL; } switch (datasend(600)) { case '2': return sent; case '5': return 0; case -1: return SMTPC_ERR_SOCKET; default: return SMTPC_ERR_PROTOCOL; } } static ssize_t session() { ssize_t rs; switch (dialog(300, NULL)) { case '2': break; case '4': case '5': return SMTPC_ERR_SUBMISSION; case -1: return SMTPC_ERR_SOCKET; default: return SMTPC_ERR_PROTOCOL; } rs = transaction(); if (rs == 0) return SMTPC_ERR_SUBMISSION; return rs; } int main(int argc, char *argv[]) { ssize_t rs; if (parse_options(&argc, &argv) < 0) exit(EX_USAGE); if (read_envelope(options.sender, argc, argv) < 0) exit(EX_USAGE); if (read_message() < 0) { perror("read_message"); if (message.buf != NULL) free(message.buf); exit(EX_OSERR); } if (options.protocol & SMTPC_PROTO_TCP) sockstr = sockstr_new(options.nodename, options.servname, NULL); else if (options.protocol & SMTPC_PROTO_UNIX) sockstr = sockstr_new(NULL, NULL, options.path); else sockstr = NULL; if (sockstr == NULL) { perror("sockstr_new"); if (message.buf != NULL) free(message.buf); exit(EX_OSERR); } sockstr->timeout = 300; signal(SIGPIPE, SIG_IGN); if (sockstr_client_connect(sockstr) < 0) { fprintf(stderr, "error: %s\n", sockstr_errstr(sockstr)); sockstr_destroy(sockstr); if (message.buf != NULL) free(message.buf); exit(EX_IOERR); } rs = session(); if (message.buf != NULL) free(message.buf); if (options.verbose && server.buf != NULL && 0 < server.buflen) fputs(server.buf, stdout); if (rs < 0) { switch (rs) { case SMTPC_ERR_SOCKET: fprintf(stderr, "Socket error: %s\n", sockstr_errstr(sockstr)); break; case SMTPC_ERR_PROTOCOL: fprintf(stderr, "Unexpected response: %s", server.buf); break; case SMTPC_ERR_SUBMISSION: dialog(10, "QUIT\r\n"); break; default: fprintf(stderr, "Unknown error %ld\n", (long) rs); break; } sockstr_destroy(sockstr); if (server.buf != NULL) free(server.buf); exit(rs); } /* entirely or partially sent */ dialog(10, "QUIT\r\n"); sockstr_destroy(sockstr); free(server.buf); exit(EX_OK); } sympa-6.2.24/src/smtpc/Makefile.am0000644000175000017500000000037713216651447015663 0ustar rackeracke# $Id$ ACLOCAL_AMFLAGS = -I m4 bin_PROGRAMS = smtpc smtpc_SOURCES = \ smtpc.c \ sockstr.c \ sockstr.h \ utf8.c \ utf8.h dist_doc_DATA = smtpc.1.md EXTRA_DIST = configure.gnu CLEANFILES = $(bin_PROGRAMS) *~ *.bak core.* smtpc.o: sockstr.h utf8.h sympa-6.2.24/src/smtpc/m4/0000755000175000017500000000000013216651447014140 5ustar rackerackesympa-6.2.24/src/smtpc/m4/ac_func_snprintf.m40000644000175000017500000000462613216651447017733 0ustar rackerackednl @synopsis AC_FUNC_SNPRINTF dnl dnl Checks for a fully C99 compliant snprintf, in particular checks dnl whether it does bounds checking and returns the correct string dnl length; does the same check for vsnprintf. If no working snprintf dnl or vsnprintf is found, request a replacement and warn the user dnl about it. Note: the mentioned replacement is freely available and dnl may be used in any project regardless of it's licence (just like dnl the autoconf special exemption). dnl dnl @category C dnl @author RĂ¼diger Kuhlmann dnl @version 2002-09-26 dnl @license AllPermissive AC_DEFUN([AC_FUNC_SNPRINTF], [AC_CHECK_FUNCS(snprintf vsnprintf) AC_MSG_CHECKING(for working snprintf) AC_CACHE_VAL(ac_cv_have_working_snprintf, [AC_TRY_RUN( [#include int main(void) { char bufs[5] = { 'x', 'x', 'x', '\0', '\0' }; char bufd[5] = { 'x', 'x', 'x', '\0', '\0' }; int i; i = snprintf (bufs, 2, "%s", "111"); if (strcmp (bufs, "1")) exit (1); if (i != 3) exit (1); i = snprintf (bufd, 2, "%d", 111); if (strcmp (bufd, "1")) exit (1); if (i != 3) exit (1); exit(0); }], ac_cv_have_working_snprintf=yes, ac_cv_have_working_snprintf=no, ac_cv_have_working_snprintf=cross)]) AC_MSG_RESULT([$ac_cv_have_working_snprintf]) AC_MSG_CHECKING(for working vsnprintf) AC_CACHE_VAL(ac_cv_have_working_vsnprintf, [AC_TRY_RUN( [#include #include int my_vsnprintf (char *buf, const char *tmpl, ...) { int i; va_list args; va_start (args, tmpl); i = vsnprintf (buf, 2, tmpl, args); va_end (args); return i; } int main(void) { char bufs[5] = { 'x', 'x', 'x', '\0', '\0' }; char bufd[5] = { 'x', 'x', 'x', '\0', '\0' }; int i; i = my_vsnprintf (bufs, "%s", "111"); if (strcmp (bufs, "1")) exit (1); if (i != 3) exit (1); i = my_vsnprintf (bufd, "%d", 111); if (strcmp (bufd, "1")) exit (1); if (i != 3) exit (1); exit(0); }], ac_cv_have_working_vsnprintf=yes, ac_cv_have_working_vsnprintf=no, ac_cv_have_working_vsnprintf=cross)]) AC_MSG_RESULT([$ac_cv_have_working_vsnprintf]) if test x$ac_cv_have_working_snprintf$ac_cv_have_working_vsnprintf != "xyesyes"; then AC_LIBOBJ(snprintf) AC_MSG_WARN([Replacing missing/broken (v)snprintf() with version from http://www.ijs.si/software/snprintf/.]) AC_DEFINE(PREFER_PORTABLE_SNPRINTF, 1, "enable replacement (v)snprintf if system (v)snprintf is broken") fi]) sympa-6.2.24/src/smtpc/smtpc.1.md0000644000175000017500000000723013216651447015431 0ustar rackeracke%SMTPC(1) # NAME smtpc - SMTP / LMTP client # SYNOPSIS `smtpc` `--esmtp` _host_`:`_port_ `-f` _envelope_@_sen.der_ [ _options_... ] [ `--` ] _recipient_@_add.ress_ ... `smtpc` `--lmtp` _host_`:`_port_ `-f` _envelope_@_sen.der_ [ _options_... ] [ `--` ] _recipient_@_add.ress_ ... `smtpc` `--lmtp` _path_ `-f` _envelope_@_sen.der_ [ _options_... ] [ `--` ] _recipient_@_add.ress_ ... # DESCRIPTION **smtpc** is an email client. It reads a message from standard input and submits it to email server through socket. ## Options Any options not listed here are silently ignored. * `--dump` Show dialog in the session. * `--esmtp` _host_[:_port_] Uses TCP socket and ESMTP protocol to submit message. Either this option or `--lmtp` option is required. If _host_ is the IPv6 address, it must be enclosed in [...] to avoid confusion with colon separating _host_ and _port_, e.g. "`[::1]`", "`[::1]:587`". If _port_ is omitted, "25" is used. * `-f` _envelope_@_sen.der_, `-f`_envelope_@_sen.der_ Specifys envelope sender. This option is required. To specify "null envelope sender", use a separate empty argument or "`<>`". * `--iam` _host.name_ Specifys host name or IP address literal used in EHLO or LHLO request. Default is "`localhost`". * `--lmtp` _host_[:_port_], `--lmtp` _path_ Uses TCP or Unix domain socket and LMTP protocol to submit message. Either this option or `--esmtp` option is required. If _port_ is omitted, "24" is used. _path_ must be full path to socket file. * `-N` _dsn_, `-N`_dsn_ Controls delivery status notification. _dsn_ may be single word "`NEVER`" or one or more of words "`SUCCESS`", "`FAILURE`" and "`DELAY`" separated by comma. If this option is not given, delivery status notification will be controlled by server. * `--smtputf8` Enables support for SMTPUTF8 extension. **smtpc** detects valid UTF-8 sequence in envelope and message header, then requests this extension as neccessity. * `-V` _envid_, `-V`_envid_ Specifys envelope ID. * `--verbose` Output the last response from the server to standard output. * `--` Terminates options. Remainder of command line arguments are considered to be recipient addresses, even if any of them begin with "`-`". * _recipent_@_add.ress_ ... Recipients to whom the message would be delivered. At least one recipient is required. ## Exit status * `0` Message was successfully submitted. * `253` Message was rejected by server. * `254` The server returns malformed or illegal response. * `255` Network error occurred. ## SMTP extensions **smtpc** supports following extensions. * **8-bit MIME Transport** (RFC 6152) **smtpc** requests this extension if message contains octets with high bit. * **Delivery Status Notification** (RFC 3461) **smtpc** issues ORCPT parameters. See also `-N` and `-V` options. * **Internationalized Email** (RFC 6531) Experimentally supported. See `--smtputf8` option. * **Message Size Declaration** (RFC 1870) Estimated size of the message is informed to the server. # LIMITATIONS **smtpc** provides the feature of SMTP / LMTP client submitting messages to particular server. It will never provide extensive features such as message queuing, retry after temporary failure, routing using MX DNS record and so on. Once the server rejects delivery, **smtpc** exits and message is discarded. # KNOWN BUGS * If NUL octets (\\0) are included in messages, they are transmitted to the server. # SEE ALSO sendmail(1) # HISTORY **smtpc** was initially written for Sympa project by IKEDA Soji . sympa-6.2.24/src/smtpc/configure.gnu0000644000175000017500000000046513216651447016321 0ustar rackeracke# $Id$ args= for arg; do case "$arg" in *\'*) arg=`cat <. */ #include #include extern size_t utf8_check(const unsigned char *, const size_t); sympa-6.2.24/src/sbin/0000755000175000017500000000000013216651447013425 5ustar rackerackesympa-6.2.24/src/sbin/sympa_wizard.pl.in0000644000175000017500000005504713216651447017113 0ustar rackeracke#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # # 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, see . use lib '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use Sys::Hostname qw(); use Sympa::ConfDef; use Sympa::Constants; use Sympa::ModDef; my $with_CPAN; # check if module "CPAN" installed. my $modfail; # any of required modules are not installed. BEGIN { $with_CPAN = eval { require CPAN; }; $modfail = !eval { require Conf; require Sympa::Language; require Sympa::Tools::Text; }; } # Detect console encoding. if (-t) { no warnings; eval { require Encode::Locale; }; unless ($EVAL_ERROR or Encode::resolve_alias($Encode::Locale::ENCODING_CONSOLE_IN) eq 'ascii' or Encode::resolve_alias($Encode::Locale::ENCODING_CONSOLE_OUT) eq 'ascii') { binmode(STDIN, ':encoding(console_in):bytes'); binmode(STDOUT, ':encoding(console_out):bytes'); binmode(STDERR, ':encoding(console_out):bytes'); } } # Set language context if possible. if ($modfail) { no warnings; *gettext = sub { $_[1] ? sprintf('%*s', $_[1], $_[0]) : $_[0] }; eval { require Text::Wrap; }; if ($EVAL_ERROR) { *Sympa::Tools::Text::wrap_text = sub {"$_[1]$_[0]\n"}; } else { $Text::Wrap::columns = 78; *Sympa::Tools::Text::wrap_text = sub { Text::Wrap::wrap($_[1], $_[2], $_[0]) . "\n"; }; } } else { no warnings; my $language = Sympa::Language->instance; *gettext = sub { $_[1] ? Sympa::Tools::Text::pad($language->gettext($_[0]), $_[1]) : $language->gettext($_[0]); }; *gettext_strftime = sub { $language->gettext_strftime(@_) }; my $lang = $ENV{'LANGUAGE'} || $ENV{'LC_ALL'} || $ENV{'LANG'}; $lang =~ s/\..*// if $lang; $language->set_lang($lang, 'en-US', 'en'); } ## sympa configuration file my $sympa_conf = Sympa::Constants::CONFIG; my %options; GetOptions( \%options, 'target=s', 'create:s', # parameter is optional and only "sympa.conf" is allowed. 'batch', 'display', 'check', 'help|h', 'version|v', ); if ($options{help}) { pod2usage(); } elsif ($options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } elsif (defined $options{create}) { create_configuration(); } elsif ($options{display}) { display_configuration(); } elsif ($options{check}) { check_cpan(); } else { my %user_param; foreach my $arg (@ARGV) { # check for key/values settings if ($arg =~ /\A(\w+)=(.+)/) { $user_param{$1} = $2; } else { die "$0: Invalid commandline argument: $arg\n"; } } edit_configuration(%user_param); } exit 0; sub create_configuration { my $conf; if ($options{create} eq '' or $options{create} eq 'sympa.conf') { $conf = $options{target} ? $options{target} : $sympa_conf; } else { pod2usage("$options{create} is not a valid argument"); exit 1; } if (-f $conf) { print STDERR "$conf file already exists, exiting\n"; exit 1; } my $umask = umask 037; unless (open NEWF, '>', $conf) { umask $umask; die "$0: " . sprintf(gettext("Unable to open %s : %s"), $conf, $ERRNO) . "\n"; } umask $umask; if ($options{create} eq 'sympa.conf') { # print NEWF <{'name'}) { $title = gettext($param->{'gettext_id'}) if $param->{'gettext_id'}; next; } next unless $param->{'file'}; ##next unless defined $param->{'default'} or defined $param->{'sample'}; if ($title) { printf NEWF "###\\\\\\\\ %s ////###\n\n", $title; undef $title; } printf NEWF "## %s\n", $param->{'name'}; if ($param->{'gettext_id'}) { print NEWF Sympa::Tools::Text::wrap_text( gettext($param->{'gettext_id'}), '## ', '## '); } print NEWF Sympa::Tools::Text::wrap_text( gettext($param->{'gettext_comment'}), '## ', '## ') if $param->{'gettext_comment'}; if (defined $param->{'sample'}) { printf NEWF '## ' . gettext('Example: ') . "%s\t%s\n", $param->{'name'}, $param->{'sample'}; } if (defined $param->{'default'}) { printf NEWF "#%s\t%s\n", $param->{'name'}, $param->{'default'}; } elsif ($param->{'optional'}) { printf NEWF "#%s\t\n", $param->{'name'}; } else { printf NEWF "#%s\t%s\n", $param->{'name'}, gettext("(You must define this parameter)"); } print NEWF "\n"; } close NEWF; print STDERR "$conf file has been created\n"; } sub display_configuration { die "$0: You must run as superuser.\n" if $UID; die "$0: Installation of Sympa has not been completed.\n" . "Run sympa_wizard.pl --check\n" if $modfail; # Load sympa config (but not using database) unless (defined Conf::load($sympa_conf, 1)) { printf STDERR "$0: Unable to load sympa configuration, file %s or one of the virtual host robot.conf files contain errors. Exiting.\n", $sympa_conf; exit 1; } my ($var, $disp); print "[SYMPA]\n"; foreach my $key (sort keys %Conf::Conf) { next if grep { $key eq $_ } qw(auth_services blacklist cas_number crawlers_detection generic_sso_number ldap ldap_number listmasters locale2charset nrcpt_by_domain robot_by_http_host request robot_name robots source_file sympa trusted_applications use_passwd); $var = $Conf::Conf{$key}; if ($key eq 'automatic_list_families') { $disp = join ';', map { my $name = $_; join ':', map { sprintf '%s=%s', $_, $var->{$name}{$_} } grep { !/\Aescaped_/ } sort keys %{$var->{$name} || {}}; } sort keys %{$var || {}}; } elsif (ref $var eq 'ARRAY') { $disp = join(',', map { defined $_ ? $_ : '' } @$var); } else { $disp = defined $var ? $var : ''; } printf "%s=\"%s\"\n", $key, $disp; } } sub edit_configuration { my %user_param = @_; die "$0: You must run as superuser.\n" if $UID; die "$0: Installation of Sympa has not been completed.\n" . "Run sympa_wizard.pl --check\n" if $modfail; # complement required fields. foreach my $param (@Sympa::ConfDef::params) { next unless $param->{'name'}; if ($param->{'name'} eq 'domain') { $param->{'default'} = Sys::Hostname::hostname(); } elsif ($param->{'name'} eq 'wwsympa_url') { $param->{'default'} = sprintf 'http://%s/sympa', Sys::Hostname::hostname(); } elsif ($param->{'name'} eq 'listmaster') { $param->{'default'} = sprintf 'your_email_address@%s', Sys::Hostname::hostname(); } } ## Load sympa config (but not using database) unless (defined Conf::load($sympa_conf, 1)) { printf STDERR "$0: Unable to load sympa configuration, file %s or one of the virtual host robot.conf files contain errors. Exiting.\n", $sympa_conf; exit 1; } my $somechange = 0; my @new_sympa_conf; my $title = undef; # dynamic defaults my $domain = Sys::Hostname::hostname(); my $http_host = "http://$domain"; ## Edition mode foreach my $param (@Sympa::ConfDef::params) { unless ($param->{'name'}) { $title = gettext($param->{'gettext_id'}) if $param->{'gettext_id'}; next; } my $file = $param->{'file'}; my $name = $param->{'name'}; my $query = $param->{'gettext_id'} || ''; $query = gettext($query) if $query; my $advice = $param->{'gettext_comment'}; $advice = gettext($advice) if $advice; my $sample = $param->{'sample'}; my $current_value; next unless $file; if ($file eq 'sympa.conf' or $file eq 'wwsympa.conf') { $current_value = $Conf::Conf{$name}; $current_value = '' unless defined $current_value; } else { next; } if ($title) { ## write to conf file push @new_sympa_conf, sprintf "###\\\\\\\\ %s ////###\n\n", $title; } my $new_value = ''; if ($options{batch}) { if (exists $user_param{$name}) { $new_value = $user_param{$name}; } } elsif ($param->{'edit'} and $param->{'edit'} eq '1') { print "\n\n** $title **\n" if $title; print "\n"; print Sympa::Tools::Text::wrap_text($query || '', '* ', ' '); print Sympa::Tools::Text::wrap_text($advice, ' ... ', ' ') if $advice; printf(gettext('%s [%s] : '), $name, $current_value); $new_value = ; chomp $new_value; } if ($new_value eq '') { $new_value = $current_value; } undef $title; ## Skip empty parameters next if $new_value eq '' and !$sample; ## param is an ARRAY if (ref($new_value) eq 'ARRAY') { $new_value = join ',', @{$new_value}; } unless ($file eq 'sympa.conf' or $file eq 'wwsympa.conf') { printf STDERR gettext("Incorrect parameter definition: %s\n"), $file; } if ($new_value eq '') { next unless $sample; push @new_sympa_conf, Sympa::Tools::Text::wrap_text($query, '## ', '## '); if (defined $advice and length $advice) { push @new_sympa_conf, Sympa::Tools::Text::wrap_text($advice, '## ', '## '); } push @new_sympa_conf, "# $name\t$sample\n\n"; } else { push @new_sympa_conf, Sympa::Tools::Text::wrap_text($query, '## ', '## '); if (defined $advice and length $advice) { push @new_sympa_conf, Sympa::Tools::Text::wrap_text($advice, '## ', '## '); } if ($current_value ne $new_value) { push @new_sympa_conf, "# was $name $current_value\n"; $somechange = 1; } push @new_sympa_conf, "$name\t$new_value\n\n"; } } if ($somechange) { my $date = gettext_strftime("%d.%b.%Y-%H.%M.%S", localtime(time)); ## Keep old config file unless (rename $sympa_conf, $sympa_conf . '.' . $date) { warn sprintf(gettext("Unable to rename %s : %s"), $sympa_conf, $ERRNO); } ## Write new config file my $umask = umask 037; unless (open(SYMPA, "> $sympa_conf")) { umask $umask; die "$0: " . sprintf(gettext("Unable to open %s : %s"), $sympa_conf, $ERRNO) . "\n"; } umask $umask; chown [getpwnam(Sympa::Constants::USER)]->[2], [getgrnam(Sympa::Constants::GROUP)]->[2], $sympa_conf; print SYMPA @new_sympa_conf; close SYMPA; printf gettext( "%s have been updated.\nPrevious versions have been saved as %s.\n" ), $sympa_conf, "$sympa_conf.$date"; } } sub check_cpan { print gettext( "############################################################################## # This process will help you install all Perl (CPAN) modules required by Sympa # software. # Sympa requires from 50 to 65 additional Perl modules to run properly. # The whole installation process should take around 15 minutes. # You'll first have to configure the CPAN shell itself and select your # favourite CPAN server. # Note that you might prefer to install the required Perl modules using your # favourite DEB/RPM mechanism. # Feel free to interrupt the process if needed ; you can restart it safely # afterward. ############################################################################## Press the Enter key to continue..." ) . "\n"; my $rep = ; print "\n"; # Choose default DBD module if it has not been defined. my $db_type; if (open my $fh, '<', $sympa_conf) { foreach my $line (<$fh>) { if ($line =~ /\Adb_type\s+(\S*)/) { $db_type = $1; } } close $fh; } if ($db_type and exists $Sympa::ModDef::cpan_modules{'DBD::' . $db_type}) { $Sympa::ModDef::cpan_modules{'DBD::' . $db_type}->{mandatory} = 1; } else { my @dbd = ( 'MySQL/MariaDB' => 'DBD::mysql', 'PostgreSQL' => 'DBD::Pg', 'SQLite' => 'DBD::SQLite', 'Oracle' => 'DBD::Oracle', 'Sybase' => 'DBD::Sybase', ); my $selected; while (1) { print "\n" . gettext('Which RDBMS will you use for core database:') . "\n"; for (my $i = 0; $i < scalar @dbd; $i += 2) { printf "%d: %s\n", $i / 2 + 1, $dbd[$i]; } printf gettext('-> Select RDBMS [1-%d] '), scalar @dbd / 2; $selected = ; chomp $selected; last if $selected =~ /\A\d+\z/ and 0 < $selected and $selected * 2 <= scalar @dbd; } $Sympa::ModDef::cpan_modules{$dbd[$selected * 2 - 1]}->{mandatory} = 1; } ### REQ perl version print "\n" . gettext('Checking for PERL version:') . "\n\n"; my $rpv = $Sympa::ModDef::cpan_modules{"perl"}{'required_version'}; if ($] >= $Sympa::ModDef::cpan_modules{"perl"}{'required_version'}) { printf gettext('Your version of perl is OK (%s >= %s)') . "\n", $], $rpv; } else { printf gettext( "Your version of perl is TOO OLD (%s < %s)\nPlease INSTALL a new one !" ) . "\n", $], $rpv; } print "\n" . gettext('Checking for REQUIRED modules:') . "\n\n"; check_modules('y', \%Sympa::ModDef::cpan_modules, 'mandatory'); print "\n" . gettext('Checking for OPTIONAL modules:') . "\n\n"; check_modules('n', \%Sympa::ModDef::cpan_modules, 'optional'); print gettext( "******* NOTE ******* You can retrieve all theses modules from any CPAN server (for example ftp://ftp.pasteur.fr/pub/computing/CPAN/CPAN.html)" ) . "\n"; ###-------------------------- # reports modules status # $cpan_modules is the cpan_modules structure # $type is the type of modules (mandatory | optional) that should be installed ###-------------------------- } sub check_modules { # my($default, $todo, $versions, $opt_features) = @_; my ($default, $cpan_modules, $type) = @_; printf "%s%s%s\n", gettext('perl module', -24), gettext('from CPAN', -24), gettext('STATUS'); printf "%-24s%-24s%s\n", gettext('-----------'), gettext('---------'), gettext('------'); foreach my $mod (sort keys %$cpan_modules) { ## Only check modules of the expected type if ($type eq 'mandatory') { next unless ($cpan_modules->{$mod}{mandatory}); } elsif ($type eq 'optional') { next if ($cpan_modules->{$mod}{mandatory}); } ## Skip perl itself to prevent a huge upgrade next if ($mod eq 'perl'); printf("%-24s%-24s", $mod, $cpan_modules->{$mod}{package_name}); eval "require $mod"; if ($EVAL_ERROR) { ### not installed print gettext('was not found on this system.') . "\n"; install_module($mod, {'default' => $default}, $cpan_modules); } else { my ($vs, $v); ## MHonArc module does not provide its version the standard way if ($mod =~ /^MHonArc/i) { require "mhamain.pl"; $v = $mhonarc::VERSION; } else { $vs = "$mod" . "::VERSION"; { no strict 'refs'; $v = $$vs; } } my $rv = $cpan_modules->{$mod}{required_version} || "1.0"; ### OK: check version if ($v ge $rv) { printf gettext('OK (%-6s >= %s)') . "\n", $v, $rv; next; } else { printf gettext('version is too old (%s < %s)') . "\n", $v, $rv; printf gettext( '>>>>>>> You must update "%s" to version "%s" <<<<<<.') . "\n", $cpan_modules->{$mod}{package_name}, $cpan_modules->{$mod}{required_version}; install_module($mod, {'default' => $default}, $cpan_modules); } } } } ##---------------------- # Install a CPAN module ##---------------------- sub install_module { return unless $with_CPAN; my ($module, $options, $cpan_modules) = @_; my $default = $options->{'default'}; unless ($ENV{'FTP_PASSIVE'} and $ENV{'FTP_PASSIVE'} eq 1) { $ENV{'FTP_PASSIVE'} = 1; print "Setting FTP Passive mode\n"; } # This is required on RedHat 9 for DBD::mysql installation my $lang; if ($ENV{'LANG'} and $ENV{'LANG'} =~ /UTF-8/) { $lang = $ENV{'LANG'}; $ENV{'LANG'} = 'C'; } unless ($EUID == 0) { printf gettext('## You need root privileges to install %s module. ##') . "\n", $module; print gettext( '## Press the Enter key to continue checking modules. ##') . "\n"; my $t = ; return undef; } unless ($options->{'force'}) { print Sympa::Tools::Text::wrap_text( sprintf( gettext('-> Usage of this module: %s') . "\n", gettext($cpan_modules->{$module}{'gettext_id'}) ), '', ' ' ) if ($cpan_modules->{$module}{'gettext_id'}); print Sympa::Tools::Text::wrap_text( sprintf( gettext('-> Prerequisites: %s') . "\n", gettext($cpan_modules->{$module}{'gettext_comment'}) ), '', ' ' ) if ($cpan_modules->{$module}{'gettext_comment'}); printf gettext('-> Install module %s ? [%s] '), $module, $default; my $answer = ; chomp $answer; $answer ||= $default; return unless ($answer =~ /^y$/i); } $CPAN::Config->{'inactivity_timeout'} = 0; ## disable timeout to prevent timeout during modules installation $CPAN::Config->{'colorize_output'} = 1; $CPAN::Config->{'build_requires_install_policy'} = 'yes'; ## automatically installed prerequisites without asking $CPAN::Config->{'prerequisites_policy'} = 'follow'; ## build prerequisites automatically $CPAN::Config->{'load_module_verbosity'} = 'none'; ## minimum verbosity during module loading $CPAN::Config->{'tar_verbosity'} = 'none'; ## minimum verbosity with tar command # CPAN::Shell->clean($module) if ($options->{'force'}); # CPAN::Shell->make($module); # if ($options->{'force'}) { # CPAN::Shell->force('test', $module); # } else { # CPAN::Shell->test($module); # } # # Could use CPAN::Shell->force('install') if make test failed CPAN::Shell->install($module); ## Check if module has been successfuly installed unless (eval "require $module") { ## Prevent recusive calls if already in force mode if ($options->{'force'}) { printf gettext( "Installation of %s still FAILED. You should download the tar.gz from http://search.cpan.org and install it manually." ), $module; my $answer = ; } else { printf gettext( 'Installation of %s FAILED. Do you want to force the installation of this module? (y/N) ' ), $module; my $answer = ; chomp $answer; if ($answer =~ /^y/i) { install_module($module, {'force' => 1}, $cpan_modules); } } } # Restore lang $ENV{'LANG'} = $lang if $lang; } __END__ =encoding utf-8 =head1 NAME sympa_wizard, sympa_wizard.pl - Help Performing Sympa Initial Setup =head1 SYNOPSIS C S<[ C<--batch> [ I=I ... ] ]> S<[ C<--check> ]> S<[ C<--create> [ C<--target=>I ] ]> S<[ C<--display> ]> S<[ C<-h, --help> ]> S<[ C<-v, --version> ]> =head1 OPTIONS =over 4 =item C Edit current Sympa configuration. =item C C<--batch> I=I ... Edit in batch mode. Arguments would include pairs of parameter name and value. =item C C<--check> Check CPAN modules needed for running Sympa. =item C C<--create> [ C<--target> I ] Creates a new F configuration file. =item C C<--display> Outputs all configuration parameters. =item C C<--help> Display usage instructions. =item C C<--version> Print version number. =back =head1 HISTORY This program was originally written by: =over 4 =item Serge Aumont =item Olivier SalaE<252>n =back C<--batch> and C<--display> options are added on Sympa 6.1.25 and 6.2.15. =cut sympa-6.2.24/src/sbin/sympa.pl.in0000644000175000017500000011603713216651447015530 0ustar rackeracke#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use Digest::MD5; use English qw(-no_match_vars); use Fcntl qw(); use File::Basename qw(); use File::Copy qw(); use File::Path qw(); use Getopt::Long; use Pod::Usage; use POSIX qw(); use Conf; use Sympa::Config_XML; use Sympa::Constants; use Sympa::DatabaseManager; use Sympa::Family; use Sympa::Language; use Sympa::List; use Sympa::Log; use Sympa::Mailer; use Sympa::Spindle::ProcessDigest; use Sympa::Spindle::ProcessRequest; use Sympa::Tools::Data; use Sympa::Upgrade; ## Init random engine srand(time()); # Check options. my %options; unless ( GetOptions( \%main::options, 'dump=s', 'debug|d', 'log_level=s', 'config|f=s', 'lang|l=s', 'mail|m', 'help|h', 'version|v', 'import=s', 'make_alias_file', 'lowercase', 'sync_list_db', 'md5_encode_password', 'close_list=s', 'rename_list=s', 'new_listname=s', 'new_listrobot=s', 'purge_list=s', 'create_list', 'instantiate_family=s', 'robot=s', 'add_list=s', 'modify_list=s', 'close_family=s', 'md5_digest=s', 'change_user_email', 'current_email=s', 'new_email=s', 'input_file=s', 'sync_include=s', 'upgrade', 'upgrade_shared', 'from=s', 'to=s', 'reload_list_config', 'list=s', 'quiet', 'close_unknown', 'test_database_message_buffer', 'conf_2_db', 'export_list', 'health_check', 'send_digest', 'keep_digest', 'upgrade_config_location', ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($main::options{'help'}) { pod2usage(0); } elsif ($main::options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; } my $log = Sympa::Log->instance; $log->{log_to_stderr} = 'notice,err' if $main::options{'upgrade'} || $main::options{'reload_list_config'} || $main::options{'test_database_message_buffer'} || $main::options{'conf_2_db'}; if ($main::options{'upgrade_config_location'}) { my $config_file = Conf::get_sympa_conf(); if (-f $config_file) { printf "Sympa configuration already located at %s\n", $config_file; exit 0; } my ($file, $dir, $suffix) = File::Basename::fileparse($config_file); my $old_dir = $dir; $old_dir =~ s/sympa\///; # Try to create config path if it does not exist unless (-d $dir) { my $error; File::Path::make_path( $dir, { mode => 0755, owner => 'sympa', group => 'sympa', error => \$error } ); if (@$error) { my $diag = pop @$error; my ($target, $error) = %$diag; die "Unable to create $target: $error"; } } # Check ownership of config folder my @stat = stat($dir); my $user = (getpwuid $stat[4])[0]; if ($user ne 'sympa') { die "Config dir $dir exists but is not owned by sympa (owned by $user)"; } # Check permissions on config folder if (($stat[2] & Fcntl::S_IRWXU()) != Fcntl::S_IRWXU()) { die "Config dir $dir exists, but sympa does not have rwx permissions on it"; } # Move files from old location to new one opendir(my $dh, $old_dir) or die("Could not open $dir for reading"); my @files = grep(/^(ww)?sympa\.conf.*$/, readdir($dh)); closedir($dh); foreach my $file (@files) { unless (File::Copy::move("$old_dir/$file", "$dir/$file")) { die sprintf 'Could not move %s/%s to %s/%s: %s', $old_dir, $file, $dir, $file, $ERRNO; } } printf "Sympa configuration moved to $dir\n"; exit 0; } elsif ($main::options{'health_check'}) { ## Health check ## Load configuration file. Ignoring database config for now: it avoids ## trying to load a database that could not exist yet. unless (Conf::load(Conf::get_sympa_conf(), 'no_db')) { #FIXME: force reload die sprintf "Configuration file %s has errors.\n", Conf::get_sympa_conf(); } ## Open the syslog and say we're read out stuff. $log->openlog( $Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}, service => 'sympa/health_check' ); ## Setting log_level using conf unless it is set by calling option if ($main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; $log->syslog( 'info', 'Configuration file read, log level set using options: %s', $main::options{'log_level'} ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog( 'info', 'Configuration file read, default log level %s', $Conf::Conf{'log_level'} ); } if (Conf::cookie_changed()) { die sprintf 'sympa.conf/cookie parameter has changed. You may have severe inconsitencies into password storage. Restore previous cookie or write some tool to re-encrypt password in database and check spools contents (look at %s/cookies.history file).', $Conf::Conf{'etc'}; } ## Check database connectivity and probe database unless (Sympa::DatabaseManager::probe_db()) { die sprintf "Database %s defined in sympa.conf has not the right structure or is unreachable. verify db_xxx parameters in sympa.conf\n", $Conf::Conf{'db_name'}; } ## Now trying to load full config (including database) unless (Conf::load()) { #FIXME: load Site, then robot cache die sprintf "Unable to load Sympa configuration, file %s or any of the virtual host robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } ## Change working directory. if (!chdir($Conf::Conf{'home'})) { printf STDERR "Can't chdir to %s: %s\n", $Conf::Conf{'home'}, $ERRNO; exit 1; } ## Check for several files. unless (Conf::checkfiles_as_root()) { printf STDERR "Missing files.\n"; exit 1; } ## Check that the data structure is uptodate unless (Conf::data_structure_uptodate()) { printf STDOUT "Data structure was not updated; you should run sympa.pl --upgrade to run the upgrade process.\n"; } exit 0; } my $default_lang; my $language = Sympa::Language->instance; my $mailer = Sympa::Mailer->instance; _load(); $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); # Set the User ID & Group ID for the process $GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2]; $UID = $EUID = (getpwnam(Sympa::Constants::USER))[2]; ## Required on FreeBSD to change ALL IDs ## (effective UID + real UID + saved UID) POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]); POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]); ## Check if the UID has correctly been set (useful on OS X) unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2]) && ($UID == (getpwnam(Sympa::Constants::USER))[2])) { die "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n"; } # Sets the UMASK umask(oct($Conf::Conf{'umask'})); ## Most initializations have now been done. $log->syslog('notice', 'Sympa %s Started', Sympa::Constants::VERSION()); # Check for several files. #FIXME: This would be done in --health_check mode. unless (Conf::checkfiles()) { die "Missing files.\n"; ## No return. } # Daemon called for dumping subscribers list if ($main::options{'dump'}) { my ($all_lists, $list); if ($main::options{'dump'} eq 'ALL') { $all_lists = Sympa::List::get_lists('*'); } else { ## The parameter can be a list address unless ($main::options{'dump'} =~ /\@/) { $log->syslog('err', 'Incorrect list address %s', $main::options{'dump'}); exit; } my $list = Sympa::List->new($main::options{'dump'}); unless (defined $list) { $log->syslog('err', 'Unknown list %s', $main::options{'dump'}); exit; } push @$all_lists, $list; } foreach my $list (@$all_lists) { unless ($list->dump()) { print STDERR "Could not dump list(s)\n"; } } exit 0; } elsif ($main::options{'make_alias_file'}) { my $robots = $main::options{'robot'} || '*'; my @robots; if ($robots eq '*') { @robots = Sympa::List::get_robots(); } else { @robots = grep { length $_ } split(/[\s,]+/, $robots); } exit 0 unless @robots; # There may be multiple aliases files. Give each of them suffixed # name. my ($basename, %robots_of, %sympa_aliases); $basename = sprintf '%s/sympa_aliases.%s', $Conf::Conf{'tmpdir'}, $PID; foreach my $robot (@robots) { my $file = Conf::get_robot_conf($robot, 'sendmail_aliases'); $robots_of{$file} ||= []; push @{$robots_of{$file}}, $robot; } if (1 < scalar(keys %robots_of)) { my $i = 0; %sympa_aliases = map { $i++; map { $_ => sprintf('%s.%03d', $basename, $i) } @{$robots_of{$_}} } sort keys %robots_of; } else { %sympa_aliases = map { $_ => $basename } @robots; } # Create files. foreach my $sympa_aliases (values %sympa_aliases) { my $fh; unless (open $fh, '>', $sympa_aliases) { # truncate if exists printf STDERR "Unable to create %s: %s\n", $sympa_aliases, $ERRNO; exit 1; } close $fh; } # Write files. foreach my $robot (sort @robots) { my $all_lists = Sympa::List::get_lists($robot); my $alias_manager = Conf::get_robot_conf($robot, 'alias_manager'); my $sympa_aliases = $sympa_aliases{$robot}; my $fh; unless (open $fh, '>>', $sympa_aliases) { # append printf STDERR "Unable to create %s: %s\n", $sympa_aliases, $ERRNO; exit 1; } printf $fh "#\n#\tAliases for all Sympa lists open on %s\n#\n", $robot; close $fh; foreach my $list (@{$all_lists || []}) { next unless $list->{'admin'}{'status'} eq 'open'; system($alias_manager, 'add', $list->{'name'}, $list->{'domain'}, $sympa_aliases); } } if (1 < scalar(keys %robots_of)) { printf "Sympa aliases files %s.??? were made. You probably need to install them in your SMTP engine.\n", $basename; } else { printf "Sympa aliases file %s was made. You probably need to install it in your SMTP engine.\n", $basename; } exit 0; } elsif ($main::options{'md5_digest'}) { my $md5 = Digest::MD5::md5_hex($main::options{'md5_digest'}); printf "md5 digest : $md5 \n"; exit 0; } elsif ($main::options{'import'}) { #FIXME The parameter should be a list address. unless ($main::options{'import'} =~ /\@/) { printf STDERR "Incorrect list address %s\n", $main::options{'import'}; exit 1; } my $list; unless ($list = Sympa::List->new($main::options{'import'})) { printf STDERR "Unknown list name %s\n", $main::options{'import'}; exit 1; } my $dump = do { local $RS; }; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'import', dump => $dump, force => 1, sender => Sympa::get_address($list, 'listmaster'), scenario_context => {skip => 1}, ); unless ($spindle and $spindle->spin) { printf STDERR "Failed to add email addresses to %s\n", $list; exit 1; } my $status = _report($spindle); printf STDERR "Total imported subscribers: %d\n", scalar(grep { $_->[1] eq 'notice' and $_->[2] eq 'now_subscriber' } @{$spindle->{stash} || []}); exit($status ? 0 : 1); } elsif ($main::options{'md5_encode_password'}) { printf STDERR "Obsoleted. Use upgrade_sympa_password.pl.\n"; exit 0; } elsif ($main::options{'lowercase'}) { print STDERR "Working on user_table...\n"; my $total = _lowercase_field('user_table', 'email_user'); if (defined $total) { print STDERR "Working on subscriber_table...\n"; my $total_sub = _lowercase_field('subscriber_table', 'user_subscriber'); if (defined $total_sub) { $total += $total_sub; } } unless (defined $total) { print STDERR "Could not work on dabatase.\n"; exit 1; } printf STDERR "Total lowercased rows: %d\n", $total; exit 0; } elsif ($main::options{'close_list'}) { my ($listname, $robot_id) = split /\@/, $main::options{'close_list'}, 2; my $current_list = Sympa::List->new($listname, $robot_id); unless ($current_list) { printf STDERR "Incorrect list name %s.\n", $main::options{'close_list'}; exit 1; } my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot_id, action => 'close_list', current_list => $current_list, sender => Sympa::get_address($robot_id, 'listmaster'), scenario_context => {skip => 1}, ); unless ($spindle and $spindle->spin and _report($spindle)) { printf STDERR "Could not close list %s\n", $current_list->get_id; exit 1; } exit 0; } elsif ($main::options{'change_user_email'}) { unless ($main::options{'current_email'} and $main::options{'new_email'}) { print STDERR "Missing current_email or new_email parameter\n"; exit 1; } my $spindle = Sympa::Spindle::ProcessRequest->new( context => [Sympa::List::get_robots()], action => 'move_user', current_email => $main::options{'current_email'}, email => $main::options{'new_email'}, sender => Sympa::get_address('*', 'listmaster'), scenario_context => {skip => 1}, ); unless ($spindle and $spindle->spin and _report($spindle)) { printf STDERR "Failed to change user email address %s to %s\n", $main::options{'current_email'}, $main::options{'new_email'}; exit 1; } exit 0; } elsif ($main::options{'purge_list'}) { my ($listname, $robot_id) = split /\@/, $main::options{'purge_list'}, 2; my $current_list = Sympa::List->new($listname, $robot_id); unless ($current_list) { print STDERR "Incorrect list name $main::options{'purge_list'}\n"; exit 1; } my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot_id, action => 'close_list', current_list => $current_list, mode => 'purge', scenario_context => {skip => 1}, ); unless ($spindle and $spindle->spin and _report($spindle)) { printf STDERR "Could not purge list %s\n", $current_list->get_id; exit 1; } exit 0; } elsif ($main::options{'rename_list'}) { my $current_list = Sympa::List->new(split(/\@/, $main::options{'rename_list'}, 2), {just_try => 1}); unless ($current_list) { printf STDERR "Incorrect list name %s\n", $main::options{'rename_list'}; exit 1; } my $listname = $main::options{'new_listname'}; unless (defined $listname and length $listname) { print STDERR "Missing parameter new_listname\n"; exit 1; } my $robot_id = $main::options{'new_listrobot'}; unless (defined $robot_id) { $robot_id = $current_list->{'domain'}; } else { unless (length $robot_id and Conf::valid_robot($robot_id)) { print STDERR "Unknown robot \"%s\"\n"; exit 1; } } my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot_id, action => 'move_list', current_list => $current_list, listname => $listname, sender => Sympa::get_address($robot_id, 'listmaster'), scenario_context => {skip => 1}, ); unless ($spindle and $spindle->spin and _report($spindle)) { printf STDERR "Could not rename list %s to %s%s\n", $current_list->get_id, $listname, $robot_id; exit 1; } exit 0; } elsif ($main::options{'test_database_message_buffer'}) { printf "Deprecated. Size of messages no longer limited by database packet size.\n"; exit 1; } elsif ($main::options{'conf_2_db'}) { printf "Sympa is going to store %s in database conf_table. This operation do NOT remove original files\n", Conf::get_sympa_conf(); if (Conf::conf_2_db()) { printf "Done"; } else { printf "an error occur"; } exit 1; } elsif ($main::options{'create_list'}) { my $robot = $main::options{'robot'} || $Conf::Conf{'host'}; unless ($main::options{'input_file'}) { print STDERR "Error : missing 'input_file' parameter\n"; exit 1; } my $fh; unless (open $fh, '<', $main::options{'input_file'}) { print STDERR "Unable to open %s: %s\n", $main::options{'input_file'}, $ERRNO; exit 1; } my $config = Sympa::Config_XML->new($fh); close $fh; unless (defined $config->createHash()) { print STDERR "Error in representation data with these XML data\n"; exit 1; } my $hash = $config->getHash(); my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'create_list', listname => $hash->{config}{listname}, parameters => {%{$hash->{config} || {}}, template => $hash->{type}}, sender => Sympa::get_address($robot, 'listmaster'), scenario_context => {skip => 1} ); unless ($spindle and $spindle->spin and _report($spindle)) { printf STDERR "Could not create list %s\n", $hash->{config}{listname}; exit 1; } exit 0; } elsif ($main::options{'instantiate_family'}) { my $robot = $main::options{'robot'} || $Conf::Conf{'host'}; my $family_name; unless ($family_name = $main::options{'instantiate_family'}) { print STDERR "Error : missing family parameter\n"; exit 1; } my $family; unless ($family = Sympa::Family->new($family_name, $robot)) { print STDERR "The family $family_name does not exist, impossible instantiation\n"; exit 1; } unless ($main::options{'input_file'}) { print STDERR "Error : missing input_file parameter\n"; exit 1; } unless (-r $main::options{'input_file'}) { print STDERR "Unable to read $main::options{'input_file'} file"; exit 1; } unless ( $family->instantiate( $main::options{'input_file'}, close_unknown => $main::options{'close_unknown'}, quiet => $main::options{quiet}, ) ) { print STDERR "\nImpossible family instantiation : action stopped \n"; exit 1; } my %result; my $err = $family->get_instantiation_results(\%result); close INFILE; unless ($main::options{'quiet'}) { print STDOUT "@{$result{'info'}}"; print STDOUT "@{$result{'warn'}}"; } if ($err) { print STDERR "@{$result{'errors'}}"; } exit 0; } elsif ($main::options{'add_list'}) { my $robot = $main::options{'robot'} || $Conf::Conf{'host'}; my $family_name; unless ($family_name = $main::options{'add_list'}) { print STDERR "Error : missing family parameter\n"; exit 1; } my $family; unless ($family = Sympa::Family->new($family_name, $robot)) { print STDERR "The family %s does not exist, impossible to add a list\n", $family_name; exit 1; } unless ($main::options{'input_file'}) { print STDERR "Error : missing 'input_file' parameter\n"; exit 1; } my $fh; unless (open $fh, '<', $main::options{'input_file'}) { printf STDERR "\n Impossible to open input file : %s\n", $ERRNO; exit 1; } # get list data my $config = Sympa::Config_XML->new($fh); close $fh; unless (defined $config->createHash()) { print STDERR "Error in representation data with these XML data\n"; exit 1; } my $hash = $config->getHash(); my $spindle = Sympa::Spindle::ProcessRequest->new( context => $family, action => 'create_automatic_list', listname => $hash->{config}{listname}, parameters => $hash->{config}, sender => Sympa::get_address($family, 'listmaster'), scenario_context => {skip => 1}, ); unless ($spindle and $spindle->spin and _report($spindle)) { print STDERR "Impossible to add a list %s to the family %s\n", $hash->{config}{listname}, $family_name; exit 1; } my $list = Sympa::List->new($hash->{config}{listname}, $family->{'robot'}); unless (File::Copy::copy($main::options{'input_file'}, $list->{'dir'} . '/instance.xml')) { $list->set_status_error_config('error_copy_file', $family->{'name'}); print STDERR "Impossible to copy the XML file in the list directory, the list is set in status error_config.\n"; } exit 0; } elsif ($main::options{'sync_include'}) { my $list = Sympa::List->new($main::options{'sync_include'}); unless (defined $list) { print STDERR "Incorrect list name $main::options{'sync_include'}\n"; exit 1; } unless (defined $list->sync_include()) { print STDERR "Failed to synchronize list members\n"; exit 1; } printf "Members of list %s have been successfully update.\n", $list->get_id; exit 0; ## Migration from one version to another } elsif ($main::options{'upgrade'}) { $log->syslog('notice', "Upgrade process..."); $main::options{'from'} ||= Sympa::Upgrade::get_previous_version(); $main::options{'to'} ||= Sympa::Constants::VERSION; if ($main::options{'from'} eq $main::options{'to'}) { $log->syslog('notice', 'Current version: %s; no upgrade is required', $main::options{'to'}); exit 0; } else { $log->syslog('notice', "Upgrading from %s to %s...", $main::options{'from'}, $main::options{'to'}); } unless ( Sympa::Upgrade::upgrade($main::options{'from'}, $main::options{'to'})) { $log->syslog('err', "Migration from %s to %s failed", $main::options{'from'}, $main::options{'to'}); exit 1; } $log->syslog('notice', 'Upgrade process finished'); Sympa::Upgrade::update_version(); exit 0; } elsif ($main::options{'upgrade_shared'}) { printf STDERR "Obsoleted. Use upgrade_shared_repository.pl.\n"; exit 0; } elsif ($main::options{'reload_list_config'}) { if ($main::options{'list'}) { $log->syslog('notice', 'Loading list %s...', $main::options{'list'}); my $list = Sympa::List->new($main::options{'list'}, '', {'reload_config' => 1, 'force_sync_admin' => 1}); unless (defined $list) { print STDERR "Error : incorrect list name '$main::options{'list'}'\n"; exit 1; } } else { $log->syslog('notice', "Loading ALL lists..."); my $all_lists = Sympa::List::get_lists('*', 'reload_config' => 1, 'force_sync_admin' => 1); } $log->syslog('notice', '...Done.'); exit 0; } ########################################## elsif ($main::options{'modify_list'}) { my $robot = $main::options{'robot'} || $Conf::Conf{'host'}; my $family_name; unless ($family_name = $main::options{'modify_list'}) { print STDERR "Error : missing family parameter\n"; exit 1; } print STDOUT "\n************************************************************\n"; my $family; unless ($family = Sympa::Family->new($family_name, $robot)) { print STDERR "The family $family_name does not exist, impossible to modify the list.\n"; exit 1; } unless ($main::options{'input_file'}) { print STDERR "Error : missing input_file parameter\n"; exit 1; } unless (open INFILE, $main::options{'input_file'}) { print STDERR "Unable to open $main::options{'input_file'}) file"; exit 1; } my $result; unless ($result = $family->modify_list(\*INFILE)) { print STDERR "\nImpossible to modify the family list : action stopped. \n"; exit 1; } print STDOUT "\n************************************************************\n"; unless (defined $result->{'ok'}) { printf STDERR "\n%s\n", join("\n", @{$result->{'string_info'}}); print STDERR "\nThe action has been stopped because of error :\n"; printf STDERR "\n%s\n", join("\n", @{$result->{'string_error'}}); exit 1; } close INFILE; printf STDOUT "\n%s\n", join("\n", @{$result->{'string_info'}}); exit 0; } ########################################## elsif ($main::options{'close_family'}) { my $robot = $main::options{'robot'} || $Conf::Conf{'host'}; my $family_name; unless ($family_name = $main::options{'close_family'}) { pod2usage(-exitval => 1, -output => \*STDERR); } my $family; unless ($family = Sympa::Family->new($family_name, $robot)) { print STDERR "The family $family_name does not exist, impossible family closure\n"; exit 1; } my $string; unless ($string = $family->close_family()) { print STDERR "\nImpossible family closure : action stopped \n"; exit 1; } print STDOUT $string; exit 0; } ########################################## elsif ($main::options{'sync_list_db'}) { my $listname = $main::options{'list'} || ''; if (length($listname) > 1) { my $list = Sympa::List->new($listname); unless (defined $list) { print STDOUT "\nList '$listname' does not exist. \n"; exit 1; } $list->_update_list_db; } else { Sympa::List::_flush_list_db(); my $all_lists = Sympa::List::get_lists('*', 'reload_config' => 1); foreach my $list (@$all_lists) { $list->_update_list_db; } } exit 0; } elsif ($main::options{'export_list'}) { my $robot_id = $main::options{'robot'} || '*'; my $all_lists = Sympa::List::get_lists($robot_id); exit 1 unless defined $all_lists; foreach my $list (@$all_lists) { printf "%s\n", $list->{'name'}; } exit 0; } elsif ($main::options{'send_digest'}) { Sympa::Spindle::ProcessDigest->new( send_now => 1, keep_digest => $main::options{'keep_digest'}, )->spin; exit 0; } die 'Unknown option'; exit(0); # Load configuration. sub _load { ## Load sympa.conf. unless (Conf::load(Conf::get_sympa_conf(), 'no_db')) { #Site and Robot die sprintf "Unable to load sympa configuration, file %s or one of the vhost robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } ## Open the syslog and say we're read out stuff. $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); # Enable SMTP logging if required $mailer->{log_smtp} = $main::options{'mail'} || Sympa::Tools::Data::smart_eq($Conf::Conf{'log_smtp'}, 'on'); # setting log_level using conf unless it is set by calling option if (defined $main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; $log->syslog( 'info', 'Configuration file read, log level set using options: %s', $main::options{'log_level'} ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog( 'info', 'Configuration file read, default log level %s', $Conf::Conf{'log_level'} ); } if (Conf::cookie_changed()) { die sprintf 'sympa.conf/cookie parameter has changed. You may have severe inconsitencies into password storage. Restore previous cookie or write some tool to re-encrypt password in database and check spools contents (look at %s/cookies.history file).', $Conf::Conf{'etc'}; } # Check database connectivity. unless (Sympa::DatabaseManager->instance) { die sprintf "Database %s defined in sympa.conf is unreachable. verify db_xxx parameters in sympa.conf\n", $Conf::Conf{'db_name'}; } # Now trying to load full config (including database) unless (Conf::load()) { #FIXME: load Site, then robot cache die sprintf "Unable to load Sympa configuration, file %s or any of the virtual host robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } ## Set locale configuration ## Compatibility with version < 2.3.3 $main::options{'lang'} =~ s/\.cat$// if defined $main::options{'lang'}; $default_lang = $language->set_lang($main::options{'lang'}, $Conf::Conf{'lang'}, 'en'); ## Main program if (!chdir($Conf::Conf{'home'})) { die sprintf 'Can\'t chdir to %s: %s', $Conf::Conf{'home'}, $ERRNO; ## Function never returns. } ## Check for several files. unless (Conf::checkfiles_as_root()) { die "Missing files\n"; } } sub _report { my $spindle = shift; my @reports = @{$spindle->{stash} || []}; @reports = ([undef, 'notice', 'performed']) unless @reports; my $template = Sympa::Template->new('*', subdir => 'mail_tt2'); foreach my $report (@reports) { my ($request, $report_type, $report_entry, $report_param) = @$report; my $action = $request ? $request->{action} : 'sympa'; my $message = ''; $template->parse( { report_type => $report_type, report_entry => $report_entry, report_param => ($report_param || {}), }, 'report.tt2', \$message ); $message ||= $report_entry; $message =~ s/\n/ /g; printf STDERR "%s [%s] %s\n", $action, $report_type, $message; } return $spindle->success ? 1 : undef; } # DEPRECATED. Use Sympa::Spindle::ProcessDigest class. #sub SendDigest; # Lowercase field from database. # Old names: List::lowercase_field(), Sympa::List::lowercase_field(). sub _lowercase_field { my ($table, $field) = @_; my $sth; my $sdm = Sympa::DatabaseManager->instance; my $total = 0; unless ($sdm and $sth = $sdm->do_query(q{SELECT %s FROM %s}, $field, $table)) { $log->syslog('err', 'Unable to get values of field %s for table %s', $field, $table); return undef; } while (my $user = $sth->fetchrow_hashref('NAME_lc')) { my $lower_cased = lc($user->{$field}); next if $lower_cased eq $user->{$field}; $total++; ## Updating database. unless ( $sth = $sdm->do_prepared_query( sprintf( q{UPDATE %s SET %s = ? WHERE %s = ?}, $table, $field, $field ), $lower_cased, $user->{$field} ) ) { $log->syslog('err', 'Unable to set field % from table %s to value %s', $field, $lower_cased, $table); next; } } $sth->finish(); return $total; } __END__ =encoding utf-8 =head1 NAME sympa, sympa.pl - Command line utility to manage Sympa =head1 SYNOPSIS C S<[ C<-d, --debug> ]> S<[ C<-f, --file>=I ]> S<[ C<-l, --lang>=I ]> S<[ C<-m, --mail> ]> S<[ C<-h, --help> ]> S<[ C<-v, --version> ]> S<> S<[ C<--import>=I ]> S<[ C<--close_list>=I[I<@robot>] ]> S<[ C<--purge_list>=I[I<@robot>] ]> S<[ C<--lowercase> ]> S<[ C<--make_alias_file> ]> S<[ C<--dump>=I | ALL ]> =head1 DESCRIPTION NOTE: On overview of Sympa documentation see L. Sympa.pl is invoked from command line then performs various administration tasks. =head1 OPTIONS F may run with following options in general. =over 4 =item C<-d>, C<--debug> Enable debug mode. =item C<-f>, C<--config=>I Force Sympa to use an alternative configuration file instead of F<--CONFIG-->. =item C<-l>, C<--lang=>I Set this option to use a language for Sympa. The corresponding gettext catalog file must be located in F<$LOCALEDIR> directory. =item C<--log_level=>I Sets Sympa log level. =back With the following options F will run in batch mode: =over 4 =item C<--add_list=>I C<--robot=>I C<--input_file=>I Add the list described by the file.xml under robot_name, to the family family_name. =item C<--change_user_email> C<--current_email=>I C<--new_email=>I Changes a user email address in all Sympa databases (subscriber_table, list config, etc) for all virtual robots. =item C<--close_family=>I C<--robot=>I Close lists of family_name family under robot_name. =item C<--close_list=>I[I<@robot>] Close the list (changing its status to closed), remove aliases and remove subscribers from DB (a dump is created in the list directory to allow restoring the list) =item C<--conf_2_db> Load sympa.conf and each robot.conf into database. =item C<--create_list> C<--robot=>I C<--input_file=>I Create a list with the XML file under robot robot_name. =item C<--dump=>I@I|C Dumps subscribers of for `listname' list or all lists. Subscribers are dumped in subscribers.db.dump. =begin comment =item C<--export_list> [ C<--robot=>I ] B. =end comment =item C<--health_check> Check if F, F of virtual robots and database structure are correct. If any errors occur, exits with non-zero status. =item C<--import=>I@I Import subscribers in the list. Data are read from standard input. The imported data should contain one entry per line : the first field is an email address, the second (optional) field is the free form name. Fields are spaces-separated. Sample: ## Data to be imported ## email gecos john.steward@some.company.com John - accountant mary.blacksmith@another.company.com Mary - secretary =item C<--instantiate_family=>I C<--robot=>I C<--input_file=>I [ C<--close_unknown> ] [ C<--quiet> ] Instantiate family_name lists described in the file.xml under robot_name. The family directory must exist; automatically close undefined lists in a new instantiation if --close_unknown is specified; do not print report if C<--quiet> is specified. =item C<--lowercase> Lowercases email addresses in database. =item C<--make_alias_file> [ C<--robot> robot ] Create an aliases file in /tmp/ with all list aliases. It uses the F template (useful when list_aliases.tt2 was changed). =item C<--md5_encode_password> Rewrite password in C of database using MD5 fingerprint. YOU CAN'T UNDO unless you save this table first. B that this option was obsoleted. Use L. =item C<--modify_list=>I C<--robot=>I C<--input_file=>I Modify the existing list installed under the robot robot_name and that belongs to the family family_name. The new description is in the C. =item C<--purge_list>=I[@I] Remove the list (remove archive, configuration files, users and owners in admin table. Restore is not possible after this operation. =item C<--reload_list_config> [ C<--list=>I@I ] [ C<--robot=>I ] Recreates all F files or cache in C. You should run this command if you edit authorization scenarios. The list and robot parameters are optional. =item C<--rename_list=>I@I C<--new_listname=>I C<--new_listrobot=>I Renames a list or move it to another virtual robot. =item C<--send_digest> [ C<--keep_digest> ] Send digest right now. If C<--keep_digest> is specified, stocked digest will not be removed. =item C<--sync_include=>I@I Trigger the list members update. =item C<--sync_list_db> [ C<--list=>I@I ] Syncs filesystem list configs to the database cache of list configs, optionally syncs an individual list if specified. =item C<--test_database_message_buffer> B: This option was deprecated. Test the database message buffer size. =item C<--upgrade> [ C<--from=>I ] [ C<--to=>I ] Runs Sympa maintenance script to upgrade from version I to version I. =item C<--upgrade_shared> [ C<--list=>I ] [ C<--robot=>I ] B: This option was deprecated. See upgrade_shared_repository(1). Rename files in shared. =back With following options F will print some information and exit. =over 4 =item C<-h>, C<--help> Print this help message. =item C<--md5_digest=>I Output a MD5 digest of a password (useful for SOAP client trusted application). =item C<-v>, C<--version> Print the version number. =back =head1 FILES F<--CONFIG--> main configuration file. =head1 SEE ALSO L. =head1 HISTORY This program was originally written by: =over 4 =item Serge Aumont ComitE<233> RE<233>seau des UniversitE<233>s =item Olivier SalaE<252>n ComitE<233> RE<233>seau des UniversitE<233>s =back As of Sympa 6.2b.4, it was split into three programs: F command line utility, F daemon and F daemon. =cut sympa-6.2.24/src/sbin/sympa_newaliases.pl.in0000644000175000017500000001664613216651447017750 0ustar rackeracke#! --PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use Conf; use Sympa::Crash; # Show traceback. use Sympa::Log; # If this program was invoked by the other, throw standard output away so # that parent's output (e.g. via CGI) won't be stained. unless (-t) { open STDOUT, '>&STDERR'; } my %options; unless ( GetOptions( \%options, 'config|f=s', 'debug|d', 'domain=s', 'help|h', 'log_level=s', 'version|v' ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($options{'help'}) { pod2usage(0); } elsif ($options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; } my $log = Sympa::Log->instance; $log->{log_to_stderr} = 'all' if $main::options{'debug'} or -t; my $robot_id = $options{'domain'}; # Load configuration unless (Conf::load()) { $log->syslog('err', 'The configuration file %s contains error', Conf::get_sympa_conf()); exit 1; } $log->openlog( $Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}, database_backend => undef ); # setting log_level using conf unless it is set by calling option if (defined $main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; } else { $log->{level} = $Conf::Conf{'log_level'}; } my ($aliases_file, $aliases_program, $aliases_db_type); if ($robot_id) { unless (Conf::valid_robot($robot_id)) { $log->syslog('err', 'Robot %s does not exist', $robot_id); exit 1; } $aliases_file = Conf::get_robot_conf($robot_id, 'sendmail_aliases'); $aliases_program = Conf::get_robot_conf($robot_id, 'aliases_program'); $aliases_db_type = Conf::get_robot_conf($robot_id, 'aliases_db_type'); } else { $aliases_file = $Conf::Conf{'sendmail_aliases'}; $aliases_program = $Conf::Conf{'aliases_program'}; $aliases_db_type = $Conf::Conf{'aliases_db_type'}; } if ($aliases_file eq 'none') { exit 0; # do nothing } elsif (!-e $aliases_file) { $log->syslog('err', 'The aliases file %s does not exist', $aliases_file); exit 1; } unless ($aliases_db_type =~ /\A\w+\z/) { $log->syslog('err', 'Invalid aliases_db_type "%s"', $aliases_db_type); exit 1; } if ($aliases_program =~ m{\A/}) { $log->syslog('debug2', 'Executing "%s %s"', $aliases_program, $aliases_file); exec $aliases_program, $aliases_file; } elsif ($aliases_program eq 'makemap') { $log->syslog('debug2', 'Executing "%s %s %s < %s"', q{--MAKEMAP--}, $aliases_db_type, $aliases_file, $aliases_file); unless (open STDIN, '<', $aliases_file) { $log->syslog('err', 'Canot open %s', $aliases_file); exit 1; } exec q{--MAKEMAP--}, $aliases_db_type, $aliases_file; } elsif ($aliases_program eq 'newaliases') { $log->syslog( 'debug2', 'Executing "%s"', q{--NEWALIASES-- --NEWALIASES_ARG--} ); # Some newaliases utilities e.g. with Postfix cannot take arguments. # OTOH if it may take arg, exec() must take separate one to avoid shell # metacharacters. if (q{--NEWALIASES_ARG--}) { exec q{--NEWALIASES--}, q{--NEWALIASES_ARG--}; } else { exec q{--NEWALIASES--}; } } elsif ($aliases_program eq 'postalias') { $log->syslog('debug2', 'Executing "%s %s:%s"', q{--POSTALIAS--}, $aliases_db_type, $aliases_file); exec q{--POSTALIAS--}, "$aliases_db_type:$aliases_file"; } elsif ($aliases_program eq 'postmap') { $log->syslog('debug2', 'Executing "%s %s:%s"', q{--POSTMAP--}, $aliases_db_type, $aliases_file); exec q{--POSTMAP--}, "$aliases_db_type:$aliases_file"; } else { $log->syslog('err', 'Invalid aliases_program "%s"', $aliases_program); exit 1; } my $errno = $ERRNO; $log->syslog('err', 'Cannot execute aliases_program "%s": %m', $aliases_program); exit($errno || 1); __END__ =encoding utf-8 =head1 NAME sympa_newaliases, sympa_newaliases.pl - Alias database maintenance =head1 SYNOPSIS sympa_newaliases.pl --domain=dom.ain =head1 DESCRIPTION F is a program to maintain alias database. It is typically invoked from L module via sympa_newaliases-wrapper, then updates alias database. =head1 OPTIONS F may run with following options. =over =item C<--domain=>I Name of virtual robot on which aliases will be updated. =item C<-f>, C<--config=>I Force sympa_newaliases to use an alternative configuration file instead of F<--CONFIG-->. =item C<-h>, C<--help> Print this help message. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in F<--CONFIG--> will be referred. They may be overridden by robot.conf of each virtual robot. =over =item sendmail_aliases Source text of alias database. Default value is F<$SENDMAIL_ALIASES>. =item aliases_program System command to update alias database. Possible values are: =over =item C Sendmail makemap utility. =item C L or compatible utility. =item C Postfix L utility. =item C Postfix L utility. =item Full path Full path to executable file. File will be invoked with the value of C as an argument. =back Default value is C. =item aliases_db_type Type of alias database. This is meaningful when value of C parameter is C, C or C. Possible values will be vary by system commands. For example, C and C can support any of C, C, C, C and C. Default value is C. =back =head1 RETURN VALUE Returns with exit code 0. If invoked system command failed, returns with its exit code. On other failures, returns with 1. =head1 FILES =over =item F<--CONFIG--> Sympa site configuration. =item F<$LIBEXECDIR/sympa_newaliases-wrapper> Set UID wrapper for sympa_newaliases.pl. =back =head1 HISTORY sympa_newaliases.pl appeared on Sympa 6.1.18. It was initially written by IKEDA Soji . =head1 SEE ALSO L. =cut sympa-6.2.24/src/sbin/archived.pl.in0000644000175000017500000002246013216651447016160 0ustar rackeracke#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use POSIX qw(); use Sympa::Alarm; use Conf; use Sympa::Constants; use Sympa::Crash; # Show traceback. use Sympa::DatabaseManager; use Sympa::Language; use Sympa::Log; use Sympa::Process; use Sympa::Spindle::ProcessArchive; my $process = Sympa::Process->instance; $process->init(pidname => 'archived'); # Check options my %options; unless ( GetOptions( \%main::options, 'config|f=s', 'debug|d', 'help|h', 'foreground|F', 'log_level=s', 'version|v' ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($main::options{'help'}) { pod2usage(0); } elsif ($main::options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; $main::options{'foreground'} = 1; } my $log = Sympa::Log->instance; $log->{'log_to_stderr'} = 'all' if $main::options{'foreground'}; # Load sympa.conf unless (Conf::load()) { die sprintf "Unable to load sympa configuration, file %s has errors.\n", Conf::get_sympa_conf(); } # Check database connectivity unless (Sympa::DatabaseManager->instance) { die sprintf "Database %s defined in sympa.conf has not the right structure or is unreachable.\n", $Conf::Conf{'db_name'}; } # Put ourselves in background if not in debug mode. unless ($main::options{'foreground'}) { $process->daemonize; } # Create and write the PID file. $process->write_pid(initial => 1); # If process is running in foreground, don't write STDERR to a dedicated file. unless ($main::options{foreground}) { $process->direct_stderr_to_file; } # setting log_level using conf unless it is set by calling option if ($main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; $log->syslog('info', "Configuration file read, log level set using options : $main::options{'log_level'}" ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog('info', "Configuration file read, default log level $Conf::Conf{'log_level'}" ); } my $log_facility = $Conf::Conf{'log_facility'} || $Conf::Conf{'syslog'}; $log->openlog($log_facility, $Conf::Conf{'log_socket_type'}); ## Set the User ID & Group ID for the process $GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2]; $UID = $EUID = (getpwnam(Sympa::Constants::USER))[2]; ## Required on FreeBSD to change ALL IDs(effective UID + real UID + saved UID) POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]); POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]); ## Check if the UID has correctly been set (useful on OS X) unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2]) && ($UID == (getpwnam(Sympa::Constants::USER))[2])) { die "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n"; } ## Sets the UMASK umask(oct($Conf::Conf{'umask'})); foreach my $robot (@{Conf::get_robots_list()}) { my $arc_dir = Conf::get_robot_conf($robot, 'arc_path'); unless ($arc_dir) { die sprintf 'Robot %s has no archives directory. Check arc_path parameter in this robot.conf and in sympa.conf', $robot; } } ## Change to list root unless (chdir($Conf::Conf{'home'})) { die sprintf 'Unable to change directory to %s: %s', $Conf::Conf{'home'}, $!; } Sympa::Language->instance->set_lang($Conf::Conf{'lang'}, 'en'); $log->syslog('notice', 'Archived %s Started', Sympa::Constants::VERSION); my $spindle = Sympa::Spindle::ProcessArchive->new; # Catch signals, in order to exit cleanly, whenever possible. $SIG{'TERM'} = 'sigterm'; $SIG{'INT'} = 'sigterm'; while (not $spindle->{finish}) { $spindle->spin; last if $spindle->{finish}; # Sleep for a while if archive spool is empty. sleep $Conf::Conf{'sleep'}; } # Purge grouped notifications. Sympa::Alarm->instance->flush(purge => 1); ## Free zombie sendmail processes. #Sympa::Process->instance->reap_child; $log->syslog('notice', 'Archived exited normally due to signal'); $process->remove_pid(final => 1); exit 0; # When we catch signal, just change the value of the loop variable. sub sigterm { my $sig = shift; $log->syslog('notice', 'Signal %s received, still processing current task', $sig); $spindle->{finish} = $sig; } # Moved to Sympa::Spindle::ProcessArchive::_twist(). #sub process_message; # Moved to Sympa::Spindle::ProcessArchive::_do_command(). #sub do_command; # Moved to Sympa::Archive::html_remove(). Use do_remove_arc() instead. #sub remove; # Moved to Sympa::Spindle::ProcessArchive::_do_remove_arc(). #sub do_remove_arc; # Moved to Sympa::Archive::html_rebuild(). Use do_rebuildarc() instead. #sub rebuild; # Moved to Sympa::Spindle::ProcessArchive::_do_rebuildarc(). #sub do_rebuildarc; # Moved to Sympa::Spindle::ProcessArchive::_mail2arc(). #sub mail2arc; # Moved to Sympa::Archive::_set_hidden_mode(). #sub set_hidden_mode; # Moved to Sympa::Archive::_unset_hidden_mode(). #sub unset_hidden_mode; # Moved to Sympa::Archive::_save_idx(). #sub save_idx; # Moved to Sympa::Archive::_create_idx(). #sub create_idx; # DEPRECATED. # Use Sympa::Archive::_get_tag(). #sub get_tag; # Checks if directory exists and we have write and read accec to it. # DEPRECATED. No longer used. #sub directory_check; __END__ =encoding utf-8 =head1 NAME archived, archived.pl - Mailing List Archiving Daemon for Sympa =head1 SYNOPSIS C S<[ C<--foreground> ]> S<[ C<--debug> ]> =head1 DESCRIPTION B is a program which scan permanently the archive spool and feeds the web archives, converting messages to the HTML format and linking them. Original mails are also kept (in I directory> for later rebuilding of archives. The HTML conversion is achieved by the means of the B program. Archives are accessed via B and B, which proposes access control; therefore archives should not be located in a public web directory. =head1 OPTIONS These programs follow the usual GNU command line syntax, with long options starting with two dashes (C<-->). A summary of options is included below. =over 5 =item C<-F>, C<--foreground> Do not detach TTY. =item C<-f>, C<--config=>I Force archived to use an alternative configuration file instead of F<--CONFIG-->. =item C<-d>, C<--debug> Run the program in a debug mode. =item C<-h>, C<--help> Print this help message. =back =head1 FILES F<$SPOOLDIR/outgoing/> outgoing Sympa directory. F<$DEFAULTDIR/mhonarc-ressources.tt2> template of MHonArc resource file. F<--CONFIG--> Sympa configuration file. F<$PIDDIR/archived.pid> this file contains the process ID of F. =head1 MORE DOCUMENTATION The full documentation in HTML and PDF formats can be found in L. The mailing lists (with web archives) can be accessed at L. =head1 HISTORY This program was originally written by: =over 4 =item Serge Aumont ComitE<233> RE<233>seau des UniversitE<233>s =item Olivier SalaE<252>n ComitE<233> RE<233>seau des UniversitE<233>s =back This manual page was initially written by JE<233>rE<244>me Marant for the Debian GNU/Linux system. =head1 LICENSE You may distribute this software under the terms of the GNU General Public License Version 2. For more details see F file. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts and no Back-Cover Texts. A copy of the license can be found under L. =head1 BUGS Report bugs to Sympa bug tracker. See L. =head1 SEE ALSO L, L, L, L. L. =cut sympa-6.2.24/src/sbin/Makefile.am0000644000175000017500000000700113216651447015457 0ustar rackeracke# $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # # 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, see . sbin_SCRIPTS = archived.pl \ bounced.pl \ bulk.pl \ sympa.pl \ sympa_automatic.pl \ sympa_msg.pl \ sympa_newaliases.pl \ sympa_wizard.pl \ task_manager.pl man1_MANS = \ sympa.1 \ sympa_newaliases.1 \ sympa_wizard.1 man8_MANS = \ archived.8 \ bounced.8 \ bulk.8 \ sympa_automatic.8 \ sympa_msg.8 \ task_manager.8 EXTRA_DIST = \ archived.pl.in \ bounced.pl.in \ bulk.pl.in \ sympa.pl.in \ sympa_automatic.pl.in \ sympa_msg.pl.in \ sympa_newaliases.pl.in \ sympa_wizard.pl.in \ task_manager.pl.in CLEANFILES = $(sbin_SCRIPTS) $(man1_MANS) $(man8_MANS) archived.pl bounced.pl bulk.pl sympa.pl sympa_automatic.pl sympa_msg.pl task_manager.pl: Makefile @rm -f $@ $(AM_V_GEN)$(SED) \ -e 's|--PERL--|$(PERL)|' \ -e 's|--defaultdir--|$(defaultdir)|' \ -e 's|--docdir--|$(docdir)|' \ -e 's|--libexecdir--|$(libexecdir)|' \ -e 's|--localedir--|$(localedir)|' \ -e 's|--modulesdir--|$(modulesdir)|' \ -e 's|--piddir--|$(piddir)|' \ -e 's|--sbindir--|$(sbindir)|' \ -e 's|--spooldir--|$(spooldir)|' \ -e 's|--CONFIG--|$(CONFIG)|' \ < $(srcdir)/$@.in > $@ @chmod +x $@ archived.pl: $(srcdir)/archived.pl.in bounced.pl: $(srcdir)/bounced.pl.in bulk.pl: $(srcdir)/bulk.pl.in sympa.pl: $(srcdir)/sympa.pl.in sympa_automatic.pl: $(srcdir)/sympa_automatic.pl.in sympa_msg.pl: $(srcdir)/sympa_msg.pl.in task_manager.pl: $(srcdir)/task_manager.pl.in sympa_newaliases.pl sympa_wizard.pl: Makefile @rm -f $@ $(AM_V_GEN)$(SED) \ -e 's|--PERL--|$(PERL)|' \ -e 's|--defaultdir--|$(defaultdir)|' \ -e 's|--docdir--|$(docdir)|' \ -e 's|--libexecdir--|$(libexecdir)|' \ -e 's|--localedir--|$(localedir)|' \ -e 's|--modulesdir--|$(modulesdir)|' \ -e 's|--piddir--|$(piddir)|' \ -e 's|--spooldir--|$(spooldir)|' \ -e 's|--sysconfdir--|$(sysconfdir)|' \ -e 's|--CONFIG--|$(CONFIG)|' \ -e 's|--SENDMAIL_ALIASES--|$(SENDMAIL_ALIASES)|' \ -e 's|--MAKEMAP--|$(MAKEMAP)|' \ -e 's|--NEWALIASES--|$(NEWALIASES)|' \ -e 's|--NEWALIASES_ARG--|$(NEWALIASES_ARG)|' \ -e 's|--POSTALIAS--|$(POSTALIAS)|' \ -e 's|--POSTMAP--|$(POSTMAP)|' \ < $(srcdir)/$@.in > $@ @chmod +x $@ sympa_newaliases.pl: $(srcdir)/sympa_newaliases.pl.in sympa_wizard.pl: $(srcdir)/sympa_wizard.pl.in .pl.1: @rm -f $@ $(AM_V_GEN)$(POD2MAN) --section=1 --center="sympa $(VERSION)" \ --lax --release="$(VERSION)" $< $@ .pl.8: @rm -f $@ $(AM_V_GEN)$(POD2MAN) --section=8 --center="sympa $(VERSION)" \ --lax --release="$(VERSION)" $*.pl $@ # Remove old sympa.8 manpage. install-data-hook: @rm -f $(DESTDIR)$(man8dir)/sympa.8 sympa-6.2.24/src/sbin/sympa_automatic.pl.in0000644000175000017500000002741313216651447017575 0ustar rackeracke#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use POSIX qw(); use Sympa::Alarm; use Conf; use Sympa::Constants; use Sympa::Crash; # Show traceback. use Sympa::DatabaseManager; use Sympa::Log; use Sympa::Mailer; use Sympa::Process; use Sympa::Spindle::ProcessAutomatic; use Sympa::Tools::Data; my $process = Sympa::Process->instance; $process->init(pidname => 'sympa_automatic', name => 'sympa/automatic'); ## Init random engine srand(time()); # Check options. my %options; unless ( GetOptions( \%main::options, 'debug|d', 'log_level=s', 'foreground', 'config|f=s', 'mail|m', 'keepcopy|k=s', 'help|h', 'version|v', ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($main::options{'help'}) { pod2usage(0); } elsif ($main::options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; $main::options{'foreground'} = 1; } my $log = Sympa::Log->instance; $log->{log_to_stderr} = 'all' if $main::options{'foreground'}; my $mailer = Sympa::Mailer->instance; _load(); # Put ourselves in background if we're not in debug mode. unless ($main::options{'foreground'}) { $process->daemonize; } $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); # Create and write the PID file. $process->write_pid(initial => 1); # If process is running in foreground, don't write STDERR to a dedicated file. unless ($main::options{foreground}) { $process->direct_stderr_to_file; } # Set the User ID & Group ID for the process $GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2]; $UID = $EUID = (getpwnam(Sympa::Constants::USER))[2]; ## Required on FreeBSD to change ALL IDs ## (effective UID + real UID + saved UID) POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]); POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]); ## Check if the UID has correctly been set (useful on OS X) unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2]) && ($UID == (getpwnam(Sympa::Constants::USER))[2])) { die "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n"; } # Sets the UMASK umask(oct($Conf::Conf{'umask'})); ## Most initializations have now been done. $log->syslog( 'notice', 'Sympa/automatic %s Started', Sympa::Constants::VERSION() ); sleep 1; ## wait until main process has created required directories ## Do we have right access in the directory if ($main::options{'keepcopy'}) { if (!-d $main::options{'keepcopy'}) { $log->syslog( 'notice', 'Cannot keep a copy of incoming messages: %s is not a directory', $main::options{'keepcopy'} ); delete $main::options{'keepcopy'}; } elsif (!-w $main::options{'keepcopy'}) { $log->syslog( 'notice', 'Cannot keep a copy of incoming messages: no write access to %s', $main::options{'keepcopy'} ); delete $main::options{'keepcopy'}; } } my $spindle = Sympa::Spindle::ProcessAutomatic->new( keepcopy => $main::options{keepcopy}, log_level => $main::options{log_level}, log_smtp => $main::options{mail} ); # Catch signals, in order to exit cleanly, whenever possible. $SIG{'TERM'} = 'sigterm'; $SIG{'INT'} = 'sigterm'; # Interrupt from terminal. $SIG{'HUP'} = 'sighup'; $SIG{'PIPE'} = 'IGNORE'; # Ignore SIGPIPE ; prevents process from dying # Main loop. # This loop is run foreach HUP signal received. # This is the main loop : look for files in the directory, handles # them, sleeps a while and continues the good job. while (not $spindle->{finish} or $spindle->{finish} ne 'term') { $spindle->spin; if ($spindle->{finish} and $spindle->{finish} eq 'hup') { # Disconnect from Database Sympa::DatabaseManager->disconnect; $log->syslog('notice', "Reloading sympa/automatic daemon"); _load(); $spindle = Sympa::Spindle::ProcessAutomatic->new( keepcopy => $main::options{keepcopy}, log_level => $main::options{log_level}, log_smtp => $main::options{mail} ); next; } elsif ($spindle->{finish}) { last; } # If the spool was empty, sleep for a while. sleep $Conf::Conf{'sleep'}; } # Purge grouped notifications Sympa::Alarm->instance->flush(purge => 1); ## Free zombie sendmail processes. #Sympa::Process->instance->reap_child; $log->syslog('notice', 'Sympa/automatic exited normally due to signal'); $process->remove_pid(final => 1); exit(0); # Load configuration. sub _load { ## Load sympa.conf. unless (Conf::load(Conf::get_sympa_conf(), 'no_db')) { #Site and Robot die sprintf "Unable to load sympa configuration, file %s or one of the vhost robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } ## Open the syslog and say we're read out stuff. $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); # Enable SMTP logging if required $mailer->{log_smtp} = $main::options{'mail'} || Sympa::Tools::Data::smart_eq($Conf::Conf{'log_smtp'}, 'on'); # setting log_level using conf unless it is set by calling option if (defined $main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; $log->syslog( 'info', 'Configuration file read, log level set using options: %s', $main::options{'log_level'} ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog( 'info', 'Configuration file read, default log level %s', $Conf::Conf{'log_level'} ); } if (Conf::cookie_changed()) { die sprintf 'sympa.conf/cookie parameter has changed. You may have severe inconsitencies into password storage. Restore previous cookie or write some tool to re-encrypt password in database and check spools contents (look at %s/cookies.history file).', $Conf::Conf{'etc'}; } # Check database connectivity. unless (Sympa::DatabaseManager->instance) { die sprintf "Database %s defined in sympa.conf is unreachable. verify db_xxx parameters in sympa.conf\n", $Conf::Conf{'db_name'}; } # Now trying to load full config (including database) unless (Conf::load()) { #FIXME: load Site, then robot cache die sprintf "Unable to load Sympa configuration, file %s or any of the virtual host robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } ## Main program if (!chdir($Conf::Conf{'home'})) { die sprintf 'Can\'t chdir to %s: %s', $Conf::Conf{'home'}, $ERRNO; ## Function never returns. } ## Check for several files. unless (Conf::checkfiles_as_root()) { die "Missing files\n"; } } ############################################################ # sigterm ############################################################ # When we catch signal, just changes the $spindle->{finish}. # # IN : - # # OUT : - # ############################################################ sub sigterm { my $sig = shift; $log->syslog('notice', 'Signal %s received, still processing current task', $sig); $spindle->{finish} = 'term'; } ############################################################ # sighup ############################################################ # When we catch SIGHUP, changes the value of $spindle->{finish} # and puts the "-mail" logging option # # IN : - # # OUT : - # ########################################################### sub sighup { if ($mailer->{log_smtp}) { $log->syslog('notice', 'signal HUP received, switch of the "-mail" logging option and continue current task' ); $mailer->{log_smtp} = undef; } else { $log->syslog('notice', 'signal HUP received, switch on the "-mail" logging option and continue current task' ); $mailer->{log_smtp} = 1; } $spindle->{finish} = 'hup'; } # Moved to Sympa::Spindle::ProcessAutomatic::_twist(). #sub process_message; __END__ =encoding utf-8 =head1 NAME sympa_automatic, sympa_automatic.pl - Automatic list creation daemon =head1 SYNOPSIS C S<[ C<-d, --debug> ]> S<[ C<-f, --file>=I ]> S<[ C<-k, --keepcopy>=I ]> S<[ [ C<-m, --mail> ]> S<[ C<-h, --help> ]> S<[ C<-v, --version> ]> =head1 DESCRIPTION Sympa_automatic.pl is a program which scans permanently the automatic creation spool and processes each message. If the list a message is bound for has not been there and list creation is authorized, it will be created. Then the message is stored into incoming message spool again and wait for processing by F. =head1 OPTIONS F may run with following options in general. =over 4 =item C<-d>, C<--debug> Enable debug mode. =item C<-f>, C<--config=>I Force Sympa to use an alternative configuration file instead of F<--CONFIG-->. =item C<--log_level=>I Sets Sympa log level. =back F may run in daemon mode with following options. =over 4 =item C<--foreground> The process remains attached to the TTY. =item C<-k>, C<--keepcopy=>I This option tells Sympa to keep a copy of every incoming message, instead of deleting them. `directory' is the directory to store messages. =item C<-m>, C<--mail> Sympa will log calls to sendmail, including recipients. This option is useful for keeping track of each mail sent (log files may grow faster though). =back With following options F will print some information and exit. =over 4 =item C<-h>, C<--help> Print this help message. =item C<-v>, C<--version> Print the version number. =back =head1 FILES F<--CONFIG--> main configuration file. F<$PIDDIR/sympa_automatic.pid> this file contains the process ID of F. =head1 SEE ALSO L, L. L. =head1 HISTORY F was originally written by: =over 4 =item Serge Aumont ComitE<233> RE<233>seau des UniversitE<233>s =item Olivier SalaE<252>n ComitE<233> RE<233>seau des UniversitE<233>s =back As of Sympa 6.2b.4, it was split into three programs: F command line utility, F daemon and F daemon. =cut sympa-6.2.24/src/sbin/task_manager.pl.in0000644000175000017500000017736413216651447017045 0ustar rackeracke#! --PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use POSIX qw(); use Template; use Sympa; use Sympa::Alarm; use Conf; use Sympa::Constants; use Sympa::Crash; # Show traceback. use Sympa::DatabaseManager; use Sympa::List; use Sympa::Log; use Sympa::Process; use Sympa::Scenario; use Sympa::Session; use Sympa::Spool; use Sympa::Task; use Sympa::Ticket; use Sympa::Tools::File; use Sympa::Tools::Time; use Sympa::Tools::Text; use Sympa::Tracking; use Sympa::User; my $process = Sympa::Process->instance; $process->init(pidname => 'task_manager'); my %options; unless ( GetOptions( \%main::options, 'config|f=s', 'debug|d', 'help|h', 'version|v', 'log_level=s', 'foreground|F' ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($main::options{'help'}) { pod2usage(0); } elsif ($main::options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; $main::options{'foreground'} = 1; } my $log = Sympa::Log->instance; $log->{log_to_stderr} = 'all' if $main::options{'foreground'}; my $adrlist = {}; # Load sympa.conf unless (Conf::load()) { die sprintf "Unable to load Sympa configuration, file %s or one of the virtual host robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } $log->openlog($Conf::Conf{'log_facility'}, $Conf::Conf{'log_socket_type'}); # setting log_level using conf unless it is set by calling option if ($main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; $log->syslog( 'info', 'Configuration file read, log level set using options: %s', $main::options{'log_level'} ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog( 'info', 'Configuration file read, default log level %s', $Conf::Conf{'log_level'} ); } # Put ourselves in background if not in debug mode. unless ($main::options{'foreground'}) { $process->daemonize; } # Create and write the PID file. $process->write_pid(initial => 1); # If process is running in foreground, don't write STDERR to a dedicated file. unless ($main::options{foreground}) { $process->direct_stderr_to_file; } ## Set the UserID & GroupID for the process $GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2]; $UID = $EUID = (getpwnam(Sympa::Constants::USER))[2]; ## Required on FreeBSD to change ALL IDs(effective UID + real UID + saved UID) POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]); POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]); # Check if the UID has correctly been set (useful on OS X) unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2]) && ($UID == (getpwnam(Sympa::Constants::USER))[2])) { die "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n"; } ## Sets the UMASK umask(oct($Conf::Conf{'umask'})); ## Change to list root unless (chdir($Conf::Conf{'home'})) { die sprintf 'Unable to change to directory %s', $Conf::Conf{'home'}; } ## Catch signals, in order to exit cleanly, whenever possible. $SIG{'TERM'} = \&sigterm; $SIG{'INT'} = \&sigterm; my $end = 0; # Most initializations have now been done. $log->syslog('notice', 'Task_Manager %s Started', Sympa::Constants::VERSION()); ###### VARIABLES DECLARATION ###### my $spool_task = $Conf::Conf{'queuetask'}; my @tasks; # list of tasks in the spool # won't execute send_msg and delete_subs commands if true, only log undef my $log_only; #$log_only = 1; ## list of list task models #my @list_models = ('expire', 'remind', 'sync_include'); my @list_models = ('sync_include', 'remind'); ## hash of the global task models my %global_models = ( 'expire_bounce_task' => 'expire_bounce', 'purge_user_table_task' => 'purge_user_table', 'purge_logs_table_task' => 'purge_logs_table', 'purge_session_table_task' => 'purge_session_table', 'purge_spools_task' => 'purge_spools', 'purge_tables_task' => 'purge_tables', 'purge_one_time_ticket_table_task' => 'purge_one_time_ticket_table', 'purge_orphan_bounces_task' => 'purge_orphan_bounces', 'eval_bouncers_task' => 'eval_bouncers', 'process_bouncers_task' => 'process_bouncers', ); ## month hash used by epoch conversion routines my %months = ( 'Jan', 0, 'Feb', 1, 'Mar', 2, 'Apr', 3, 'May', 4, 'Jun', 5, 'Jul', 6, 'Aug', 7, 'Sep', 8, 'Oct', 9, 'Nov', 10, 'Dec', 11 ); ###### DEFINITION OF AVAILABLE COMMANDS FOR TASKS ###### my $date_arg_regexp1 = '\d+|execution_date'; my $date_arg_regexp2 = '(\d\d\d\dy)(\d+m)?(\d+d)?(\d+h)?(\d+min)?(\d+sec)?'; my $date_arg_regexp3 = '(\d+|execution_date)(\+|\-)(\d+y)?(\d+m)?(\d+w)?(\d+d)?(\d+h)?(\d+min)?(\d+sec)?'; my $delay_regexp = '(\d+y)?(\d+m)?(\d+w)?(\d+d)?(\d+h)?(\d+min)?(\d+sec)?'; my $var_regexp = '@\w+'; my $subarg_regexp = '(\w+)(|\((.*)\))'; # for argument with sub argument (ie arg(sub_arg)) # regular commands my %commands = ( 'next' => ['date', '\w*'], # date label 'stop' => [], 'create' => ['subarg', '\w+', '\w+'], #object model model choice 'exec' => ['.+'], #file #delay 'expire_bounce' => ['\d+'], #template date 'sync_include' => [], 'purge_user_table' => [], 'purge_logs_table' => [], 'purge_session_table' => [], 'purge_spools' => [], 'purge_tables' => [], 'purge_one_time_ticket_table' => [], 'purge_orphan_bounces' => [], 'eval_bouncers' => [], 'process_bouncers' => [] ); # commands which use a variable. If you add such a command, the first # parameter must be the variable my %var_commands = ( 'delete_subs' => ['var'], # variable 'send_msg' => ['var', '\w+'], #variable template 'rm_file' => ['var'], # variable ); foreach (keys %var_commands) { $commands{$_} = $var_commands{$_}; } # commands which are used for assignments my %asgn_commands = ( 'select_subs' => ['subarg'], # condition 'delete_subs' => ['var'], # variable ); foreach (keys %asgn_commands) { $commands{$_} = $asgn_commands{$_}; } ###### INFINITE LOOP SCANING THE QUEUE (unless a sig TERM is received) ###### while (!$end) { my $current_date = time; # current epoch date # Process grouped notifications. Sympa::Alarm->instance->flush; ## List all tasks unless (Sympa::Task::list_tasks($spool_task)) { Sympa::send_notify_to_listmaster('*', 'intern_error', {'error' => "Failed to list task files in $spool_task"}); $log->syslog('err', "Failed to list task files in %s", $spool_task); exit -1; } my %used_models; # models for which a task exists foreach my $model (Sympa::Task::get_used_models()) { $used_models{$model} = 1; } ### creation of required tasks my %default_data = ( 'creation_date' => $current_date, # hash of datas necessary to the creation of tasks 'execution_date' => 'execution_date' ); ## global tasks foreach my $key (keys %global_models) { unless ($used_models{$global_models{$key}}) { if ($Conf::Conf{$key}) { # hash of datas necessary to the creation of tasks my %data = %default_data; create($current_date, '', $global_models{$key}, $Conf::Conf{$key}, \%data); $used_models{$1} = 1; #!!!FIXME FIXME FIXME!!! } } } # list tasks foreach my $robot (Sympa::List::get_robots()) { my $all_lists = Sympa::List::get_lists($robot); last if $end; foreach my $list (@{$all_lists || []}) { my %data = %default_data; $data{'list'} = { 'name' => $list->{'name'}, 'robot' => $list->{'domain'} }; my %used_list_models; # stores which models already have a task foreach (@list_models) { $used_list_models{$_} = undef; } foreach my $model (Sympa::Task::get_used_models($list->get_id)) { $used_list_models{$model} = 1; } foreach my $model (@list_models) { unless ($used_list_models{$model}) { my $model_task_parameter = "$model" . '_task'; if ($model eq 'sync_include') { next unless ($list->has_include_data_sources() && ($list->{'admin'}{'status'} eq 'open')); create($current_date, 'INIT', $model, 'ttl', \%data); } elsif (defined $list->{'admin'}{$model_task_parameter} && defined $list->{'admin'}{$model_task_parameter} {'name'} && ($list->{'admin'}{'status'} eq 'open')) { create($current_date, '', $model, $list->{'admin'}{$model_task_parameter}{'name'}, \%data); } } } } } last if $end; ## Execute existing tasks ## List all tasks unless (Sympa::Task::list_tasks($spool_task)) { Sympa::send_notify_to_listmaster('*', 'intern_error', {'error' => "Failed to list task files in $spool_task"}); $log->syslog('err', "Failed to list task files in %s", $spool_task); exit -1; } ## processing of tasks anterior to the current date $log->syslog('debug3', 'Processing of tasks anterior to the current date'); foreach my $task (Sympa::Task::get_task_list()) { last if $end; my $task_file = $task->{'filepath'}; $log->syslog('debug3', 'Procesing %s', $task_file); last unless ($task->{'date'} < $current_date); if ($task->{'object'} ne '_global') { # list task my $list = $task->{'list_object'}; ## Skip closed lists unless (defined $list && ($list->{'admin'}{'status'} eq 'open')) { $log->syslog('notice', 'Removing task file %s because the list is not opened', $task_file); unless (unlink $task_file) { $log->syslog('err', 'Unable to remove task file %s: %m', $task_file); next; } next; } ## Skip if parameter is not defined if ($task->{'model'} eq 'sync_include') { unless ($list->{'admin'}{'status'} eq 'open') { $log->syslog('notice', 'Removing task file %s', $task_file); unless (unlink $task_file) { $log->syslog('err', 'Unable to remove task file %s: %m', $task_file); next; } next; } } else { unless (defined $list->{'admin'}{$task->{'model'}} && defined $list->{'admin'}{$task->{'model'}}{'name'}) { $log->syslog('notice', 'Removing task file %s', $task_file); unless (unlink $task_file) { $log->syslog('err', 'Unable to remove task file %s: %m', $task_file); next; } next; } } } execute($task); } sleep 60; } # Purge grouped notifications Sympa::Alarm->instance->flush(purge => 1); $log->syslog('notice', 'Task_Manager exited normally due to signal'); $process->remove_pid(final => 1); exit(0); ####### SUBROUTINES ####### ## task creations sub create { my $date = shift; my $label = shift; my $model = shift; my $model_choice = shift; my $Rdata = shift; $log->syslog('debug2', "create date : $date label : $label model $model : $model_choice Rdata :$Rdata" ); my $task_file; my $list_name; my $robot; my $object; if (defined $Rdata->{'list'}) { $list_name = $Rdata->{'list'}{'name'}; $robot = $Rdata->{'list'}{'robot'}; $task_file = "$spool_task/$date.$label.$model.$list_name\@$robot"; $object = 'list'; } else { $object = '_global'; $task_file = $spool_task . '/' . $date . '.' . $label . '.' . $model . '.' . $object; } ## model recovery my $model_file; my $model_name = $model . '.' . $model_choice . '.' . 'task'; $log->syslog('notice', 'Creation of %s', $task_file); # for global model if ($object eq '_global') { unless ( $model_file = Sympa::search_fullpath( '*', $model_name, subdir => 'global_task_models' ) ) { $log->syslog('err', 'Unable to find %s, creation aborted', $model_name); return undef; } } # for a list if ($object eq 'list') { my $list = Sympa::List->new($list_name, $robot); $Rdata->{'list'}{'ttl'} = $list->{'admin'}{'ttl'}; unless ( $model_file = Sympa::search_fullpath( $list, $model_name, subdir => 'list_task_models' ) ) { $log->syslog('err', "error : unable to find $model_name, for list $list_name creation aborted" ); return undef; } } $log->syslog('notice', 'With model %s', $model_file); ## creation open(TASK, ">$task_file"); my $tt2 = Template->new( { 'START_TAG' => quotemeta('['), 'END_TAG' => quotemeta(']'), 'ABSOLUTE' => 1 } ); unless (defined $tt2 && $tt2->process($model_file, $Rdata, \*TASK)) { $log->syslog('err', 'Failed to parse task template "%s": %s', $model_file, $tt2->error()); } #&parser::parse_tpl($Rdata, $model_file, \*TASK); close(TASK); if (!check($task_file)) { $log->syslog('err', "error : syntax error in $task_file, you should check $model_file" ); unlink($task_file) ? $log->syslog('notice', '%s deleted', $task_file) : $log->syslog('err', 'Unable to delete %s', $task_file); return undef; } return 1; } ### SYNTAX CHECKING SUBROUTINES ### ## check the syntax of a task sub check { my $task_file = shift; # the task to check $log->syslog('debug2', '(%s)', $task_file); my %result; # stores the result of the chk_line subroutine my $lnb = 0; # line number my %used_labels; # list of labels used as parameter in commands my %labels; # list of declared labels my %used_vars; # list of vars used as parameter in commands my %vars; # list of declared vars unless (open(TASK, $task_file)) { $log->syslog('err', 'Unable to read %s, checking is impossible', $task_file); return undef; } while () { chomp; $lnb++; next if ($_ =~ /^\s*\#/); unless (chk_line($_, \%result)) { $log->syslog('err', 'Error at line %s: %s', $lnb, $_); $log->syslog('err', '%s', $result{'error'}); return undef; } if ($result{'nature'} eq 'assignment') { if (chk_cmd( $result{'command'}, $lnb, $result{'Rarguments'}, \%used_labels, \%used_vars ) ) { $vars{$result{'var'}} = 1; } else { return undef; } } if ($result{'nature'} eq 'command') { return undef unless ( chk_cmd( $result{'command'}, $lnb, $result{'Rarguments'}, \%used_labels, \%used_vars ) ); } $labels{$result{'label'}} = 1 if ($result{'nature'} eq 'label'); } # are all labels used ? foreach my $label (keys %labels) { $log->syslog('debug3', 'Warning: Label %s exists but is not used', $label) unless ($used_labels{$label}); } # do all used labels exist ? foreach my $label (keys %used_labels) { unless ($labels{$label}) { $log->syslog('err', 'Label %s is used but does not exist', $label); return undef; } } # are all variables used ? foreach my $var (keys %vars) { $log->syslog('notice', 'Warning: Var %s exists but is not used', $var) unless ($used_vars{$var}); } # do all used variables exist ? foreach my $var (keys %used_vars) { unless ($vars{$var}) { $log->syslog('err', 'Var %s is used but does not exist', $var); return undef; } } return 1; } ## check a task line sub chk_line { my $line = $_[0]; my $Rhash = $_[1]; # will contain nature of line (label, command, error...) ## just in case... chomp $line; $log->syslog('debug2', '(%s, %s)', $line, $Rhash->{'nature'}); $Rhash->{'nature'} = undef; # empty line if (!$line) { $Rhash->{'nature'} = 'empty line'; return 1; } # comment if ($line =~ /^\s*\#.*/) { $Rhash->{'nature'} = 'comment'; return 1; } # title if ($line =~ /^\s*title\...\s*(.*)\s*/i) { $Rhash->{'nature'} = 'title'; $Rhash->{'title'} = $1; return 1; } # label if ($line =~ /^\s*\/\s*(.*)/) { $Rhash->{'nature'} = 'label'; $Rhash->{'label'} = $1; return 1; } # command if ($line =~ /^\s*(\w+)\s*\((.*)\)\s*/i) { my $command = lc($1); my @args = split(/,/, $2); foreach (@args) { s/\s//g; } unless ($commands{$command}) { $Rhash->{'nature'} = 'error'; $Rhash->{'error'} = "unknown command $command"; return 0; } $Rhash->{'nature'} = 'command'; $Rhash->{'command'} = $command; # arguments recovery. no checking of their syntax !!! $Rhash->{'Rarguments'} = \@args; return 1; } # assignment if ($line =~ /^\s*(@\w+)\s*=\s*(.+)/) { my %hash2; chk_line($2, \%hash2); unless ($asgn_commands{$hash2{'command'}}) { $Rhash->{'nature'} = 'error'; $Rhash->{'error'} = "non valid assignment $2"; return 0; } $Rhash->{'nature'} = 'assignment'; $Rhash->{'var'} = $1; $Rhash->{'command'} = $hash2{'command'}; $Rhash->{'Rarguments'} = $hash2{'Rarguments'}; return 1; } $Rhash->{'nature'} = 'error'; $Rhash->{'error'} = 'syntax error'; return 0; } ## check the arguments of a command sub chk_cmd { my $cmd = $_[0]; # command name my $lnb = $_[1]; # line number my $Rargs = $_[2]; # argument list my $Rused_labels = $_[3]; my $Rused_vars = $_[4]; $log->syslog('debug2', '(%s, %d, %s)', $cmd, $lnb, join(',', @{$Rargs})); if (defined $commands{$cmd}) { my @expected_args = @{$commands{$cmd}}; my @args = @{$Rargs}; unless ($#expected_args == $#args) { $log->syslog('err', 'Error at line %s: wrong number of arguments for %s', $lnb, $cmd); $log->syslog('err', 'Args = @args; expected_args = @expected_args'); return undef; } foreach (@args) { undef my $error; my $regexp = $expected_args[0]; shift(@expected_args); if ($regexp eq 'date') { $error = 1 unless ((/^$date_arg_regexp1$/i) or (/^$date_arg_regexp2$/i) or (/^$date_arg_regexp3$/i)); } elsif ($regexp eq 'delay') { $error = 1 unless (/^$delay_regexp$/i); } elsif ($regexp eq 'var') { $error = 1 unless (/^$var_regexp$/i); } elsif ($regexp eq 'subarg') { $error = 1 unless (/^$subarg_regexp$/i); } else { $error = 1 unless (/^$regexp$/i); } if ($error) { $log->syslog('err', 'Error at line %s: argument %s is not valid', $lnb, $_); return undef; } $Rused_labels->{$args[1]} = 1 if ($cmd eq 'next' && ($args[1])); $Rused_vars->{$args[0]} = 1 if ($var_commands{$cmd}); } } return 1; } ### TASK EXECUTION SUBROUTINES ### sub execute { my $task = shift; my $task_file = $task->{'filepath'}; # task to execute my %result; # stores the result of the chk_line subroutine my %vars; # list of task vars my $lnb = 0; # line number $log->syslog('notice', 'Running task %s, line %d with vars %s)', $task_file, $lnb, join('/', %vars)); unless (open(TASK, $task_file)) { $log->syslog('err', 'Can\'t read the task %s', $task_file); return undef; } my $label = $task->{'label'}; return undef if ($label eq 'ERROR'); $log->syslog('debug2', '* execution of the task %s', $task_file); if (length $label) { while () { chomp; $lnb++; chk_line($_, \%result); next unless defined $result{'label'}; last if $result{'label'} eq $label; } } # execution my $status; while () { chomp; $lnb++; unless (chk_line($_, \%result)) { $log->syslog('err', '%s', $result{'error'}); return undef; } # processing of the assignments if ($result{'nature'} eq 'assignment') { $status = $vars{$result{'var'}} = cmd_process($result{'command'}, $result{'Rarguments'}, $task, \%vars, $lnb); last unless defined($status); } # processing of the commands if ($result{'nature'} eq 'command') { $status = cmd_process($result{'command'}, $result{'Rarguments'}, $task, \%vars, $lnb); last unless (defined($status) && $status >= 0); } } close(TASK); unless (defined $status) { $log->syslog('err', 'Error while processing task, removing %s', $task_file); unless (unlink($task_file)) { $log->syslog('err', 'Unable to remove task file %s: %m', $task_file); return undef; } return undef; } unless ($status >= 0) { $log->syslog('notice', 'The task %s is now useless. Removing it', $task_file); unless (unlink($task_file)) { $log->syslog('err', 'Unable to remove task file %s: %m', $task_file); return undef; } } return 1; } sub cmd_process { my $command = $_[0]; # command name my $Rarguments = $_[1]; # command arguments my $task = $_[2]; # task my $Rvars = $_[3]; # variable list of the task my $lnb = $_[4]; # line number my $task_file = $task->{'filepath'}; $log->syslog('debug2', '(%s, %s, %d)', $command, $task_file, $lnb); # building of %context my %context = ('line_number' => $lnb); $log->syslog( 'debug2', 'Current task: %s', join(':', map { (defined $_) ? $_ : '' } (%$task)) ); # regular commands return stop($task, \%context) if ($command eq 'stop'); return next_cmd($task, $Rarguments, \%context) if ($command eq 'next'); return create_cmd($task, $Rarguments, \%context) if ($command eq 'create'); return exec_cmd($task, $Rarguments) if ($command eq 'exec'); return expire_bounce($task, $Rarguments, \%context) if ($command eq 'expire_bounce'); return purge_user_table($task, \%context) if ($command eq 'purge_user_table'); return purge_logs_table($task, \%context) if ($command eq 'purge_logs_table'); return purge_session_table($task, \%context) if ($command eq 'purge_session_table'); return purge_spools($task, \%context) if $command eq 'purge_spools'; return purge_tables($task, \%context) if ($command eq 'purge_tables'); return purge_one_time_ticket_table($task, \%context) if ($command eq 'purge_one_time_ticket_table'); return sync_include($task, \%context) if ($command eq 'sync_include'); return purge_orphan_bounces($task, \%context) if ($command eq 'purge_orphan_bounces'); return eval_bouncers($task, \%context) if ($command eq 'eval_bouncers'); return process_bouncers($task, \%context) if ($command eq 'process_bouncers'); # commands which use a variable return send_msg($task, $Rarguments, $Rvars, \%context) if ($command eq 'send_msg'); return rm_file($task, $Rarguments, $Rvars, \%context) if ($command eq 'rm_file'); # commands which return a variable return select_subs($task, $Rarguments, \%context) if ($command eq 'select_subs'); # commands which return and use a variable return delete_subs_cmd($task, $Rarguments, $Rvars, \%context) if ($command eq 'delete_subs'); } ### command subroutines ### # remove files whose name is given in the key 'file' of the hash sub rm_file { my ($task, $Rarguments, $Rvars, $context) = @_; my @tab = @{$Rarguments}; my $var = $tab[0]; foreach my $key (keys %{$Rvars->{$var}}) { my $file = $Rvars->{$var}{$key}{'file'}; next unless ($file); unless (unlink($file)) { error($task->{'filepath'}, "error in rm_file command : unable to remove $file"); return undef; } } return 1; } sub stop { my ($task, $context) = @_; my $task_file = $spool_task . '/' . $task->{'filename'}; $log->syslog('notice', '%s: stop %s', $context->{'line_number'}, $task_file); unlink($task_file) ? $log->syslog('notice', '--> %s deleted', $task_file) : error($task_file, "error in stop command : unable to delete task file"); return 0; } sub send_msg { my ($task, $Rarguments, $Rvars, $context) = @_; my @tab = @{$Rarguments}; my $template = $tab[1]; my $var = $tab[0]; $log->syslog( 'notice', 'Line %s: send_msg (@{%s})', $context->{'line_number'}, $Rarguments ); if ($task->{'object'} eq '_global') { foreach my $email (keys %{$Rvars->{$var}}) { $log->syslog('notice', '--> message sent to %s', $email); unless ($log_only) { unless ( Sympa::send_file( '*', $template, $email, $Rvars->{$var}{$email} ) ) { $log->syslog('notice', 'Unable to send template %s to %s', $template, $email); } } } } else { my $list = $task->{'list_object'}; foreach my $email (keys %{$Rvars->{$var}}) { $log->syslog('notice', '--> message sent to %s', $email); unless ($log_only) { unless ( Sympa::send_file( $list, $template, $email, $Rvars->{$var}{$email} ) ) { $log->syslog('notice', 'Unable to send template %s to %s', $template, $email); } } } } return 1; } sub next_cmd { my ($task, $Rarguments, $context) = @_; my @tab = @{$Rarguments}; # conversion of the date argument into epoch format my $date = Sympa::Tools::Time::epoch_conv($tab[0], $task->{'date'}); my $label = $tab[1]; $log->syslog('notice', "line $context->{'line_number'} of $task->{'model'} : next ($date, $label)" ); my $listname = $task->{'object'}; my $model = $task->{'model'}; my $filename = $task->{'filepath'}; ## Determine type my ($type, $model_choice); my %data = ( 'creation_date' => $task->{'date'}, 'execution_date' => 'execution_date' ); if ($listname eq '_global') { $type = '_global'; foreach my $key (keys %global_models) { if ($global_models{$key} eq $model) { $model_choice = $Conf::Conf{$key}; last; } } } else { $type = 'list'; my $list = $task->{'list_object'}; $data{'list'}{'name'} = $list->{'name'}; $data{'list'}{'robot'} = $list->{'domain'}; if ($model eq 'sync_include') { $data{'list'}{'ttl'} = $list->{'admin'}{'ttl'}; $model_choice = 'ttl'; } else { unless (defined $list->{'admin'}{"$model\_task"}) { error($filename, "List $list->{'name'} no more require $model task"); return undef; } $model_choice = $list->{'admin'}{"$model\_task"}{'name'}; } } unless (create($date, $tab[1], $model, $model_choice, \%data)) { error($filename, "error in create command : creation subroutine failure"); return undef; } # my $new_task = "$date.$label.$name[2].$name[3]"; my $human_date = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime $date); # my $new_task_file = "$spool_task/$new_task"; # unless (rename ($filename, $new_task_file)) { # error ($filename, # "error in next command : unable to rename task file into $new_task"); # return undef; # } unless (unlink($filename)) { error($filename, "error in next command : unable to remove task file $filename"); return undef; } $log->syslog('notice', '--> new task %s (%s)', $model, $human_date); return 0; } sub select_subs { my ($task, $Rarguments, $context) = @_; my @tab = @{$Rarguments}; my $condition = $tab[0]; $log->syslog( 'debug2', 'Line %s: select_subs (%s)', $context->{'line_number'}, $condition ); $condition =~ /(\w+)\(([^\)]*)\)/; if ($2) { # conversion of the date argument into epoch format my $date = Sympa::Tools::Time::epoch_conv($2, $task->{'date'}); $condition = "$1($date)"; } my @users; # the subscribers of the list my %selection; # hash of subscribers who match the condition my $list = $task->{'list_object'}; for ( my $user = $list->get_first_list_member(); $user; $user = $list->get_next_list_member() ) { push(@users, $user); } # parameter of subroutine Sympa::Scenario::verify my $verify_context = { 'sender' => 'nobody', 'email' => 'nobody', 'remote_host' => 'unknown_host', 'listname' => $task->{'object'} }; my $new_condition = $condition; # necessary to the older & newer condition rewriting # loop on the subscribers of $list_name foreach my $user (@users) { # AF : voir 'update' $log->syslog ('notice', "date $user->{'date'} & update $user->{'update'}"); # condition rewriting for older and newer $new_condition = "$1($user->{'update_date'}, $2)" if ($condition =~ /(older|newer)\((\d+)\)/); if (Sympa::Scenario::verify($verify_context, $new_condition) == 1) { $selection{$user->{'email'}} = undef; $log->syslog('notice', '--> user %s has been selected', $user->{'email'}); } } return \%selection; } sub delete_subs_cmd { my ($task, $Rarguments, $Rvars, $context) = @_; my @tab = @{$Rarguments}; my $var = $tab[0]; $log->syslog( 'notice', 'Line %s: delete_subs (%s)', $context->{'line_number'}, $var ); my $list = $task->{'list_object'}; my %selection; # hash of subscriber emails who are successfully deleted foreach my $email (keys %{$Rvars->{$var}}) { $log->syslog('notice', '%s', $email); my $result = Sympa::Scenario::request_action( $list, 'del', 'smime', { 'sender' => $Conf::Conf{'listmaster'}, #FIXME 'email' => $email, } ); my $action; $action = $result->{'action'} if (ref($result) eq 'HASH'); if ($action =~ /reject/i) { error($task->{'filepath'}, "error in delete_subs command : deletion of $email not allowed" ); } else { my $u = $list->delete_list_member( users => [$email], operation => 'auto_del' ) unless $log_only; $log->syslog('notice', '--> %s deleted', $email); $selection{$email} = {}; } } return \%selection; } sub create_cmd { my ($task, $Rarguments, $context) = @_; my @tab = @{$Rarguments}; my $arg = $tab[0]; my $model = $tab[1]; my $model_choice = $tab[2]; $log->syslog('notice', "line $context->{'line_number'} : create ($arg, $model, $model_choice)" ); # recovery of the object type and object my $type; my $object; if ($arg =~ /$subarg_regexp/) { $type = $1; $object = $3; } else { error($task->{'filepath'}, "error in create command : don't know how to create $arg"); return undef; } # building of the data hash necessary to the create subroutine my %data = ( 'creation_date' => $task->{'date'}, 'execution_date' => 'execution_date' ); if ($type eq 'list') { my $list = Sympa::List->new($object); $data{'list'}{'name'} = $list->{'name'}; } $type = '_global'; unless (create($task->{'date'}, '', $model, $model_choice, \%data)) { error($task->{'filepath'}, "error in create command : creation subroutine failure"); return undef; } return 1; } sub exec_cmd { my ($task, $Rarguments, $context) = @_; my @tab = @{$Rarguments}; my $file = $tab[0]; $log->syslog( 'notice', 'Line %s: exec (%s)', $context->{'line_number'}, $file ); system($file); return 1; } sub purge_logs_table { $log->syslog('debug2', '(%s, %s)', @_); my ($task, $context) = @_; #my $execution_date = $task->{'date'}; unless (_db_log_del()) { $log->syslog('err', 'Failed to delete logs'); return undef; } $log->syslog('notice', 'Logs purged'); if ($log->aggregate_stat) { $log->syslog('notice', 'Stats aggregated'); } return 1; } # Deletes logs in RDBMS. # If a log is older than $list->get_latest_distribution_date() - $delay # expire the log. sub _db_log_del { my ($exp, $date); my $sdm = Sympa::DatabaseManager->instance; $exp = Conf::get_robot_conf('*', 'logs_expiration_period'); $date = time - ($exp * 31 * 24 * 60 * 60); unless ( $sdm and $sdm->do_prepared_query( q{DELETE FROM logs_table WHERE date_logs <= ?}, $date ) ) { $log->syslog('err', 'Unable to delete db_log entry from the database'); return undef; } $exp = Conf::get_robot_conf('*', 'stats_expiration_period'); $date = time - ($exp * 31 * 24 * 60 * 60); unless ( $sdm->do_prepared_query( q{DELETE FROM stat_table WHERE date_stat <= ?}, $date ) ) { $log->syslog('err', 'Unable to delete db_log entry from the database'); return undef; } unless ( $sdm->do_prepared_query( q{DELETE FROM stat_counter_table WHERE end_date_counter <= ?}, $date ) ) { $log->syslog('err', 'Unable to delete db_log entry from the database'); return undef; } return 1; } ## remove sessions from session_table if older than $Conf::Conf{'session_table_ttl'} sub purge_session_table { $log->syslog('info', ''); my $removed = Sympa::Session::purge_old_sessions('*'); unless (defined $removed) { $log->syslog('err', 'Failed to remove old sessions'); return undef; } $log->syslog('notice', '%s row removed in session_table', $removed); return 1; } # Remove messages from spools if older than duration given by configuration. sub purge_spools { # Expiring bad messages in incoming spools and archive spool. foreach my $queue (qw(queue queueautomatic queuebounce queueoutgoing)) { my $directory = $Conf::Conf{$queue} . '/bad'; my $clean_delay = $Conf::Conf{'clean_delay_' . $queue}; if (-e $directory) { _clean_spool($directory, $clean_delay); } } # Expiring bad messages in digest spool. if (opendir my $dh, $Conf::Conf{'queuedigest'}) { my $base_dir = $Conf::Conf{'queuedigest'}; my @dirs = grep { !/\A\./ and -d $base_dir . '/' . $_ } readdir $dh; closedir $dh; foreach my $subdir (@dirs) { my $directory = $base_dir . '/' . $subdir . '/bad'; my $clean_delay = $Conf::Conf{'clean_delay_queuedigest'}; if (-e $directory) { _clean_spool($directory, $clean_delay); } } } # Expiring bad packets and messages in bulk spool. foreach my $subdir (qw(pct msg)) { my $directory = $Conf::Conf{'queuebulk'} . '/bad/' . $subdir; my $clean_delay = $Conf::Conf{'clean_delay_queuebulk'}; if (-e $directory) { _clean_spool($directory, $clean_delay); } } # Expiring moderation spools except mod, topic spool and temporary files. foreach my $queue ( qw(queueauth queueautomatic queuesubscribe queuetopic tmpdir)) { my $directory = $Conf::Conf{$queue}; my $clean_delay = $Conf::Conf{'clean_delay_' . $queue}; if (-e $directory) { _clean_spool($directory, $clean_delay); } } # Expiring mod spool. my $modqueue = $Conf::Conf{'queuemod'}; if (opendir my $dh, $modqueue) { my @qfiles = sort readdir $dh; closedir $dh; foreach my $i (@qfiles) { next if $i =~ /\A[.]/; next unless -f $modqueue . '/' . $i; $i =~ /\A(.+)_[.\w]+\z/; my $list = Sympa::List->new($1, '*', {just_try => 1}) if $1; my $moddelay; if (ref $list eq 'Sympa::List') { $moddelay = $list->{'admin'}{'clean_delay_queuemod'}; } else { $moddelay = $Conf::Conf{'clean_delay_queuemod'}; } if ($moddelay) { my $mtime = Sympa::Tools::File::get_mtime($modqueue . '/' . $i); if ($mtime < time - $moddelay * 86400) { unlink($modqueue . '/' . $i); $log->syslog('notice', 'Deleting unmoderated message %s, too old', $i); } } } } # Expiring formatted held messages. if (opendir my $dh, $Conf::Conf{'viewmail_dir'} . '/mod') { my $base_dir = $Conf::Conf{'viewmail_dir'} . '/mod'; my @dirs = grep { !/\A\./ and -d $base_dir . '/' . $_ } readdir $dh; closedir $dh; foreach my $list_id (@dirs) { my $clean_delay; my $list = Sympa::List->new($list_id, '*', {just_try => 1}); if (ref $list eq 'Sympa::List') { $clean_delay = $list->{'admin'}{'clean_delay_queuemod'}; } else { $clean_delay = $Conf::Conf{'clean_delay_queuemod'}; } my $directory = $base_dir . '/' . $list_id; if ($clean_delay and -e $directory) { _clean_spool($directory, $clean_delay); } } } # Removing messages in bulk spool with no more packet. my $pct_directory = $Conf::Conf{'queuebulk'} . '/pct'; my $msg_directory = $Conf::Conf{'queuebulk'} . '/msg'; if (opendir my $dh, $pct_directory) { my $msgpath; while ($msgpath = readdir $dh) { next if $msgpath =~ /\A\./; next unless -d $pct_directory . '/' . $msgpath; next if time - 3600 < Sympa::Tools::File::get_mtime( $pct_directory . '/' . $msgpath); # If packet directory is empty, remove message also. unlink($msg_directory . '/' . $msgpath) if rmdir($pct_directory . '/' . $msgpath); } closedir $dh; } return 1; } # Old name: tools::CleanSpool(), Sympa::Tools::File::CleanDir(). sub _clean_spool { $log->syslog('debug2', '(%s, %s)', @_); my ($directory, $clean_delay) = @_; return 1 unless $clean_delay; my $dh; unless (opendir $dh, $directory) { $log->syslog('err', 'Unable to open "%s" spool: %m', $directory); return undef; } my @qfile = sort grep { !/\A\.+\z/ and !/\Abad\z/ } readdir $dh; closedir $dh; my ($curlist, $moddelay); foreach my $f (@qfile) { if (Sympa::Tools::File::get_mtime("$directory/$f") < time - $clean_delay * 60 * 60 * 24) { if (-f "$directory/$f") { unlink("$directory/$f"); $log->syslog('notice', 'Deleting old file %s', "$directory/$f"); } elsif (-d "$directory/$f") { unless (Sympa::Tools::File::remove_dir("$directory/$f")) { $log->syslog('err', 'Cannot remove old directory %s: %m', "$directory/$f"); next; } $log->syslog('notice', 'Deleting old directory %s', "$directory/$f"); } } } return 1; } ## remove messages from bulkspool table when no more packet have any pointer ## to this message sub purge_tables { $log->syslog('info', ''); my $removed; $removed = 0; foreach my $robot (Sympa::List::get_robots()) { my $all_lists = Sympa::List::get_lists($robot); return 1 if $end; foreach my $list (@{$all_lists || []}) { my $tracking = Sympa::Tracking->new(context => $list); $removed += $tracking->remove_message_by_period( $list->{'admin'}{'tracking'}{'retention_period'}); } } $log->syslog('notice', "%s rows removed in tracking table", $removed); return 1; } ## remove one time ticket table if older than $Conf::Conf{'one_time_ticket_table_ttl'} sub purge_one_time_ticket_table { $log->syslog('info', ''); my $removed = Sympa::Ticket::purge_old_tickets('*'); unless (defined $removed) { $log->syslog('err', 'Failed to remove old tickets'); return undef; } $log->syslog('notice', '%s row removed in one_time_ticket_table', $removed); return 1; } sub purge_user_table { my ($task, $Rarguments, $context) = @_; $log->syslog('debug2', ''); my $sdm = Sympa::DatabaseManager->instance; my $time = time; # Marking super listmasters foreach my $l (Sympa::get_listmasters_email('*')) { unless ( $sdm and $sdm->do_prepared_query( q{UPDATE user_table SET last_active_date_user = ? WHERE email_user = ?}, $time, lc $l ) ) { $log->syslog('err', 'Failed to check activity of users'); return undef; } } # Marking per-robot listmasters. foreach my $robot_id (Sympa::List::get_robots()) { foreach my $l (Sympa::get_listmasters_email($robot_id)) { unless ( $sdm->do_prepared_query( q{UPDATE user_table SET last_active_date_user = ? WHERE email_user = ?}, $time, lc $l ) ) { $log->syslog('err', 'Failed to check activity of users'); return undef; } } } # Marking new users, owners/editors and subscribers. unless ( $sdm->do_prepared_query( q{UPDATE user_table SET last_active_date_user = ? WHERE last_active_date_user IS NULL OR EXISTS ( SELECT 1 FROM admin_table WHERE admin_table.user_admin = user_table.email_user ) OR EXISTS ( SELECT 1 FROM subscriber_table WHERE subscriber_table.user_subscriber = user_table.email_user )}, $time ) ) { $log->syslog('err', 'Failed to check activity of users'); return undef; } # Look for unused entries. my @purged_users; my $sth; unless ( $sth = $sdm->do_prepared_query( q{SELECT email_user FROM user_table WHERE last_active_date_user IS NOT NULL AND last_active_date_user < ?}, $time ) ) { $log->syslog('err', 'Failed to get inactive users'); return undef; } @purged_users = grep {$_} map { $_->[0] } @{$sth->fetchall_arrayref || []}; $sth->finish; # Purge unused entries. foreach my $email (@purged_users) { my $user = Sympa::User->new($email); next unless $user; unless ($user->expire) { $log->syslog('err', 'Failed to purge inactive user %s', $user); return undef; } else { $log->syslog('info', 'User %s was expired', $user); } } return scalar @purged_users; } ## Subroutine which remove bounced message of no-more known users sub purge_orphan_bounces { my ($task, $context) = @_; $log->syslog('info', ''); my $all_lists = Sympa::List::get_lists('*'); foreach my $list (@{$all_lists || []}) { # First time: loading DB entries into %bounced_users, # hash {'bounced address' => 1} my %bounced_users; for ( my $user_ref = $list->get_first_bouncing_list_member(); $user_ref; $user_ref = $list->get_next_bouncing_list_member() ) { my $user_id = $user_ref->{'email'}; $bounced_users{Sympa::Tools::Text::escape_chars($user_id)} = 1; } my $bounce_dir = $list->get_bounce_dir(); unless (-d $bounce_dir) { $log->syslog('notice', 'No bouncing subscribers in list %s', $list); next; } # Then reading Bounce directory & compare with %bounced_users my $dh; unless (opendir $dh, $bounce_dir) { $log->syslog('err', 'Error while opening bounce directory %s', $bounce_dir); return undef; } # Finally removing orphan files my $marshalled; while ($marshalled = readdir $dh) { my $metadata = Sympa::Spool::unmarshal_metadata($bounce_dir, $marshalled, qr/\A([^\s\@]+\@[\w\.\-*]+?)(?:_(\w+))?\z/, [qw(recipient envid)]); next unless $metadata; # Skip _ which is used by tracking feature. next if defined $metadata->{envid}; unless ($bounced_users{$marshalled}) { $log->syslog('info', 'Removing orphan Bounce for user %s in list %s', $marshalled, $list); unless (unlink($bounce_dir . '/' . $marshalled)) { $log->syslog('err', 'Error while removing file %s/%s', $bounce_dir, $marshalled); } } } closedir $dh; } return 1; } # If a bounce is older than $list->get_latest_distribution_date() - $delay # expire the bounce. sub expire_bounce { $log->syslog('debug2', '(%s, %s, %s)', @_); #FXIME: May this variable be set in to task model ? my ($task, $Rarguments, $context) = @_; my @tab = @{$Rarguments}; my $delay = $tab[0]; my $all_lists = Sympa::List::get_lists('*'); foreach my $list (@{$all_lists || []}) { my $listname = $list->{'name'}; # the reference date is the date until which we expire bounces in # second # the latest_distribution_date is the date of last distribution #days # from 01 01 1970 unless ($list->get_latest_distribution_date()) { $log->syslog( 'debug2', 'Bounce expiration: skipping list %s because could not get latest distribution date', $listname ); next; } my $refdate = (($list->get_latest_distribution_date() - $delay) * 3600 * 24); for ( my $u = $list->get_first_bouncing_list_member(); $u; $u = $list->get_next_bouncing_list_member() ) { $u->{'bounce'} =~ /^(\d+)\s+(\d+)\s+(\d+)(\s+(.*))?$/; $u->{'last_bounce'} = $2; if ($u->{'last_bounce'} < $refdate) { my $email = $u->{'email'}; unless ($list->is_list_member($email)) { $log->syslog('info', '%s not subscribed', $email); next; } unless ( $list->update_list_member( $email, bounce => undef, bounce_address => undef ) ) { $log->syslog('info', 'Failed update database for %s', $email); next; } my $escaped_email = Sympa::Tools::Text::escape_chars($email); my $bounce_dir = $list->get_bounce_dir(); unless (unlink $bounce_dir . '/' . $escaped_email) { $log->syslog( 'info', 'Failed deleting %s', $bounce_dir . '/' . $escaped_email ); next; } $log->syslog( 'info', 'Expire bounces for subscriber %s of list %s (last distribution %s, last bounce %s)', $email, $listname, POSIX::strftime( "%Y-%m-%d", localtime( $list->get_latest_distribution_date() * 3600 * 24 ) ), POSIX::strftime( "%Y-%m-%d", localtime($u->{'last_bounce'}) ) ); } } } # Expiring formatted bounce messages. if (opendir my $dh, $Conf::Conf{'viewmail_dir'} . '/bounce') { my $base_dir = $Conf::Conf{'viewmail_dir'} . '/bounce'; my @dirs = grep { !/\A\./ and -d $base_dir . '/' . $_ } readdir $dh; closedir $dh; foreach my $list_id (@dirs) { my $directory = $base_dir . '/' . $list_id; if (-e $directory) { _clean_spool($directory, $delay); } } } return 1; } # Removed because not yet fully implemented. See r11771. #sub chk_cert_expiration; # Removed becuase not yet fully implemented. See r11771. #sub update_crl; ## Subroutine for bouncers evaluation: # give a score for each bouncing user sub eval_bouncers { ################# my ($task, $context) = @_; my $all_lists = Sympa::List::get_lists('*'); foreach my $list (@{$all_lists || []}) { my $listname = $list->{'name'}; my $list_traffic = {}; $log->syslog('info', '(%s)', $listname); ## Analizing file Msg-count and fill %$list_traffic unless (open(COUNT, $list->{'dir'} . '/msg_count')) { $log->syslog('debug', '** Could not open msg_count FILE for list %s', $listname); next; } while () { if (/^(\w+)\s+(\d+)/) { my ($a, $b) = ($1, $2); $list_traffic->{$a} = $b; } } close(COUNT); #for each bouncing user for ( my $user_ref = $list->get_first_bouncing_list_member(); $user_ref; $user_ref = $list->get_next_bouncing_list_member() ) { my $score = get_score($user_ref, $list_traffic) || 0; # Copying score into database. unless ( $list->update_list_member( $user_ref->{'email'}, bounce_score => $score ) ) { $log->syslog('err', 'Error while updating DB for user %s', $user_ref->{'email'}); next; } } } return 1; } sub none { 1; } # Routine for automatic bouncing users management # # This sub apply a treatment foreach category of bouncing-users # # The relation between possible actions and correponding subroutines # is indicated by the following hash (%actions). # It's possible to add actions by completing this hash and the one in list # config (file List.pm, in sections "bouncers_levelX"). Then you must write # the code for your action: # The action subroutines have two parameter : # - the name of the current list # - a reference on users email list: # Look at the "remove_bouncers" sub in List.pm for an example sub process_bouncers { my ($task, $context) = @_; $log->syslog('info', 'Processing automatic actions on bouncing users'); ## possible actions my %actions = ( 'remove_bouncers' => \&Sympa::List::remove_bouncers, 'notify_bouncers' => \&Sympa::List::notify_bouncers, 'none' => \&none ); my $all_lists = Sympa::List::get_lists('*'); foreach my $list (@{$all_lists || []}) { my $listname = $list->{'name'}; my @bouncers; # @bouncers = ( # ['email1', 'email2', 'email3',....,], There is one line # ['email1', 'email2', 'email3',....,], foreach bounce # ['email1', 'email2', 'email3',....,], level. # ); my $max_level; for ( my $level = 1; defined($list->{'admin'}{'bouncers_level' . $level}); $level++ ) { $max_level = $level; } ## first, bouncing email are sorted in @bouncer for ( my $user_ref = $list->get_first_bouncing_list_member(); $user_ref; $user_ref = $list->get_next_bouncing_list_member() ) { # Skip included users (cannot be removed) next if $user_ref->{'included'}; for (my $level = $max_level; ($level >= 1); $level--) { if ($user_ref->{'bounce_score'} >= $list->{'admin'}{'bouncers_level' . $level}{'rate'}) { push(@{$bouncers[$level]}, $user_ref->{'email'}); $level = ($level - $max_level); } } } ## then, calling action foreach level for (my $level = $max_level; ($level >= 1); $level--) { my $action = $list->{'admin'}{'bouncers_level' . $level}{'action'}; my $notification = $list->{'admin'}{'bouncers_level' . $level}{'notification'}; my $robot_id = $list->{'domain'}; if (@{$bouncers[$level] || []}) { ## calling action subroutine with (list,email list) in ## parameter unless ($actions{$action}->($list, $bouncers[$level])) { $log->syslog( 'err', 'Error while calling action sub for bouncing users in list %s', $listname ); return undef; } # Notify owner or listmaster with list, action, email list. my $param = { #'listname' => $listname, # No longer used (<=6.1) 'action' => $action, 'user_list' => \@{$bouncers[$level]}, 'total' => scalar(@{$bouncers[$level]}), }; if ($notification eq 'owner') { $list->send_notify_to_owner('automatic_bounce_management', $param); } elsif ($notification eq 'listmaster') { Sympa::send_notify_to_listmaster($list, 'automatic_bounce_management', $param); } } } } return 1; } sub get_score { my $user_ref = shift; my $list_traffic = shift; $log->syslog('debug', '(%s)', $user_ref->{'email'}); my $min_period = $Conf::Conf{'minimum_bouncing_period'}; my $min_msg_count = $Conf::Conf{'minimum_bouncing_count'}; # Analizing bounce_subscriber_field and keep useful infos for notation $user_ref->{'bounce'} =~ /^(\d+)\s+(\d+)\s+(\d+)(\s+(.*))?$/; my $BO_period = int($1 / 86400) - $Conf::Conf{'bounce_delay'}; my $EO_period = int($2 / 86400) - $Conf::Conf{'bounce_delay'}; my $bounce_count = $3; my $bounce_type = $4; my $msg_count = 0; my $min_day = $EO_period; unless ($bounce_count >= $min_msg_count) { #not enough messages distributed to keep score $log->syslog('debug', 'Not enough messages for evaluation of user %s', $user_ref->{'email'}); return undef; } unless (($EO_period - $BO_period) >= $min_period) { #too short bounce period to keep score $log->syslog('debug', 'Too short period for evaluate %s', $user_ref->{'email'}); return undef; } # calculate number of messages distributed in list while user was bouncing foreach my $date (sort { $b <=> $a } keys(%$list_traffic)) { if (($date >= $BO_period) && ($date <= $EO_period)) { $min_day = $date; $msg_count += $list_traffic->{$date}; } } # Adjust bounce_count when msg_count file is too recent, compared to the # bouncing period my $tmp_bounce_count = $bounce_count; unless ($EO_period == $BO_period) { my $ratio = (($EO_period - $min_day) / ($EO_period - $BO_period)); $tmp_bounce_count *= $ratio; } ## Regularity rate tells how much user has bounced compared to list ## traffic $msg_count ||= 1; ## Prevents "Illegal division by zero" error my $regularity_rate = $tmp_bounce_count / $msg_count; ## type rate depends on bounce type (5 = permanent ; 4 =tewmporary) my $type_rate = 1; $bounce_type =~ /(\d)\.(\d)\.(\d)/; if ($1 == 4) { # if its a temporary Error: score = score/2 $type_rate = .5; } my $note = $bounce_count * $regularity_rate * $type_rate; ## Note should be an integer $note = int($note + 0.5); # $note = 100 if ($note > 100); # shift between message ditrib & bounces => # note > 100 return $note; } ### MISCELLANEOUS SUBROUTINES ### ## when we catch signal, just change the value of the loop variable. sub sigterm { my ($sig) = @_; $log->syslog('notice', 'Signal %s received, still processing current task', $sig); $end = 1; } ## sort task name by their epoch date sub epoch_sort { $a =~ /(\d+)\..+/; my $date1 = $1; $b =~ /(\d+)\..+/; my $date2 = $1; $date1 <=> $date2; } ## change the label of a task file sub change_label { my $task_file = $_[0]; my $new_label = $_[1]; my $new_task_file = $task_file; $new_task_file =~ s/(.+\.)(\w*)(\.\w+\.\w+$)/$1$new_label$3/; if (rename($task_file, $new_task_file)) { $log->syslog('notice', '%s renamed in %s', $task_file, $new_task_file); } else { $log->syslog('err', 'Error; can\'t rename %s in %s', $task_file, $new_task_file); } } ## send a error message to list-master, log it, and change the label task into ## 'ERROR' sub error { my $task_file = $_[0]; my $message = $_[1]; my @param; $param[0] = "An error has occurred during the execution of the task $task_file : $message"; $log->syslog('err', '%s', $message); change_label($task_file, 'ERROR') unless $task_file eq ''; #FIXME: Coresponding mail template would be added. Sympa::send_notify_to_listmaster('*', 'error_in_task', \@param); } sub sync_include { my ($task, $context) = @_; $log->syslog('debug2', '(%s)', $task->{'id'}); my $list = $task->{'list_object'}; $list->sync_include; $list->sync_include_admin if @{$list->{'admin'}{'editor_include'} || []} or @{$list->{'admin'}{'owner_include'} || []}; if (not $list->has_include_data_sources and (not -e $list->{'dir'} . '/.last_sync.member' or [stat $list->{'dir'} . '/.last_sync.member']->[9] > [stat $list->{'dir'} . '/config']->[9]) ) { $log->syslog('debug', 'List %s no more require sync_include task', $list); return -1; } } __END__ =encoding utf-8 =head1 NAME task_manager, task_manager.pl - Daemon to Process Periodical Sympa Tasks =head1 SYNOPSIS C S<[ C<--foreground> ]> S<[ C<--debug> ]> =head1 DESCRIPTION XXX @todo doc =head1 OPTIONS =over 4 =item C<-d>, C<--debug> Sets the debug mode =item C<-f>, C<--config=>I Force task_manager to use an alternative configuration file instead of F<--CONFIG-->. =item C<-F>, C<--foreground> Prevents the script from being daemonized =item C<-h>, C<--help> Prints this help message. =item C<--log_level=>I Set log level. =back =head1 FILES F<$SPOOLDIR/task/> directory for task spool. F<$PIDDIR/task_manager.pid> this file contains the process ID of F. =head1 MORE DOCUMENTATION The full documentation in HTML and PDF formats can be found in L. The mailing lists (with web archives) can be accessed at L. =head1 BUGS Report bugs to Sympa bug tracker. See L. =head1 SEE ALSO L, L =cut sympa-6.2.24/src/sbin/bounced.pl.in0000644000175000017500000002207713216651447016016 0ustar rackeracke#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . ## Worl Wide Sympa is a front-end to Sympa Mailing Lists Manager ## Copyright Comite Reseau des Universites ## Patch 2001.07.24 by nablaphi ## Change the Getopt::Std to Getopt::Long use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use POSIX qw(); use Sympa::Alarm; use Conf; use Sympa::Constants; use Sympa::Crash; # Show traceback. use Sympa::DatabaseManager; use Sympa::Log; use Sympa::Process; use Sympa::Spindle::ProcessBounce; my $process = Sympa::Process->instance; $process->init(pidname => 'bounced'); ## Check options my %options; unless ( GetOptions( \%main::options, 'config|f=s', 'debug|d', 'help|h', 'log_level=s', 'foreground|F', 'version|v', ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($main::options{'help'}) { pod2usage(0); } elsif ($main::options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; $main::options{'foreground'} = 1; } my $log = Sympa::Log->instance; $log->{log_to_stderr} = 'all' if $main::options{'foreground'}; # Load sympa.conf unless (Conf::load()) { die sprintf "Unable to load sympa configuration, file %s has errors.\n", Conf::get_sympa_conf(); } # Check database connectivity unless (Sympa::DatabaseManager->instance) { die sprintf "Database %s defined in sympa.conf has not the right structure or is unreachable.\n", $Conf::Conf{'db_name'}; } # Put ourselves in background if not in debug mode. unless ($main::options{'foreground'}) { $process->daemonize; } # Create and write the PID file. $process->write_pid(initial => 1); # If process is running in foreground, don't write STDERR to a dedicated file. unless ($main::options{foreground}) { $process->direct_stderr_to_file; } if ($main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; $log->syslog( 'info', 'Configuration file read, log level set using options: %s', $main::options{'log_level'} ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog( 'info', 'Configuration file read, default log level %s', $Conf::Conf{'log_level'} ); } my $log_facility = $Conf::Conf{'log_facility'} || $Conf::Conf{'syslog'}; $log->openlog($log_facility, $Conf::Conf{'log_socket_type'}); ## Set the User ID & Group ID for the process $GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2]; $UID = $EUID = (getpwnam(Sympa::Constants::USER))[2]; ## Required on FreeBSD to change ALL IDs ## (effective UID + real UID + saved UID) POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]); POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]); ## Check if the UID has correctly been set (useful on OS X) unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2]) && ($UID == (getpwnam(Sympa::Constants::USER))[2])) { die "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n"; } ## Sets the UMASK umask(oct($Conf::Conf{'umask'})); ## Change to list root unless (chdir($Conf::Conf{'home'})) { die sprintf "Unable to change directory to %s: %s", $Conf::Conf{'home'}, $!; } $log->syslog('notice', 'Bounced %s Started', Sympa::Constants::VERSION()); my $spindle = Sympa::Spindle::ProcessBounce->new; ## Catch signals, in order to exit cleanly, whenever possible. $SIG{'TERM'} = 'sigterm'; $SIG{'INT'} = 'sigterm'; while (not $spindle->{finish}) { $spindle->spin; last if $spindle->{finish}; # If the spool was empty, sleep for a while. sleep $Conf::Conf{'sleep'}; } # Purge grouped notifications Sympa::Alarm->instance->flush(purge => 1); $log->syslog('notice', 'Bounced exited normally due to signal'); $process->remove_pid(final => 1); exit(0); # When we catch signal, just change the value of the loop variable. sub sigterm { my ($sig) = @_; $log->syslog('notice', 'Signal %s received, still processing current task', $sig); $spindle->{finish} = $sig; } ## copy the bounce to the appropriate filename # Moved: Use Sympa::Tracking::store(). #sub store_bounce; # Moved to Sympa::Spindle::ProcessBounce::_twist(). #sub process_message; # Old name: Bounce::rfc1891(). # Moved to Sympa::Spindle::ProcessBounce::_parse_dsn(). #sub _parse_dsn; # Moved to Sympa::Spindle::ProcessBounce::_parse_multipart_report(). #sub _parse_multipart_report; # Moved to Sympa::Spindle::ProcessBounce::_decode_utf_8_addr_xtext(). #sub _decode_utf_8_addr_xtext; # Moved to %Sympa::Spindle::ProcessBounce::equiv. #my %equiv; # Old name: Bounce::corrige(). # Moved to Sympa::Spindle::ProcessBounce::_corrige(). #sub _corrige; # Old name: Bounce::anabounce(). # Moved to Sympa::Spindle::ProcessBounce::_anabounce(). #sub _anabounce; # Moved to Sympa::Spindle::ProcessBounce::_canonicalize_status(). #sub _canonicalize_status; # Moved: Now a subroutine of Sympa::Tracking::store(). #sub _update_subscriber_bounce_history; # If bounce can't be handled correctly, saves it to the "bad" subdirectory of # the bounce spool. #DEPRECATED. Use Sympa::Spool::Bounce::quarantine(). # sub quarantine; #DEPRECATED. Use Sympa::Spool::Bounce::remove(). # sub remove; __END__ =encoding utf-8 =head1 NAME bounced, bounced.pl - Mailing List Bounce Processing Daemon for Sympa =head1 SYNOPSIS C S<[ C<--foreground> ]> S<[ C<--debug> ]> =head1 DESCRIPTION Bounced is a program which scans permanently the bounce spool and processes bounces (non-delivery messages), looking or bad addresses. Bouncing addresses are tagged in database ; last bounce is kept for each bouncing address. List owners will latter access bounces information via WWSympa. =head1 OPTIONS These programs follow the usual GNU command line syntax, with long options starting with two dashes (C<-->). A summary of options is included below. =over 5 =item C<-F>, C<--foreground> Do not detach TTY. =item C<-f>, C<--config=>I Force bounced to use an alternative configuration file instead of F<--CONFIG-->. =item C<-d>, C<--debug> Run the program in a debug mode. =item C<-h>, C<--help> Print this help message. =item C<--log_level=>I Sets daemon log level. =back =head1 FILES F<--CONFIG--> Sympa configuration file. F<$LIBEXECDIR/bouncequeue> bounce spooler, referenced from sendmail alias file F<$SPOOLDIR/bounce> incoming bounces directory F<$PIDDIR/bounced.pid> this file contains the process ID of F. =head1 MORE DOCUMENTATION The full documentation can be found in L. The mailing lists (with web archives) can be accessed at L. =head1 HISTORY This program was originally written by: =over 4 =item Serge Aumont ComitE<233> RE<233>seau des UniversitE<233>s =item Olivier SalaE<252>n ComitE<233> RE<233>seau des UniversitE<233>s =back This manual page was initially written by JE<233>rE<244>me Marant for the Debian GNU/Linux system. =head1 LICENSE You may distribute this software under the terms of the GNU General Public License Version 2. For more details see F file. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts and no Back-Cover Texts. A copy of the license can be found under L. =head1 BUGS Report bugs to Sympa bug tracker. See L. =head1 SEE ALSO L, L, L, L. L. =cut sympa-6.2.24/src/sbin/sympa_msg.pl.in0000644000175000017500000004027713216651447016400 0ustar rackeracke#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use POSIX qw(); use Sympa::Alarm; use Conf; use Sympa::Constants; use Sympa::Crash; # Show traceback. use Sympa::DatabaseManager; use Sympa::Language; use Sympa::Log; use Sympa::Mailer; use Sympa::Process; use Sympa::Spindle::ProcessDigest; use Sympa::Spindle::ProcessIncoming; use Sympa::Tools::Data; my $process = Sympa::Process->instance; $process->init(pidname => 'sympa_msg', name => 'sympa/msg'); ## Internal tuning # delay between each read of the digestqueue my $digestsleep = 5; ## Init random engine srand(time()); # Check options. my %options; unless ( GetOptions( \%main::options, 'debug|d', 'log_level=s', 'foreground', 'config|f=s', 'lang|l=s', 'mail|m', 'keepcopy|k=s', 'help|h', 'version|v', ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($main::options{'help'}) { pod2usage(0); } elsif ($main::options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; $main::options{'foreground'} = 1; } my $log = Sympa::Log->instance; $log->{log_to_stderr} = 'all' if $main::options{'foreground'}; my $language = Sympa::Language->instance; my $mailer = Sympa::Mailer->instance; _load(); # Put ourselves in background if we're not in debug mode. unless ($main::options{'foreground'}) { $process->daemonize; # Fork a new process dedicated to automatic list creation, if required. if ($Conf::Conf{'automatic_list_feature'} eq 'on') { my $child_pid = fork; if ($child_pid) { waitpid $child_pid, 0; $CHILD_ERROR and die; } elsif (not defined $child_pid) { die sprintf 'Cannot fork: %s', $ERRNO; } else { # We're in the specialized child process: # automatic lists creation. exec q{--sbindir--/sympa_automatic.pl}, map { defined $main::options{$_} ? ("--$_", $main::options{$_}) : () } qw(config log_level mail); die sprintf 'Cannot exec: %s', $ERRNO; } } } $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); # Create and write the PID file. $process->write_pid(initial => 1); # If process is running in foreground, don't write STDERR to a dedicated file. unless ($main::options{foreground}) { $process->direct_stderr_to_file; } # Start multiple processes if required. unless ($main::options{'foreground'}) { if (0 == $process->{generation} and ($Conf::Conf{'incoming_max_count'} || 0) > 1) { # Disconnect from database before fork to prevent DB handles # to be shared by different processes. Sharing database # handles may crash sympa_msg.pl. Sympa::DatabaseManager->disconnect; for my $process_count (2 .. $Conf::Conf{'incoming_max_count'}) { my $child_pid = $process->fork; if ($child_pid) { $log->syslog('info', 'Starting child daemon, PID %s', $child_pid); # Saves the PID number $process->write_pid(pid => $child_pid); #$created_children{$child_pid} = 1; sleep 1; } elsif (not defined $child_pid) { $log->syslog('err', 'Cannot fork: %m'); last; } else { # We're in a child process close STDERR; $process->direct_stderr_to_file; $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); $log->syslog('info', 'Slave daemon started with PID %s', $PID); last; } } # Restore persistent connection. Sympa::DatabaseManager->instance or die 'Reconnecting database failed'; } } # Set the User ID & Group ID for the process $GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2]; $UID = $EUID = (getpwnam(Sympa::Constants::USER))[2]; ## Required on FreeBSD to change ALL IDs ## (effective UID + real UID + saved UID) POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]); POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]); ## Check if the UID has correctly been set (useful on OS X) unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2]) && ($UID == (getpwnam(Sympa::Constants::USER))[2])) { die "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n"; } # Sets the UMASK umask(oct($Conf::Conf{'umask'})); ## Most initializations have now been done. $log->syslog('notice', 'Sympa/msg %s Started', Sympa::Constants::VERSION()); # Check for several files. # Prevent that 2 processes perform checks at the same time... #FIXME: This would be done in --health_check mode. unless (Conf::checkfiles()) { die "Missing files.\n"; ## No return. } ## Do we have right access in the directory if ($main::options{'keepcopy'}) { if (!-d $main::options{'keepcopy'}) { $log->syslog( 'notice', 'Cannot keep a copy of incoming messages: %s is not a directory', $main::options{'keepcopy'} ); delete $main::options{'keepcopy'}; } elsif (!-w $main::options{'keepcopy'}) { $log->syslog( 'notice', 'Cannot keep a copy of incoming messages: no write access to %s', $main::options{'keepcopy'} ); delete $main::options{'keepcopy'}; } } my $spindle = Sympa::Spindle::ProcessIncoming->new( keepcopy => $main::options{keepcopy}, lang => $main::options{lang}, log_level => $main::options{log_level}, log_smtp => $main::options{mail}, #FIXME: Is it required? debug_virus_check => $main::options{debug}, ); # Catch signals, in order to exit cleanly, whenever possible. $SIG{'TERM'} = 'sigterm'; $SIG{'INT'} = 'sigterm'; # Interrupt from terminal. $SIG{'HUP'} = 'sighup'; $SIG{'PIPE'} = 'IGNORE'; # Ignore SIGPIPE ; prevents process from dying # Main loop. # This loop is run foreach HUP signal received. my $index_queuedigest = 0; # verify the digest queue while (not $spindle->{finish} or $spindle->{finish} ne 'term') { # Process digest only in master process ({generation} is 0). # Scan queuedigest. if (0 == $process->{generation} and $index_queuedigest++ >= $digestsleep) { $index_queuedigest = 0; Sympa::Spindle::ProcessDigest->new->spin; } $spindle->spin; if ($spindle->{finish} and $spindle->{finish} eq 'hup') { # Disconnect from Database Sympa::DatabaseManager->disconnect; $log->syslog('notice', 'Sympa %s reload config', Sympa::Constants::VERSION); _load(); $spindle = Sympa::Spindle::ProcessIncoming->new( keepcopy => $main::options{keepcopy}, lang => $main::options{lang}, log_level => $main::options{log_level}, log_smtp => $main::options{mail}, #FIXME: Is it required? debug_virus_check => $main::options{debug}, ); next; } elsif ($spindle->{finish}) { last; } # Sleep for a while if spool is empty. sleep $Conf::Conf{'sleep'}; } # Purge grouped notifications Sympa::Alarm->instance->flush(purge => 1); $log->syslog('notice', 'Sympa/msg exited normally due to signal'); $process->remove_pid; exit(0); # Load configuration. sub _load { ## Load sympa.conf. unless (Conf::load(Conf::get_sympa_conf(), 'no_db')) { #Site and Robot die sprintf "Unable to load sympa configuration, file %s or one of the vhost robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } ## Open the syslog and say we're read out stuff. $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); # Enable SMTP logging if required $mailer->{log_smtp} = $main::options{'mail'} || Sympa::Tools::Data::smart_eq($Conf::Conf{'log_smtp'}, 'on'); # setting log_level using conf unless it is set by calling option if (defined $main::options{'log_level'}) { $log->{level} = $main::options{'log_level'}; $log->syslog( 'info', 'Configuration file read, log level set using options: %s', $main::options{'log_level'} ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog( 'info', 'Configuration file read, default log level %s', $Conf::Conf{'log_level'} ); } if (Conf::cookie_changed()) { die sprintf 'sympa.conf/cookie parameter has changed. You may have severe inconsitencies into password storage. Restore previous cookie or write some tool to re-encrypt password in database and check spools contents (look at %s/cookies.history file).', $Conf::Conf{'etc'}; } # Check database connectivity. unless (Sympa::DatabaseManager->instance) { die sprintf "Database %s defined in sympa.conf is unreachable. verify db_xxx parameters in sympa.conf\n", $Conf::Conf{'db_name'}; } # Now trying to load full config (including database) unless (Conf::load()) { #FIXME: load Site, then robot cache die sprintf "Unable to load Sympa configuration, file %s or any of the virtual host robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } ## Set locale configuration ## Compatibility with version < 2.3.3 $main::options{'lang'} =~ s/\.cat$// if defined $main::options{'lang'}; $language->set_lang($main::options{'lang'}, $Conf::Conf{'lang'}, 'en'); ## Main program if (!chdir($Conf::Conf{'home'})) { die sprintf 'Can\'t chdir to %s: %s', $Conf::Conf{'home'}, $ERRNO; ## Function never returns. } ## Check for several files. unless (Conf::checkfiles_as_root()) { die "Missing files\n"; } } ############################################################ # sigterm ############################################################ # When we catch signal, just changes the value of the $signal # loop variable. # # IN : - # # OUT : - # ############################################################ sub sigterm { my ($sig) = @_; $log->syslog('notice', 'Signal %s received, still processing current task', $sig); $spindle->{finish} = 'term'; } ############################################################ # sighup ############################################################ # When we catch SIGHUP, changes the value of the $signal # loop variable and puts the "-mail" logging option # # IN : - # # OUT : - # ########################################################### sub sighup { if ($mailer->{log_smtp}) { $log->syslog('notice', 'signal HUP received, switch of the "-mail" logging option and continue current task' ); $mailer->{log_smtp} = undef; } else { $log->syslog('notice', 'signal HUP received, switch on the "-mail" logging option and continue current task' ); $mailer->{log_smtp} = 1; } $spindle->{finish} = 'hup'; } # Moved to Sympa::Spindle::ProcessIncoming::_twist(). #sub process_message; #sub DoSendMessage($message); #DEPRECATED: Run upgrade_send_spool.pl to migrate message with old format. # Moved to Sympa::Spindle::DoForward::_twist(). #sub DoForward; # Moved (divided) to Sympa::Spindle::DoMessage::_twist() & # Sympa::Spindle::AuthorizeMessage::_twist(). #sub DoMessage; # Old name: tools::checkcommand(). # Moved to Sympa::Spindle::DoMessage::_check_command(). #sub _check_command; # Moved to Sympa::Spindle::DoCommand::_twist(). #sub DoCommand; # DEPRECATED. Use Sympa::Spindle::ProcessDigest class. #sub SendDigest; # Moved to Sympa::Spindle::ProcessIncoming::_clean_msgid_table(). #sub clean_msgid_table; __END__ =encoding utf-8 =head1 NAME sympa_msg, sympa_msg.pl - Daemon to handle incoming messages =head1 SYNOPSIS C S<[ C<-d>, C<--debug> ]> S<[ C<-f>, C<--file>=I ]> S<[ C<-k>, C<--keepcopy>=I ]> S<[ C<-l>, C<--lang>=I ]> S<[ C<-m>, C<--mail> ]> S<[ C<-h>, C<--help> ]> S<[ C<-v>, C<--version> ]> =head1 DESCRIPTION Sympa_msg.pl is a program which scans permanently the incoming message spool and processes each message. Messages bound for the lists and authorized sending are modified as neccesity and at last stored into digest spool, archive spool and outgoing spool. Those bound for command addresses are interpreted and appropriate actions are taken. Those bound for listmasters or list admins are forwarded to them. =head1 OPTIONS Sympa_msg.pl follows the usual GNU command line syntax, with long options starting with two dashes (C<-->). A summary of options is included below. =over 4 =item C<-d>, C<--debug> Enable debug mode. =item C<-f>, C<--config=>I Force Sympa to use an alternative configuration file instead of F<--CONFIG-->. =item C<-l>, C<--lang=>I Set this option to use a language for Sympa. The corresponding gettext catalog file must be located in F<$LOCALEDIR> directory. =item C<--log_level=>I Sets Sympa log level. =back F may run in daemon mode with following options. =over 4 =item C<--foreground> The process remains attached to the TTY. =item C<-k>, C<--keepcopy=>F This option tells Sympa to keep a copy of every incoming message, instead of deleting them. `directory' is the directory to store messages. =item C<-m>, C<--mail> Sympa will log calls to sendmail, including recipients. This option is useful for keeping track of each mail sent (log files may grow faster though). =item C<--service=>I B: This option was deprecated. Process is dedicated to messages distribution (C), commands (C) or to automatic lists creation (C, default three of them). =back With following options F will print some information and exit. =over 4 =item C<-h>, C<--help> Print this help message. =item C<-v>, C<--version> Print the version number. =back =head1 FILES F<--CONFIG--> main configuration file. F<$PIDDIR/sympa_msg.pid> this file contains the process ID of F. =head1 SEE ALSO L, L. L, L, L, L, L. L, L. =head1 HISTORY F was originally written by: =over 4 =item Serge Aumont ComitE<233> RE<233>seau des UniversitE<233>s =item Olivier SalaE<252>n ComitE<233> RE<233>seau des UniversitE<233>s =back As of Sympa 6.2b.4, it was split into three programs: F command line utility, F daemon and F daemon. =cut sympa-6.2.24/src/sbin/bulk.pl.in0000644000175000017500000001551013216651447015326 0ustar rackeracke#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use strict; use warnings; use English qw(-no_match_vars); use Getopt::Long; use Pod::Usage; use POSIX qw(); use Sympa::Alarm; use Conf; use Sympa::Constants; use Sympa::Crash; # Show traceback. use Sympa::Log; use Sympa::Mailer; use Sympa::Process; use Sympa::Spindle::ProcessOutgoing; use Sympa::Tools::Data; my $process = Sympa::Process->instance; $process->init(pidname => 'bulk'); ## Check options ## --debug : sets the debug mode ## --foreground : prevents the script from beeing daemonized ## --mail : logs every sendmail calls my %options; unless ( GetOptions( \%main::options, 'config|f=s', 'debug|d', 'foreground|F', 'help|h', 'log_level=s', 'mail|m', 'version|v', ) ) { pod2usage(-exitval => 1, -output => \*STDERR); } if ($main::options{'help'}) { pod2usage(0); } elsif ($main::options{'version'}) { printf "Sympa %s\n", Sympa::Constants::VERSION; exit 0; } $Conf::sympa_config = $main::options{config}; if ($main::options{'debug'}) { $main::options{'log_level'} = 2 unless $main::options{'log_level'}; $main::options{'foreground'} = 1; } my $log = Sympa::Log->instance; $log->{log_to_stderr} = 'all' if $main::options{'foreground'}; # Load sympa.conf unless (Conf::load()) { die sprintf "Unable to load Sympa configuration, file %s or one of the virtual host robot.conf files contain errors. Exiting.\n", Conf::get_sympa_conf(); } $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); my $mailer = Sympa::Mailer->instance; # Enable SMTP logging if required $mailer->{log_smtp} = $main::options{'mail'} || Sympa::Tools::Data::smart_eq($Conf::Conf{'log_smtp'}, 'on'); # Setting log_level using conf unless it is set by calling option. if ($main::options{log_level}) { $log->{level} = $main::options{log_level}; $log->syslog( 'info', 'Configuration file read, log level set using options: %s', $main::options{log_level} ); } else { $log->{level} = $Conf::Conf{'log_level'}; $log->syslog( 'info', 'Configuration file read, default log level %s', $Conf::Conf{'log_level'} ); } # Put ourselves in background if not in debug mode. unless ($main::options{'foreground'}) { $process->daemonize; } $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'}); # Create and write the PID file. $process->write_pid(initial => 1); # If process is running in foreground, don't write STDERR to a dedicated file. unless ($main::options{foreground}) { $process->direct_stderr_to_file; } ## Set the User ID & Group ID for the process $GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2]; $UID = $EUID = (getpwnam(Sympa::Constants::USER))[2]; ## Required on FreeBSD to change ALL IDs(effective UID + real UID + saved UID) POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]); POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]); ## Check if the UID has correctly been set (useful on OS X) unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2]) && ($UID == (getpwnam(Sympa::Constants::USER))[2])) { die "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n"; } ## Sets the UMASK umask(oct($Conf::Conf{'umask'})); ## Change to list root unless (chdir($Conf::Conf{'home'})) { die sprintf 'Can\'t chdir to %s: %s', $Conf::Conf{'home'}, $!; } $log->syslog('notice', 'Bulk %s Started', Sympa::Constants::VERSION); my $spindle = Sympa::Spindle::ProcessOutgoing->new( log_level => $main::options{log_level}, log_smtp => $main::options{mail}, ); ## Catch signals, in order to exit cleanly, whenever possible. $SIG{'TERM'} = 'sigterm'; $SIG{'INT'} = 'sigterm'; $mailer->{redundancy} = $Conf::Conf{'bulk_max_count'} || 1; while (not $spindle->{finish}) { $spindle->spin; last if $spindle->{finish}; # Sleep for a while if bulk_mailer DB table is empty sleep $Conf::Conf{'bulk_sleep'}; } # Purge grouped notifications Sympa::Alarm->instance->flush(purge => 1); ## Free zombie sendmail process. #Sympa::Process->instance->reap_child; $log->syslog('notice', 'Bulk exited normally due to signal'); $process->remove_pid; exit(0); ## When we catch signal, just change the value of the loop ## variable. sub sigterm { my ($sig) = @_; $log->syslog('notice', 'Signal %s received, still processing current task', $sig); $spindle->{finish} = 'term'; } # Moved to Sympa::Spindle::ProcessOutgoing::_trace_smime(). #sub trace_smime; __END__ =encoding utf-8 =head1 NAME bulk, bulk.pl - Daemon for submitting messages to SMTP engine =head1 SYNOPSIS C S<[ C<--foreground> ]> S<[ C<--debug> ]> =head1 DESCRIPTION This daemon must be run along with sympa_msg.pl(8). It regularly checks the content of outgoing (bulk) spool and submit the messages it finds in it to the sendmail engine. Several daemons may be used on deferent server for huge traffic. =head1 OPTIONS =over 4 =item C<-d>, C<--debug> Sets the debug mode =item C<-f>, C<--config=>I Force bulk to use an alternative configuration file instead of F<--CONFIG-->. =item C<-F>, C<--foreground> Prevents the script from being daemonized =item C<-h>, C<--help> Prints this help message. =item C<--log_level=>I Set log level. =item C<-m>, C<--mail> Logs every sendmail calls. =back =head1 FILES F<$PIDDIR/bulk.pid> this file contains the process IDs of F. =head1 SEE ALSO L, L. L. =head1 HISTORY bulk.pl initially written by Serge Aumont appeared on Sympa 6.0. =cut sympa-6.2.24/src/lib/0000755000175000017500000000000013216651447013240 5ustar rackerackesympa-6.2.24/src/lib/Conf.pm0000644000175000017500000025630713216651447014500 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . ## This module handles the configuration file for Sympa. package Conf; use strict; use warnings; use English qw(-no_match_vars); use Storable; use Sympa; use Sympa::ConfDef; use Sympa::Constants; use Sympa::DatabaseManager; use Sympa::Language; use Sympa::LockedFile; use Sympa::Log; use Sympa::Regexps; use Sympa::Tools::Data; use Sympa::Tools::File; use Sympa::Tools::Text; my $log = Sympa::Log->instance; =encoding utf-8 #=head1 NAME # #Conf - Sympa configuration =head1 DESCRIPTION =head2 CONSTANTS AND EXPORTED VARIABLES =cut ## Database and SQL statement handlers my $sth; # parameters hash, keyed by parameter name our %params = map { $_->{name} => $_ } grep { $_->{name} } @Sympa::ConfDef::params; # valid virtual host parameters, keyed by parameter name my %valid_robot_key_words; my %db_storable_parameters; my %optional_key_words; foreach my $hash (@Sympa::ConfDef::params) { $valid_robot_key_words{$hash->{'name'}} = 1 if ($hash->{'vhost'}); $db_storable_parameters{$hash->{'name'}} = 1 if (defined($hash->{'db'}) and $hash->{'db'} ne 'none'); $optional_key_words{$hash->{'name'}} = 1 if ($hash->{'optional'}); } our $params_by_categories = _get_parameters_names_by_category(); my %old_params = ( trusted_ca_options => 'capath,cafile', 'msgcat' => '', queueexpire => '', clean_delay_queueother => '', web_recode_to => 'filesystem_encoding', # ??? - 5.2 'localedir' => '', 'ldap_export_connection_timeout' => '', # 3.3b3 - 4.1? 'ldap_export_dnmanager' => '', # ,, 'ldap_export_host' => '', # ,, 'ldap_export_name' => '', # ,, 'ldap_export_password' => '', # ,, 'ldap_export_suffix' => '', # ,, 'tri' => 'sort', # ??? - 1.3.4-1 'sort' => '', # 1.4.0 - ??? 'pidfile' => '', # ??? - 6.1.17 'pidfile_distribute' => '', # ,, 'pidfile_creation' => '', # ,, 'pidfile_bulk' => '', # ,, 'archived_pidfile' => '', # ,, 'bounced_pidfile' => '', # ,, 'task_manager_pidfile' => '', # ,, 'email_gecos' => 'gecos', # 6.2a.?? - 6.2a.33 'lock_method' => '', # 5.3b.3 - 6.2a.33 'html_editor_file' => 'html_editor_url', # 6.2a 'openssl' => '', # ?? - 6.2a.40 'distribution_mode' => '', # 5.0a.1 - 6.2a.40 'queuedistribute' => '', # ,, # These are not yet implemented 'crl_dir' => '', 'dkim_header_list' => '', ); ## These parameters now have a hard-coded value ## Customized value can be accessed though as %Ignored_Conf my %Ignored_Conf; my %hardcoded_params = (filesystem_encoding => 'utf8'); my %trusted_applications = ( 'trusted_application' => { 'occurrence' => '0-n', 'format' => { 'name' => { 'format' => '\S*', 'occurrence' => '1', 'case' => 'insensitive', }, 'ip' => { 'format' => '\d+\.\d+\.\d+\.\d+', 'occurrence' => '0-1' }, 'md5password' => { 'format' => '.*', 'occurrence' => '0-1' }, 'proxy_for_variables' => { 'format' => '.*', 'occurrence' => '0-n', 'split_char' => ',' }, 'set_variables' => { 'format' => '\S+=.*', 'occurrence' => '0-n', 'split_char' => ',', }, 'allow_commands' => { 'format' => '\S+', 'occurrence' => '0-n', 'split_char' => ',', }, } } ); my $binary_file_extension = ".bin"; our $wwsconf; our %Conf = (); =head2 FUNCTIONS =over 4 =item load ( [ CONFIG_FILE ], [ NO_DB ], [ RETURN_RESULT ] ) Loads and parses the configuration file. Reports errors if any. do not try to load database values if NO_DB is set; do not change gloval hash %Conf if RETURN_RESULT is set; ## we known that's dirty, this proc should be rewritten without this global ## var %Conf =back =cut sub load { my $config_file = shift || get_sympa_conf(); my $no_db = shift; my $return_result = shift; my $force_reload; my $config_err = 0; my %line_numbered_config; if (_source_has_not_changed($config_file) and !$return_result) { if (my $tmp_conf = _load_binary_cache( {'config_file' => $config_file . $binary_file_extension} ) ) { %Conf = %{$tmp_conf}; # Will force the robot.conf reloading, as sympa.conf is the # default. $force_reload = 1; } } else { $log->syslog('debug3', 'File %s has changed since the last cache. Loading file', $config_file); # Will force the robot.conf reloading, as sympa.conf is the default. $force_reload = 1; ## Loading the Sympa main config file. if (my $config_loading_result = _load_config_file_to_hash( {'path_to_config_file' => $config_file} ) ) { %line_numbered_config = %{$config_loading_result->{'numbered_config'}}; %Conf = %{$config_loading_result->{'config'}}; $config_err = $config_loading_result->{'errors'}; } else { return undef; } # Returning the config file content if this is what has been asked. return (\%line_numbered_config) if ($return_result); # Users may define parameters with a typo or other errors. Check that # the parameters # we found in the config file are all well defined Sympa parameters. $config_err += _detect_unknown_parameters_in_config( { 'config_hash' => \%Conf, 'config_file_line_numbering_reference' => \%line_numbered_config, } ); # Some parameter values are hardcoded. In that case, ignore what was # set in the config file and simply use the hardcoded value. %Ignored_Conf = %{_set_hardcoded_parameter_values({'config_hash' => \%Conf,})}; _set_listmasters_entry({'config_hash' => \%Conf, 'main_config' => 1}); ## Some parameters must have a value specifically defined in the ## config. If not, it is an error. $config_err += _detect_missing_mandatory_parameters( {'config_hash' => \%Conf, 'file_to_check' => $config_file}); # Some parameters need special treatments to get their final values. _infer_server_specific_parameter_values({'config_hash' => \%Conf,}); _infer_robot_parameter_values({'config_hash' => \%Conf}); if ($config_err) { $log->syslog('err', 'Errors while parsing main config file %s', $config_file); return undef; } _store_source_file_name( {'config_hash' => \%Conf, 'config_file' => $config_file}); _save_config_hash_to_binary({'config_hash' => \%Conf,}); } if (my $missing_modules_count = _check_cpan_modules_required_by_config({'config_hash' => \%Conf,})) { $log->syslog('err', 'Warning: %d required modules are missing', $missing_modules_count); } _replace_file_value_by_db_value({'config_hash' => \%Conf}) unless ($no_db); _load_server_specific_secondary_config_files({'config_hash' => \%Conf,}); _load_robot_secondary_config_files({'config_hash' => \%Conf}); ## Load robot.conf files unless ( load_robots( { 'config_hash' => \%Conf, 'no_db' => $no_db, 'force_reload' => $force_reload } ) ) { return undef; } ##_create_robot_like_config_for_main_robot(); return 1; } ## load each virtual robots configuration files sub load_robots { my $param = shift; my @robots; my $robots_list_ref = get_robots_list(); unless (defined $robots_list_ref) { $log->syslog('err', 'Robots config loading failed'); return undef; } else { @robots = @{$robots_list_ref}; } unless ($#robots > -1) { return 1; } my $exiting = 0; foreach my $robot (@robots) { my $robot_config_file = "$Conf{'etc'}/$robot/robot.conf"; my $robot_conf = undef; unless ( $robot_conf = _load_single_robot_config( { 'robot' => $robot, 'no_db' => $param->{'no_db'}, 'force_reload' => $param->{'force_reload'} } ) ) { $log->syslog( 'err', 'The config for robot %s contain errors: it could not be correctly loaded', $robot ); $exiting = 1; } else { $param->{'config_hash'}{'robots'}{$robot} = $robot_conf; } #_check_double_url_usage( # {'config_hash' => $param->{'config_hash'}{'robots'}{$robot}}); } return undef if ($exiting); return 1; } ## returns a robot conf parameter sub get_robot_conf { my ($robot, $param) = @_; if (defined $robot && $robot ne '*') { if ( defined $Conf{'robots'}{$robot} && defined $Conf{'robots'}{$robot}{$param}) { return $Conf{'robots'}{$robot}{$param}; } } ## default return $Conf{$param}; } =over 4 =item get_sympa_conf Gets path name of main config file. Path name is taken from: =over 4 =item 1 C<--config> command line option =item 2 C environment variable =item 3 built-in default =back =back =cut our $sympa_config; sub get_sympa_conf { return $sympa_config || $ENV{'SYMPA_CONFIG'} || Sympa::Constants::CONFIG; } =over 4 =item get_wwsympa_conf Gets path name of wwsympa.conf file. Path name is taken from: =over 4 =item 1 C environment variable =item 2 built-in default =back =back =cut sub get_wwsympa_conf { return $ENV{'SYMPA_WWSCONFIG'} || Sympa::Constants::WWSCONFIG; } # deletes all the *.conf.bin files. sub delete_binaries { $log->syslog('debug2', ''); my @files = (get_sympa_conf(), get_wwsympa_conf()); foreach my $robot (@{get_robots_list()}) { push @files, "$Conf{'etc'}/$robot/robot.conf"; } foreach my $c_file (@files) { my $binary_file = $c_file . ".bin"; if (-f $binary_file) { if (-w $binary_file) { unlink $binary_file; } else { $log->syslog( 'err', 'Could not remove file %s. You should remove it manually to ensure the configuration used is valid', $binary_file ); } } } } # Return a reference to an array containing the names of the robots on the # server. sub get_robots_list { $log->syslog('debug2', "Retrieving the list of robots on the server"); my @robots_list; unless (opendir DIR, $Conf{'etc'}) { $log->syslog('err', 'Unable to open directory %s for virtual robots config', $Conf{'etc'}); return undef; } foreach my $robot (readdir DIR) { my $robot_config_file = "$Conf{'etc'}/$robot/robot.conf"; next unless (-d "$Conf{'etc'}/$robot"); next unless (-f $robot_config_file); push @robots_list, $robot; } closedir(DIR); return \@robots_list; } ## Returns a hash containing the values of all the parameters of the group ## (as defined in Sympa::ConfDef) whose name is given as argument, in the ## context of the robot given as argument. sub get_parameters_group { my ($robot, $group) = @_; $log->syslog('debug3', 'Getting parameters for group "%s"', $group); my $param_hash; foreach my $param_name (keys %{$params_by_categories->{$group}}) { $param_hash->{$param_name} = get_robot_conf($robot, $param_name); } return $param_hash; } ## fetch the value from parameter $label of robot $robot from conf_table sub get_db_conf { my $robot = shift; my $label = shift; # if the value is related to a robot that is not explicitly defined, apply # it to the default robot. $robot = '*' unless (-f $Conf{'etc'} . '/' . $robot . '/robot.conf'); unless ($robot) { $robot = '*' } my $sdm = Sympa::DatabaseManager->instance; unless ( $sdm and $sth = $sdm->do_prepared_query( q{SELECT value_conf AS value FROM conf_table WHERE robot_conf = ? AND label_conf = ?}, $robot, $label ) ) { $log->syslog( 'err', 'Unable retrieve value of parameter %s for robot %s from the database', $label, $robot ); return undef; } my $value = $sth->fetchrow; $sth->finish(); return $value; } ## store the value from parameter $label of robot $robot from conf_table sub set_robot_conf { my $robot = shift; my $label = shift; my $value = shift; $log->syslog('info', 'Set config for robot %s, %s="%s"', $robot, $label, $value); # set the current config before to update database. if (-f "$Conf{'etc'}/$robot/robot.conf") { $Conf{'robots'}{$robot}{$label} = $value; } else { $Conf{$label} = $value; $robot = '*'; } my $sdm = Sympa::DatabaseManager->instance; unless ( $sdm and $sth = $sdm->do_prepared_query( q{SELECT COUNT(*) FROM conf_table WHERE robot_conf = ? AND label_conf = ?}, $robot, $label ) ) { $log->syslog( 'err', 'Unable to check presence of parameter %s for robot %s in database', $label, $robot ); return undef; } my $count = $sth->fetchrow; $sth->finish(); if ($count == 0) { unless ( $sth = $sdm->do_prepared_query( q{INSERT INTO conf_table (robot_conf, label_conf, value_conf) VALUES (?, ?, ?)}, $robot, $label, $value ) ) { $log->syslog( 'err', 'Unable add value %s for parameter %s in the robot %s DB conf', $value, $label, $robot ); return undef; } } else { unless ( $sth = $sdm->do_prepared_query( q{UPDATE conf_table SET robot_conf = ?, label_conf = ?, value_conf = ? WHERE robot_conf = ? AND label_conf = ?}, $robot, $label, $value, $robot, $label ) ) { $log->syslog( 'err', 'Unable set parameter %s value to %s in the robot %s DB conf', $label, $value, $robot ); return undef; } } } # Store configs to database sub conf_2_db { $log->syslog('debug2', '(%s)', @_); my @conf_parameters = @Sympa::ConfDef::params; # store in database robots parameters. # load only parameters that are in a robot.conf file (do not apply # defaults). my $robots_conf = load_robots(); unless (opendir DIR, $Conf{'etc'}) { $log->syslog('err', 'Unable to open directory %s for virtual robots config', $Conf{'etc'}); return undef; } foreach my $robot (readdir(DIR)) { next unless (-d "$Conf{'etc'}/$robot"); next unless (-f "$Conf{'etc'}/$robot/robot.conf"); my $config; if (my $result_of_config_loading = _load_config_file_to_hash( { 'path_to_config_file' => $Conf{'etc'} . '/' . $robot . '/robot.conf' } ) ) { $config = $result_of_config_loading->{'config'}; } _remove_unvalid_robot_entry($config); for my $i (0 .. $#conf_parameters) { if ($conf_parameters[$i]->{'name'}) { # skip separators in conf_parameters structure if (($conf_parameters[$i]->{'vhost'} eq '1') && #skip parameters that can't be define by robot so not to be loaded in db at that stage ($config->{$conf_parameters[$i]->{'name'}}) ) { Conf::set_robot_conf( $robot, $conf_parameters[$i]->{'name'}, $config->{$conf_parameters[$i]->{'name'}} ); } } } } closedir(DIR); # store in database sympa;conf and wwsympa.conf ## Load configuration file. Ignoring database config and get result my $global_conf; unless ($global_conf = Conf::load(Conf::get_sympa_conf(), 1, 'return_result')) { $log->syslog('err', 'Configuration file %s has errors', Conf::get_sympa_conf()); return undef; } for my $i (0 .. $#conf_parameters) { if (($conf_parameters[$i]->{'edit'} eq '1') && $global_conf->{$conf_parameters[$i]->{'name'}}) { Conf::set_robot_conf( "*", $conf_parameters[$i]->{'name'}, $global_conf->{$conf_parameters[$i]->{'name'}}[0] ); } } } ## Check required files and create them if required sub checkfiles_as_root { my $config_err = 0; ## Check aliases file unless (-f $Conf{'sendmail_aliases'} || ($Conf{'sendmail_aliases'} =~ /^none$/i)) { unless (open ALIASES, ">$Conf{'sendmail_aliases'}") { $log->syslog( 'err', "Failed to create aliases file %s", $Conf{'sendmail_aliases'} ); return undef; } print ALIASES "## This aliases file is dedicated to Sympa Mailing List Manager\n"; print ALIASES "## You should edit your sendmail.mc or sendmail.cf file to declare it\n"; close ALIASES; $log->syslog( 'notice', "Created missing file %s", $Conf{'sendmail_aliases'} ); unless ( Sympa::Tools::File::set_file_rights( file => $Conf{'sendmail_aliases'}, user => Sympa::Constants::USER, group => Sympa::Constants::GROUP, mode => 0644, ) ) { $log->syslog('err', 'Unable to set rights on %s', $Conf{'db_name'}); return undef; } } foreach my $robot (keys %{$Conf{'robots'}}) { # create static content directory my $dir = get_robot_conf($robot, 'static_content_path'); if ($dir ne '' && !-d $dir) { unless (mkdir($dir, 0775)) { $log->syslog('err', 'Unable to create directory %s: %m', $dir); $config_err++; } unless ( Sympa::Tools::File::set_file_rights( file => $dir, user => Sympa::Constants::USER, group => Sympa::Constants::GROUP, ) ) { $log->syslog('err', 'Unable to set rights on %s', $Conf{'db_name'}); return undef; } } } return 1; } ## Check if data structures are uptodate ## If not, no operation should be performed before the upgrade process is run sub data_structure_uptodate { my $version_file = Conf::get_robot_conf('*', 'etc') . '/data_structure.version'; my $data_structure_version; if (-f $version_file) { my $fh; unless (open $fh, '<', $version_file) { $log->syslog('err', 'Unable to open %s: %m', $version_file); return undef; } while (<$fh>) { next if /^\s*$/; next if /^\s*\#/; chomp; $data_structure_version = $_; last; } close $fh; } if (defined $data_structure_version and $data_structure_version ne Sympa::Constants::VERSION) { $log->syslog('err', "Data structure (%s) is not uptodate for current release (%s)", $data_structure_version, Sympa::Constants::VERSION); return 0; } return 1; } # Check if cookie parameter was changed. # Old name: tools::cookie_changed(). sub cookie_changed { my $current = $Conf::Conf{'cookie'}; $current = '' unless defined $current; my $changed = 1; if (-f "$Conf::Conf{'etc'}/cookies.history") { my $fh; unless (open $fh, "$Conf::Conf{'etc'}/cookies.history") { $log->syslog('err', 'Unable to read %s/cookies.history', $Conf::Conf{'etc'}); return undef; } my $oldcook = <$fh>; close $fh; ($oldcook) = reverse split /\s+/, $oldcook; $oldcook = '' unless defined $oldcook; if ($oldcook eq $current) { $log->syslog('debug2', 'Cookie is stable'); $changed = 0; } return $changed; } else { my $umask = umask 037; unless (open COOK, ">$Conf::Conf{'etc'}/cookies.history") { umask $umask; $log->syslog('err', 'Unable to create %s/cookies.history', $Conf::Conf{'etc'}); return undef; } umask $umask; chown [getpwnam(Sympa::Constants::USER)]->[2], [getgrnam(Sympa::Constants::GROUP)]->[2], "$Conf::Conf{'etc'}/cookies.history"; print COOK "$current "; close COOK; return (0); } } ## Check a few files sub checkfiles { my $config_err = 0; foreach my $p (qw(sendmail antivirus_path)) { next unless $Conf{$p}; unless (-x $Conf{$p}) { $log->syslog('err', "File %s does not exist or is not executable", $Conf{$p}); $config_err++; } } foreach my $qdir (qw(spool queuetask tmpdir)) { unless (-d $Conf{$qdir}) { $log->syslog('info', 'Creating spool %s', $Conf{$qdir}); unless (mkdir($Conf{$qdir}, 0775)) { $log->syslog('err', 'Unable to create spool %s', $Conf{$qdir}); $config_err++; } unless ( Sympa::Tools::File::set_file_rights( file => $Conf{$qdir}, user => Sympa::Constants::USER, group => Sympa::Constants::GROUP, ) ) { $log->syslog('err', 'Unable to set rights on %s', $Conf{$qdir}); $config_err++; } } } # Check if directory parameters point to the same directory. my @keys = qw(bounce_path etc home queue queueauth queuebounce queuebulk queuedigest queuemod queueoutgoing queuesubscribe queuetask queuetopic spool tmpdir viewmail_dir); push @keys, 'queueautomatic' if $Conf::Conf{'automatic_list_feature'} eq 'on'; my %dirs = (Sympa::Constants::PIDDIR() => 'PID directory'); foreach my $key (@keys) { my $val = $Conf::Conf{$key}; next unless $val; if ($dirs{$val}) { $log->syslog( 'err', 'Error in config: %s and %s parameters pointing to the same directory (%s)', $dirs{$val}, $key, $val ); $config_err++; } else { $dirs{$val} = $key; } } # Create pictures dir if useful for each robot. foreach my $robot (keys %{$Conf{'robots'}}) { my $dir = get_robot_conf($robot, 'static_content_path'); if ($dir ne '' && -d $dir) { unless (-f $dir . '/index.html') { unless (open(FF, ">$dir" . '/index.html')) { $log->syslog( 'err', 'Unable to create %s/index.html as an empty file to protect directory: %m', $dir ); } close FF; } # create picture dir if (get_robot_conf($robot, 'pictures_feature') eq 'on') { my $pictures_dir = get_robot_conf($robot, 'static_content_path') . '/pictures'; unless (-d $pictures_dir) { unless (mkdir($pictures_dir, 0775)) { $log->syslog('err', 'Unable to create directory %s', $pictures_dir); $config_err++; } chmod 0775, $pictures_dir; my $index_path = $pictures_dir . '/index.html'; unless (-f $index_path) { unless (open(FF, ">$index_path")) { $log->syslog( 'err', 'Unable to create %s as an empty file to protect directory', $index_path ); } close FF; } } } } } #update_css(); return undef if ($config_err); return 1; } ## return 1 if the parameter is a known robot ## Valid options : ## 'just_try' : prevent error logs if robot is not valid sub valid_robot { my $robot = shift; my $options = shift; ## Main host return 1 if ($robot eq $Conf{'domain'}); ## Missing etc directory unless (-d $Conf{'etc'} . '/' . $robot) { $log->syslog( 'err', 'Robot %s undefined; no %s directory', $robot, $Conf{'etc'} . '/' . $robot ) unless ($options->{'just_try'}); return undef; } ## Missing expl directory unless (-d $Conf{'home'} . '/' . $robot) { $log->syslog( 'err', 'Robot %s undefined; no %s directory', $robot, $Conf{'home'} . '/' . $robot ) unless ($options->{'just_try'}); return undef; } ## Robot not loaded unless (defined $Conf{'robots'}{$robot}) { $log->syslog('err', 'Robot %s was not loaded by this Sympa process', $robot) unless ($options->{'just_try'}); return undef; } return 1; } ## Returns the SSO record correponding to the provided sso_id ## return undef if none was found sub get_sso_by_id { my %param = @_; unless (defined $param{'service_id'} && defined $param{'robot'}) { return undef; } foreach my $sso (@{$Conf{'auth_services'}{$param{'robot'}}}) { $log->syslog('notice', 'SSO: %s', $sso->{'service_id'}); next unless ($sso->{'service_id'} eq $param{'service_id'}); return $sso; } return undef; } ########################################## ## Low level subs. Not supposed to be called from other modules. ########################################## sub _load_auth { my $robot = shift; my $is_main_robot = shift; # find appropriate auth.conf file my $config_file = _get_config_file_name({'robot' => $robot, 'file' => "auth.conf"}); $log->syslog('debug', '(%s)', $config_file); $robot ||= $Conf{'domain'}; my $line_num = 0; my $config_err = 0; my @paragraphs; my %result; my $current_paragraph; my %valid_keywords = ( 'ldap' => { 'regexp' => '.*', 'negative_regexp' => '.*', 'host' => '[\w\.\-]+(:\d+)?(\s*,\s*[\w\.\-]+(:\d+)?)*', 'timeout' => '\d+', 'suffix' => '.+', 'bind_dn' => '.+', 'bind_password' => '.+', 'get_dn_by_uid_filter' => '.+', 'get_dn_by_email_filter' => '.+', 'email_attribute' => Sympa::Regexps::ldap_attrdesc(), 'alternative_email_attribute' => Sympa::Regexps::ldap_attrdesc() . '(\s*,\s*' . Sympa::Regexps::ldap_attrdesc() . ')*', 'scope' => 'base|one|sub', 'authentication_info_url' => 'http(s)?:/.*', 'use_tls' => 'starttls|ldaps|none', 'use_ssl' => '1', # Obsoleted 'use_start_tls' => '1', # Obsoleted 'ssl_version' => 'sslv2/3|sslv2|sslv3|tlsv1|tlsv1_1|tlsv1_2', 'ssl_ciphers' => '[\w:]+', 'ssl_cert' => '.+', 'ssl_key' => '.+', 'ca_verify' => '\w+', 'ca_path' => '.+', 'ca_file' => '.+', }, 'user_table' => { 'regexp' => '.*', 'negative_regexp' => '.*' }, 'cas' => { 'base_url' => 'http(s)?:/.*', 'non_blocking_redirection' => 'on|off', 'login_path' => '.*', 'logout_path' => '.*', 'service_validate_path' => '.*', 'proxy_path' => '.*', 'proxy_validate_path' => '.*', 'auth_service_name' => '[\w\-\.]+', 'auth_service_friendly_name' => '.*', 'authentication_info_url' => 'http(s)?:/.*', 'host' => '[\w\.\-]+(:\d+)?(\s*,\s*[\w\.\-]+(:\d+)?)*', 'bind_dn' => '.+', 'bind_password' => '.+', 'timeout' => '\d+', 'suffix' => '.+', 'scope' => 'base|one|sub', 'get_email_by_uid_filter' => '.+', 'email_attribute' => Sympa::Regexps::ldap_attrdesc(), 'use_tls' => 'starttls|ldaps|none', 'use_ssl' => '1', # Obsoleted 'use_start_tls' => '1', # Obsoleted 'ssl_version' => 'sslv2/3|sslv2|sslv3|tlsv1|tlsv1_1|tlsv1_2', 'ssl_ciphers' => '[\w:]+', 'ssl_cert' => '.+', 'ssl_key' => '.+', 'ca_verify' => '\w+', 'ca_path' => '.+', 'ca_file' => '.+', }, 'generic_sso' => { 'service_name' => '.+', 'service_id' => '\S+', 'http_header_prefix' => '\w+', 'http_header_list' => '[\w\.\-\,]+', 'email_http_header' => '\w+', 'http_header_value_separator' => '.+', 'logout_url' => '.+', 'host' => '[\w\.\-]+(:\d+)?(\s*,\s*[\w\.\-]+(:\d+)?)*', 'bind_dn' => '.+', 'bind_password' => '.+', 'timeout' => '\d+', 'suffix' => '.+', 'scope' => 'base|one|sub', 'get_email_by_uid_filter' => '.+', 'email_attribute' => Sympa::Regexps::ldap_attrdesc(), 'use_tls' => 'starttls|ldaps|none', 'use_ssl' => '1', # Obsoleted 'use_start_tls' => '1', # Obsoleted 'ssl_version' => 'sslv2/3|sslv2|sslv3|tlsv1|tlsv1_1|tlsv1_2', 'ssl_ciphers' => '[\w:]+', 'ssl_cert' => '.+', 'ssl_key' => '.+', 'ca_verify' => '\w+', 'ca_path' => '.+', 'ca_file' => '.+', 'force_email_verify' => '1', 'internal_email_by_netid' => '1', 'netid_http_header' => '[\w\-\.]+', }, 'authentication_info_url' => 'http(s)?:/.*' ); ## Open the configuration file or return and read the lines. unless (open(IN, $config_file)) { $log->syslog('notice', 'Unable to open %s: %m', $config_file); return undef; } $Conf{'cas_number'}{$robot} = 0; $Conf{'generic_sso_number'}{$robot} = 0; $Conf{'ldap_number'}{$robot} = 0; $Conf{'use_passwd'}{$robot} = 0; ## Parsing auth.conf while () { $line_num++; next if (/^\s*[\#\;]/o); if (/^\s*authentication_info_url\s+(.*\S)\s*$/o) { $Conf{'authentication_info_url'}{$robot} = $1; next; } elsif (/^\s*(ldap|cas|user_table|generic_sso)\s*$/io) { $current_paragraph->{'auth_type'} = lc($1); } elsif (/^\s*(\S+)\s+(.*\S)\s*$/o) { my ($keyword, $value) = ($1, $2); # Workaround: Some parameters required by cas and generic_sso auth # types may be prefixed by "ldap_", but LDAP database driver # requires those not prefixed. $keyword =~ s/\Aldap_//; unless ( defined $valid_keywords{$current_paragraph->{'auth_type'}} {$keyword}) { $log->syslog('err', 'Unknown keyword "%s" in %s line %d', $keyword, $config_file, $line_num); next; } unless ($value =~ /^$valid_keywords{$current_paragraph->{'auth_type'}}{$keyword}$/ ) { $log->syslog('err', 'Unknown format "%s" for keyword "%s" in %s line %d', $value, $keyword, $config_file, $line_num); next; } ## Allow white spaces between hosts if ($keyword =~ /host$/) { $value =~ s/\s//g; } $current_paragraph->{$keyword} = $value; } ## process current paragraph if (/^\s+$/o || eof(IN)) { if (defined($current_paragraph)) { # Parameters obsoleted as of 6.2.15. if ($current_paragraph->{use_start_tls}) { $current_paragraph->{use_tls} = 'starttls'; } elsif ($current_paragraph->{use_ssl}) { $current_paragraph->{use_tls} = 'ldaps'; } delete $current_paragraph->{use_start_tls}; delete $current_paragraph->{use_ssl}; if ($current_paragraph->{'auth_type'} eq 'cas') { unless (defined $current_paragraph->{'base_url'}) { $log->syslog('err', 'Incorrect CAS paragraph in auth.conf'); next; } $Conf{'cas_number'}{$robot}++; eval "require AuthCAS"; if ($EVAL_ERROR) { $log->syslog('err', 'Failed to load AuthCAS perl module'); return undef; } my $cas_param = {casUrl => $current_paragraph->{'base_url'}}; ## Optional parameters ## We should also cope with X509 CAs $cas_param->{'loginPath'} = $current_paragraph->{'login_path'} if (defined $current_paragraph->{'login_path'}); $cas_param->{'logoutPath'} = $current_paragraph->{'logout_path'} if (defined $current_paragraph->{'logout_path'}); $cas_param->{'serviceValidatePath'} = $current_paragraph->{'service_validate_path'} if ( defined $current_paragraph->{'service_validate_path'} ); $cas_param->{'proxyPath'} = $current_paragraph->{'proxy_path'} if (defined $current_paragraph->{'proxy_path'}); $cas_param->{'proxyValidatePath'} = $current_paragraph->{'proxy_validate_path'} if ( defined $current_paragraph->{'proxy_validate_path'}); $current_paragraph->{'cas_server'} = AuthCAS->new(%{$cas_param}); unless (defined $current_paragraph->{'cas_server'}) { $log->syslog( 'err', 'Failed to create CAS object for %s: %s', $current_paragraph->{'base_url'}, AuthCAS::get_errors() ); next; } $Conf{'cas_id'}{$robot} {$current_paragraph->{'auth_service_name'}}{'casnum'} = scalar @paragraphs; ## Default value for auth_service_friendly_name IS ## auth_service_name $Conf{'cas_id'}{$robot} {$current_paragraph->{'auth_service_name'}} {'auth_service_friendly_name'} = $current_paragraph->{'auth_service_friendly_name'} || $current_paragraph->{'auth_service_name'}; ## Force the default scope because '' is interpreted as ## 'base' $current_paragraph->{'scope'} ||= 'sub'; } elsif ($current_paragraph->{'auth_type'} eq 'generic_sso') { $Conf{'generic_sso_number'}{$robot}++; $Conf{'generic_sso_id'}{$robot} {$current_paragraph->{'service_id'}} = $#paragraphs + 1; ## Force the default scope because '' is interpreted as ## 'base' $current_paragraph->{'scope'} ||= 'sub'; ## default value for http_header_value_separator is ';' $current_paragraph->{'http_header_value_separator'} ||= ';'; ## CGI.pm changes environment variable names ('-' => '_') ## declared environment variable names needs to be ## transformed accordingly foreach my $parameter ('http_header_list', 'email_http_header', 'netid_http_header') { $current_paragraph->{$parameter} =~ s/\-/\_/g if (defined $current_paragraph->{$parameter}); } } elsif ($current_paragraph->{'auth_type'} eq 'ldap') { $Conf{'ldap'}{$robot}++; $Conf{'use_passwd'}{$robot} = 1; ## Force the default scope because '' is interpreted as ## 'base' $current_paragraph->{'scope'} ||= 'sub'; } elsif ($current_paragraph->{'auth_type'} eq 'user_table') { $Conf{'use_passwd'}{$robot} = 1; } # setting default $current_paragraph->{'regexp'} = '.*' unless (defined($current_paragraph->{'regexp'})); $current_paragraph->{'non_blocking_redirection'} = 'on' unless ( defined($current_paragraph->{'non_blocking_redirection'}) ); push(@paragraphs, $current_paragraph); undef $current_paragraph; } next; } } close(IN); return \@paragraphs; } ## load charset.conf file (charset mapping for service messages) sub load_charset { my $charset = {}; my $config_file = Sympa::search_fullpath('*', 'charset.conf'); return {} unless $config_file; unless (open CONFIG, $config_file) { $log->syslog('err', 'Unable to read configuration file %s: %m', $config_file); return {}; } while () { chomp $_; s/\s*#.*//; s/^\s+//; next unless /\S/; my ($lang, $cset) = split(/\s+/, $_); unless ($cset) { $log->syslog('err', 'Charset name is missing in configuration file %s line %d', $config_file, $NR); next; } # canonicalize lang if possible. $lang = Sympa::Language::canonic_lang($lang) || $lang; $charset->{$lang} = $cset; } close CONFIG; return $charset; } =over =item lang2charset ( $lang ) Gets charset for e-mail messages sent by Sympa. Parameters: $lang - language. Returns: Charset name. If it is not known, returns default charset. =back =cut # Old name: tools::lang2charset(). # FIXME: This would be moved to such as Site package. sub lang2charset { my $lang = shift; my $locale2charset; if ($lang and %Conf::Conf # configuration loaded and $locale2charset = $Conf::Conf{'locale2charset'} ) { foreach my $l (Sympa::Language::implicated_langs($lang)) { if (exists $locale2charset->{$l}) { return $locale2charset->{$l}; } } } return 'utf-8'; # the last resort } ## load nrcpt file (limite receipient par domain sub load_nrcpt_by_domain { my $config_file = Sympa::search_fullpath('*', 'nrcpt_by_domain.conf'); return unless $config_file; my $line_num = 0; my $config_err = 0; my $nrcpt_by_domain = {}; my $valid_dom = 0; ## Open the configuration file or return and read the lines. unless (open IN, '<', $config_file) { $log->syslog('err', 'Unable to open %s: %m', $config_file); return; } while () { $line_num++; next if (/^\s*$/o || /^[\#\;]/o); if (/^(\S+)\s+(\d+)$/io) { my ($domain, $value) = ($1, $2); chomp $domain; chomp $value; $nrcpt_by_domain->{$domain} = $value; $valid_dom += 1; } else { $log->syslog('notice', 'Error at configuration file %s line %d: %s', $config_file, $line_num, $_); $config_err++; } } close IN; return $nrcpt_by_domain; } ## load .sql named filter conf file sub load_sql_filter { my $file = shift; my %sql_named_filter_params = ( 'sql_named_filter_query' => { 'occurrence' => '1', 'format' => { 'db_type' => {'format' => 'mysql|SQLite|Pg|Oracle|Sybase',}, 'db_name' => {'format' => '.*', 'occurrence' => '1',}, 'db_host' => {'format' => '.*', 'occurrence' => '1',}, 'statement' => {'format' => '.*', 'occurrence' => '1',}, 'db_user' => {'format' => '.*', 'occurrence' => '0-1',}, 'db_passwd' => {'format' => '.*', 'occurrence' => '0-1',}, 'db_options' => {'format' => '.*', 'occurrence' => '0-1',}, 'db_env' => {'format' => '.*', 'occurrence' => '0-1',}, 'db_port' => {'format' => '\d+', 'occurrence' => '0-1',}, 'db_timeout' => {'format' => '\d+', 'occurrence' => '0-1',}, } } ); return undef unless (-r $file); return ( load_generic_conf_file($file, \%sql_named_filter_params, 'abort')); } ## load automatic_list_description.conf configuration file sub load_automatic_lists_description { my $robot = shift; my $family = shift; $log->syslog('debug2', 'Starting: Robot %s family %s', $robot, $family); my %automatic_lists_params = ( 'class' => { 'occurrence' => '1-n', 'format' => { 'name' => {'format' => '.*', 'occurrence' => '1',}, 'stamp' => {'format' => '.*', 'occurrence' => '1',}, 'description' => {'format' => '.*', 'occurrence' => '1',}, 'order' => {'format' => '\d+', 'occurrence' => '1',}, 'instances' => {'occurrence' => '1', 'format' => '.*',}, #'format' => { #'instance' => { #'occurrence' => '1-n', #'format' => { #'value' => {'format' => '.*', 'occurrence' => '1', }, #'tag' => {'format' => '.*', 'occurrence' => '1', }, #'order' => {'format' => '\d+', 'occurrence' => '1', }, #}, #}, #}, }, }, ); # find appropriate automatic_lists_description.conf file my $config = Sympa::search_fullpath($robot, 'automatic_lists_description.conf', subdir => ('families/' . $family)); return undef unless $config; my $description = load_generic_conf_file($config, \%automatic_lists_params); ## Now doing some structuration work because ## Conf::load_automatic_lists_description() can't handle ## data structured beyond one level of hash. This needs to be changed. my @structured_data; foreach my $class (@{$description->{'class'}}) { my @structured_instances; my @instances = split '%%%', $class->{'instances'}; my $default_found = 0; foreach my $instance (@instances) { my $structured_instance; my @instance_params = split '---', $instance; foreach my $instance_param (@instance_params) { $instance_param =~ /^\s*(\S+)\s+(.*)\s*$/; my $key = $1; my $value = $2; $key =~ s/^\s*//; $key =~ s/\s*$//; $value =~ s/^\s*//; $value =~ s/\s*$//; $structured_instance->{$key} = $value; } $structured_instances[$structured_instance->{'order'}] = $structured_instance; if (defined $structured_instance->{'default'}) { $default_found = 1; } } unless ($default_found) { $structured_instances[0]->{'default'} = 1; } $class->{'instances'} = \@structured_instances; $structured_data[$class->{'order'}] = $class; } $description->{'class'} = \@structured_data; return $description; } ## load trusted_application.conf configuration file sub load_trusted_application { my $robot = shift; # find appropriate trusted-application.conf file my $config_file = _get_config_file_name( {'robot' => $robot, 'file' => "trusted_applications.conf"}); return undef unless (-r $config_file); return undef unless (-r $config_file); # open TMP, ">/tmp/dump1"; # Sympa::Tools::Data::dump_var(load_generic_conf_file($config_file, # \%trusted_applications);, 0,\*TMP); # close TMP; return (load_generic_conf_file($config_file, \%trusted_applications)); } ## load trusted_application.conf configuration file sub load_crawlers_detection { my $robot = shift; my %crawlers_detection_conf = ( 'user_agent_string' => { 'occurrence' => '0-n', 'format' => '.+' } ); my $config_file = _get_config_file_name( {'robot' => $robot, 'file' => "crawlers_detection.conf"}); return undef unless (-r $config_file); my $hashtab = load_generic_conf_file($config_file, \%crawlers_detection_conf); my $hashhash; foreach my $kword (keys %{$hashtab}) { # ignore comments and default next unless ($crawlers_detection_conf{$kword}); foreach my $value (@{$hashtab->{$kword}}) { $hashhash->{$kword}{$value} = 'true'; } } return $hashhash; } ############################################################ # load_generic_conf_file ############################################################ # load a generic config organized by paragraph syntax # # IN : -$config_file (+): full path of config file # -$structure_ref (+): ref(HASH) describing expected syntax # -$on_error: optional. sub returns undef if set to 'abort' # and an error is found in conf file # OUT : ref(HASH) of parsed parameters # | undef # ############################################################## sub load_generic_conf_file { my $config_file = shift; my $structure_ref = shift; my $on_error = shift; my %structure = %$structure_ref; my %admin; my (@paragraphs); ## Just in case... local $RS = "\n"; ## Set defaults to 1 foreach my $pname (keys %structure) { $admin{'defaults'}{$pname} = 1 unless ($structure{$pname}{'internal'}); } ## Split in paragraphs my $i = 0; unless (open(CONFIG, $config_file)) { $log->syslog('err', 'Unable to read configuration file %s', $config_file); return undef; } while () { if (/^\s*$/) { $i++ if $paragraphs[$i]; } else { push @{$paragraphs[$i]}, $_; } } ## Parse each paragraph for my $index (0 .. $#paragraphs) { my @paragraph = @{$paragraphs[$index]}; my $pname; ## Clean paragraph, keep comments for my $i (0 .. $#paragraph) { my $changed = undef; for my $j (0 .. $#paragraph) { if ($paragraph[$j] =~ /^\s*\#/) { chomp($paragraph[$j]); push @{$admin{'comment'}}, $paragraph[$j]; splice @paragraph, $j, 1; $changed = 1; } elsif ($paragraph[$j] =~ /^\s*$/) { splice @paragraph, $j, 1; $changed = 1; } last if $changed; } last unless $changed; } ## Empty paragraph next unless ($#paragraph > -1); ## Look for first valid line unless ($paragraph[0] =~ /^\s*([\w-]+)(\s+.*)?$/) { $log->syslog('notice', 'Bad paragraph "%s" in %s, ignored', $paragraph[0], $config_file); return undef if $on_error eq 'abort'; next; } $pname = $1; unless (defined $structure{$pname}) { $log->syslog('notice', 'Unknown parameter "%s" in %s, ignored', $pname, $config_file); return undef if $on_error eq 'abort'; next; } ## Uniqueness if (defined $admin{$pname}) { unless (($structure{$pname}{'occurrence'} eq '0-n') or ($structure{$pname}{'occurrence'} eq '1-n')) { $log->syslog('err', 'Multiple parameter "%s" in %s', $pname, $config_file); return undef if $on_error eq 'abort'; } } ## Line or Paragraph if (ref $structure{$pname}{'format'} eq 'HASH') { ## This should be a paragraph unless ($#paragraph > 0) { $log->syslog( 'notice', 'Expecting a paragraph for "%s" parameter in %s, ignore it', $pname, $config_file ); return undef if $on_error eq 'abort'; next; } ## Skipping first line shift @paragraph; my %hash; for my $i (0 .. $#paragraph) { next if ($paragraph[$i] =~ /^\s*\#/); unless ($paragraph[$i] =~ /^\s*(\w+)\s*/) { $log->syslog('notice', 'Bad line "%s" in %s', $paragraph[$i], $config_file); return undef if $on_error eq 'abort'; } my $key = $1; unless (defined $structure{$pname}{'format'}{$key}) { $log->syslog('notice', 'Unknown key "%s" in paragraph "%s" in %s', $key, $pname, $config_file); return undef if $on_error eq 'abort'; next; } unless ($paragraph[$i] =~ /^\s*$key\s+($structure{$pname}{'format'}{$key}{'format'})\s*$/i ) { $log->syslog('notice', 'Bad entry "%s" in paragraph "%s" in %s', $paragraph[$i], $key, $pname, $config_file); return undef if $on_error eq 'abort'; next; } $hash{$key} = _load_a_param($key, $1, $structure{$pname}{'format'}{$key}); } ## Apply defaults & Check required keys my $missing_required_field; foreach my $k (keys %{$structure{$pname}{'format'}}) { ## Default value unless (defined $hash{$k}) { if (defined $structure{$pname}{'format'}{$k}{'default'}) { $hash{$k} = _load_a_param($k, 'default', $structure{$pname}{'format'}{$k}); } } ## Required fields if ($structure{$pname}{'format'}{$k}{'occurrence'} eq '1') { unless (defined $hash{$k}) { $log->syslog('notice', 'Missing key %s in param %s in %s', $k, $pname, $config_file); return undef if $on_error eq 'abort'; $missing_required_field++; } } } next if $missing_required_field; delete $admin{'defaults'}{$pname}; ## Should we store it in an array if (($structure{$pname}{'occurrence'} =~ /n$/)) { push @{$admin{$pname}}, \%hash; } else { $admin{$pname} = \%hash; } } else { ## This should be a single line my $xxxmachin = $structure{$pname}{'format'}; unless ($#paragraph == 0) { $log->syslog('err', 'Expecting a single line for %s parameter in %s %s', $pname, $config_file, $xxxmachin); return undef if $on_error eq 'abort'; } unless ($paragraph[0] =~ /^\s*$pname\s+($structure{$pname}{'format'})\s*$/i) { $log->syslog('err', 'Bad entry "%s" in %s', $paragraph[0], $config_file); return undef if $on_error eq 'abort'; next; } my $value = _load_a_param($pname, $1, $structure{$pname}); delete $admin{'defaults'}{$pname}; if (($structure{$pname}{'occurrence'} =~ /n$/) && !(ref($value) =~ /^ARRAY/)) { push @{$admin{$pname}}, $value; } else { $admin{$pname} = $value; } } } close CONFIG; return \%admin; } ### load_a_param # sub _load_a_param { my ($key, $value, $p) = @_; ## Empty value if ($value =~ /^\s*$/) { return undef; } ## Default if ($value eq 'default') { $value = $p->{'default'}; } # Lower case if useful. $value = lc($value) if (defined $p->{'case'} && $p->{'case'} eq 'insensitive'); ## Do we need to split param if it is not already an array if ( ($p->{'occurrence'} =~ /n$/) && $p->{'split_char'} && !(ref($value) eq 'ARRAY')) { my @array = split /$p->{'split_char'}/, $value; foreach my $v (@array) { $v =~ s/^\s*(.+)\s*$/$1/g; } return \@array; } else { return $value; } } ## Simply load a config file and returns a hash. ## the returned hash contains two keys: ## 1- the key 'config' points to a hash containing the data found in the ## config file. ## 2- the key 'numbered_config' points to a hash containing the data found in ## the config file. Each entry contains both the value of a parameter and the ## line where it was found in the config file. ## 3- the key 'errors' contains the number of config entries that could not be ## loaded, due to an error. ## Returns undef if something went wrong while attempting to read the file. sub _load_config_file_to_hash { my $param = shift; my $line_num = 0; ## Open the configuration file or return and read the lines. unless (open(IN, $param->{'path_to_config_file'})) { $log->syslog( 'notice', 'Unable to open %s: %m', $param->{'path_to_config_file'} ); return undef; } # Initialize result. my $result = { errors => 0, config => {}, numbered_config => {}, }; while () { $line_num++; # skip empty or commented lines next if (/^\s*$/ || /^[\#;]/); # match "keyword value" pattern if (/^(\S+)\s+(.+)$/) { my ($keyword, $value) = ($1, $2); $value =~ s/\s*$//; ## 'tri' is a synonym for 'sort' ## (for compatibility with older versions) $keyword = 'sort' if ($keyword eq 'tri'); ## 'key_password' is a synonym for 'key_passwd' ## (for compatibilyty with older versions) $keyword = 'key_passwd' if ($keyword eq 'key_password'); ## Special case: `command` if ($value =~ /^\`(.*)\`$/) { $value = qx/$1/; chomp($value); } if ( exists $params{$keyword} && defined $params{$keyword}{'multiple'} && $params{$keyword}{'multiple'} == 1) { if (defined $result->{'config'}{$keyword}) { push @{$result->{'config'}{$keyword}}, $value; push @{$result->{'numbered_config'}{$keyword}}, [$value, $line_num]; } else { $result->{'config'}{$keyword} = [$value]; $result->{'numbered_config'}{$keyword} = [[$value, $line_num]]; } } else { $result->{'config'}{$keyword} = $value; $result->{'numbered_config'}{$keyword} = [$value, $line_num]; } } else { $log->syslog('err', 'Error at line %d: %s', $line_num, $param->{'path_to_config_file'}, $_); $result->{'errors'}++; } } close(IN); return $result; } ## Checks a hash containing a sympa config and removes any entry that ## is not supposed to be defined at the robot level. sub _remove_unvalid_robot_entry { my $param = shift; my $config_hash = $param->{'config_hash'}; foreach my $keyword (keys %$config_hash) { unless ($valid_robot_key_words{$keyword}) { $log->syslog('err', 'Removing unknown robot keyword %s', $keyword) unless ($param->{'quiet'}); delete $config_hash->{$keyword}; } } return 1; } sub _detect_unknown_parameters_in_config { my $param = shift; my $number_of_unknown_parameters_found = 0; foreach my $parameter (sort keys %{$param->{'config_hash'}}) { next if (exists $params{$parameter}); if (defined $old_params{$parameter}) { if ($old_params{$parameter}) { $log->syslog( 'err', 'Line %d of sympa.conf, parameter %s is no more available, read documentation for new parameter(s) %s', $param->{'config_file_line_numbering_reference'} {$parameter}[1], $parameter, $old_params{$parameter} ); } else { $log->syslog( 'err', 'Line %d of sympa.conf, parameter %s is now obsolete', $param->{'config_file_line_numbering_reference'} {$parameter}[1], $parameter ); next; } } else { $log->syslog( 'err', 'Line %d, unknown field: %s in sympa.conf', $param->{'config_file_line_numbering_reference'}{$parameter} [1], $parameter ); } $number_of_unknown_parameters_found++; } return $number_of_unknown_parameters_found; } sub _infer_server_specific_parameter_values { my $param = shift; $param->{'config_hash'}{'robot_name'} = ''; unless ( Sympa::Tools::Data::smart_eq( $param->{'config_hash'}{'dkim_feature'}, 'on' ) ) { # dkim_signature_apply_ on nothing if dkim_feature is off # Sets empty array. $param->{'config_hash'}{'dkim_signature_apply_on'} = ['']; } else { $param->{'config_hash'}{'dkim_signature_apply_on'} =~ s/\s//g; my @dkim = split(/,/, $param->{'config_hash'}{'dkim_signature_apply_on'}); $param->{'config_hash'}{'dkim_signature_apply_on'} = \@dkim; } unless ($param->{'config_hash'}{'dkim_signer_domain'}) { $param->{'config_hash'}{'dkim_signer_domain'} = $param->{'config_hash'}{'domain'}; } my @dmarc = split /[,\s]+/, ($param->{'config_hash'}{'dmarc_protection_mode'} || ''); if (@dmarc) { $param->{'config_hash'}{'dmarc_protection_mode'} = \@dmarc; } else { delete $param->{'config_hash'}{'dmarc_protection_mode'}; } ## Set Regexp for accepted list suffixes if (defined($param->{'config_hash'}{'list_check_suffixes'})) { $param->{'config_hash'}{'list_check_regexp'} = $param->{'config_hash'}{'list_check_suffixes'}; $param->{'config_hash'}{'list_check_regexp'} =~ s/[,\s]+/\|/g; } # my $p = 1; # foreach (split(/,/, $param->{'config_hash'}{'sort'})) { # $param->{'config_hash'}{'poids'}{$_} = $p++; # } # $param->{'config_hash'}{'poids'}{'*'} = $p # if !$param->{'config_hash'}{'poids'}{'*'}; ## Parameters made of comma-separated list foreach my $parameter ( 'rfc2369_header_fields', 'anonymous_header_fields', 'remove_headers', 'remove_outgoing_headers' ) { if ($param->{'config_hash'}{$parameter} eq 'none') { delete $param->{'config_hash'}{$parameter}; } else { $param->{'config_hash'}{$parameter} = [split(/,/, $param->{'config_hash'}{$parameter})]; } } foreach my $action (split(/,/, $param->{'config_hash'}{'use_blacklist'})) { $param->{'config_hash'}{'blacklist'}{$action} = 1; } foreach my $log_module ( split(/,/, $param->{'config_hash'}{'log_module'} || '')) { $param->{'config_hash'}{'loging_for_module'}{$log_module} = 1; } foreach my $log_condition ( split(/,/, $param->{'config_hash'}{'log_condition'} || '')) { chomp $log_condition; if ($log_condition =~ /^\s*(ip|email)\s*\=\s*(.*)\s*$/i) { $param->{'config_hash'}{'loging_condition'}{$1} = $2; } else { $log->syslog('err', 'Unrecognized log_condition token %s; ignored', $log_condition); } } if ($param->{'config_hash'}{'ldap_export_name'}) { $param->{'config_hash'}{'ldap_export'} = { $param->{'config_hash'}{'ldap_export_name'} => { 'host' => $param->{'config_hash'}{'ldap_export_host'}, 'suffix' => $param->{'config_hash'}{'ldap_export_suffix'}, 'password' => $param->{'config_hash'}{'ldap_export_password'}, 'DnManager' => $param->{'config_hash'}{'ldap_export_dnmanager'}, 'connection_timeout' => $param->{'config_hash'}{'ldap_export_connection_timeout'} } }; } return 1; } sub _load_server_specific_secondary_config_files { my $param = shift; ## wwsympa.conf exists if (-f get_wwsympa_conf()) { $log->syslog( 'notice', '%s was found but it is no longer loaded. Please run sympa.pl --upgrade to migrate it', get_wwsympa_conf() ); } # canonicalize language, or if failed, apply site-wide default. $param->{'config_hash'}{'lang'} = Sympa::Language::canonic_lang($param->{'config_hash'}{'lang'}) || 'en-US'; ## Load charset.conf file if necessary. if ($param->{'config_hash'}{'legacy_character_support_feature'} eq 'on') { $param->{'config_hash'}{'locale2charset'} = load_charset(); } else { $param->{'config_hash'}{'locale2charset'} = {}; } ## Load nrcpt_by_domain.conf $param->{'config_hash'}{'nrcpt_by_domain'} = load_nrcpt_by_domain(); $param->{'config_hash'}{'crawlers_detection'} = load_crawlers_detection($param->{'config_hash'}{'robot_name'}); } sub _infer_robot_parameter_values { my $param = shift; # 'host' and 'domain' are mandatory and synonym.$Conf{'host'} is # still widely used even if the doc requires domain. $param->{'config_hash'}{'host'} = $param->{'config_hash'}{'domain'} if (defined $param->{'config_hash'}{'domain'}); $param->{'config_hash'}{'domain'} = $param->{'config_hash'}{'host'} if (defined $param->{'config_hash'}{'host'}); $param->{'config_hash'}{'wwsympa_url'} ||= "http://$param->{'config_hash'}{'host'}/sympa"; $param->{'config_hash'}{'static_content_url'} ||= $Conf{'static_content_url'}; $param->{'config_hash'}{'static_content_path'} ||= $Conf{'static_content_path'}; ## CSS my $final_separator = ''; $final_separator = '/' if ($param->{'config_hash'}{'robot_name'}); $param->{'config_hash'}{'css_url'} ||= $param->{'config_hash'}{'static_content_url'} . '/css' . $final_separator . $param->{'config_hash'}{'robot_name'}; $param->{'config_hash'}{'css_path'} ||= $param->{'config_hash'}{'static_content_path'} . '/css' . $final_separator . $param->{'config_hash'}{'robot_name'}; unless ($param->{'config_hash'}{'email'}) { $param->{'config_hash'}{'email'} = $Conf{'email'}; } # Obsoleted. Use get_address(). $param->{'config_hash'}{'sympa'} = $param->{'config_hash'}{'email'} . '@' . $param->{'config_hash'}{'host'}; # Obsoleted. Use get_address('owner'). $param->{'config_hash'}{'request'} = $param->{'config_hash'}{'email'} . '-request@' . $param->{'config_hash'}{'host'}; # split action list for blacklist usage foreach my $action (split(/,/, $Conf{'use_blacklist'})) { $param->{'config_hash'}{'blacklist'}{$action} = 1; } # Hack because multi valued parameters are not available for Sympa 6.1. if (defined $param->{'config_hash'}{'automatic_list_families'}) { my @families = split ';', $param->{'config_hash'}{'automatic_list_families'}; my %families_description; foreach my $family_description (@families) { my %family; my @family_parameters = split ':', $family_description; foreach my $family_parameter (@family_parameters) { my @parameter = split '=', $family_parameter; $family{$parameter[0]} = $parameter[1]; } $family{'escaped_prefix_separator'} = $family{'prefix_separator'}; $family{'escaped_prefix_separator'} =~ s/([+*?.])/\\$1/g; $family{'escaped_classes_separator'} = $family{'classes_separator'}; $family{'escaped_classes_separator'} =~ s/([+*?.])/\\$1/g; $families_description{$family{'name'}} = \%family; } $param->{'config_hash'}{'automatic_list_families'} = \%families_description; } # canonicalize language $param->{'config_hash'}{'lang'} = Sympa::Language::canonic_lang($param->{'config_hash'}{'lang'}) or delete $param->{'config_hash'}{'lang'}; _parse_custom_robot_parameters( {'config_hash' => $param->{'config_hash'}}); } sub _load_robot_secondary_config_files { my $param = shift; my $trusted_applications = load_trusted_application($param->{'config_hash'}{'robot_name'}); $param->{'config_hash'}{'trusted_applications'} = undef; if (defined $trusted_applications) { $param->{'config_hash'}{'trusted_applications'} = $trusted_applications->{'trusted_application'}; } my $robot_name_for_auth_storing = $param->{'config_hash'}{'robot_name'} || $Conf{'domain'}; my $is_main_robot = 0; $is_main_robot = 1 unless ($param->{'config_hash'}{'robot_name'}); $Conf{'auth_services'}{$robot_name_for_auth_storing} = _load_auth($param->{'config_hash'}{'robot_name'}, $is_main_robot); if (defined $param->{'config_hash'}{'automatic_list_families'}) { foreach my $family ( keys %{$param->{'config_hash'}{'automatic_list_families'}}) { $param->{'config_hash'}{'automatic_list_families'}{$family} {'description'} = load_automatic_lists_description( $param->{'config_hash'}{'robot_name'}, $param->{'config_hash'}{'automatic_list_families'}{$family} {'name'} ); } } return 1; } ## For parameters whose value is hard_coded, as per %hardcoded_params, set the ## parameter value to the hardcoded value, whatever is defined in the config. ## Returns a ref to a hash containing the ignored values. sub _set_hardcoded_parameter_values { my $param = shift; my %ignored_values; ## Some parameter values are hardcoded. In that case, ignore what was set ## in the config file and simply use the hardcoded value. foreach my $p (keys %hardcoded_params) { $ignored_values{$p} = $param->{'config_hash'}{$p} if (defined $param->{'config_hash'}{$p}); $param->{'config_hash'}{$p} = $hardcoded_params{$p}; } return \%ignored_values; } sub _detect_missing_mandatory_parameters { my $param = shift; my $number_of_errors = 0; $param->{'file_to_check'} =~ /^(\/.*\/)?([^\/]+)$/; my $config_file_name = $2; foreach my $parameter (keys %params) { ## next if (defined $params{$parameter}->{'file'} && $params{$parameter}->{'file'} ne $config_file_name); unless (defined $param->{'config_hash'}{$parameter} or defined $params{$parameter}->{'default'} or defined $params{$parameter}->{'optional'}) { $log->syslog('err', 'Required field not found in sympa.conf: %s', $parameter); $number_of_errors++; next; } unless (defined $param->{'config_hash'}{$parameter}) { $param->{'config_hash'}{$parameter} = $params{$parameter}->{'default'}; } } return $number_of_errors; } ## Some functionalities activated by some parameter values require that ## some optional CPAN modules are installed. This function checks whether ## these modules are installed and if they are missing, changes the config ## to fall back to a functioning that doesn't require a module and issues ## a warning. ## Returns the number of missing modules. sub _check_cpan_modules_required_by_config { my $param = shift; my $number_of_missing_modules = 0; ## Some parameters require CPAN modules if ($param->{'config_hash'}{'dkim_feature'} eq 'on') { eval "require Mail::DKIM"; if ($EVAL_ERROR) { $log->syslog('notice', 'Failed to load Mail::DKIM perl module ; setting "dkim_feature" to "off"' ); $param->{'config_hash'}{'dkim_feature'} = 'off'; $number_of_missing_modules++; } } return $number_of_missing_modules; } sub _dump_non_robot_parameters { my $param = shift; foreach my $key (keys %{$param->{'config_hash'}}) { unless ($valid_robot_key_words{$key}) { delete $param->{'config_hash'}{$key}; $log->syslog('err', 'Robot %s config: unknown robot parameter: %s', $param->{'robot'}, $key); } } } sub _load_single_robot_config { my $param = shift; my $robot = $param->{'robot'}; my $robot_conf; my $config_err; my $config_file = "$Conf{'etc'}/$robot/robot.conf"; my $force_reload = $param->{'force_reload'}; if (!$force_reload and _source_has_not_changed($config_file)) { $force_reload = 0; } if (!$force_reload) { $log->syslog('debug3', 'File %s has not changed since the last cache. Using cache', $config_file); unless (-r $config_file) { $log->syslog('err', 'No read access on %s', $config_file); return undef; } unless ( $robot_conf = _load_binary_cache( {'config_file' => $config_file . $binary_file_extension} ) ) { $force_reload = 1; } } if ($force_reload) { if (my $config_loading_result = _load_config_file_to_hash( {'path_to_config_file' => $config_file} ) ) { $robot_conf = $config_loading_result->{'config'}; $config_err = $config_loading_result->{'errors'}; } else { $log->syslog('err', 'Unable to load %s. Aborting', $config_file); return undef; } # Remove entries which are not supposed to be defined at the robot # level. _dump_non_robot_parameters( {'config_hash' => $robot_conf, 'robot' => $robot}); ## Default for 'host' is the domain $robot_conf->{'host'} ||= $robot; $robot_conf->{'robot_name'} ||= $robot; unless ($robot_conf->{'dkim_signer_domain'}) { $robot_conf->{'dkim_signer_domain'} = $robot; } my @dmarc = split /[,\s]+/, ($robot_conf->{'dmarc_protection_mode'} || ''); if (@dmarc) { $robot_conf->{'dmarc_protection_mode'} = \@dmarc; } else { delete $robot_conf->{'dmarc_protection_mode'}; } _set_listmasters_entry({'config_hash' => $robot_conf}); _infer_robot_parameter_values({'config_hash' => $robot_conf}); _store_source_file_name( {'config_hash' => $robot_conf, 'config_file' => $config_file}); _save_config_hash_to_binary( {'config_hash' => $robot_conf, 'source_file' => $config_file}); return undef if ($config_err); } _replace_file_value_by_db_value({'config_hash' => $robot_conf}) unless $param->{'no_db'}; _load_robot_secondary_config_files({'config_hash' => $robot_conf}); return $robot_conf; } sub _set_listmasters_entry { my $param = shift; my $number_of_valid_email = 0; my $number_of_email_provided = 0; # listmaster is a list of email separated by commas if (defined $param->{'config_hash'}{'listmaster'} && $param->{'config_hash'}{'listmaster'} !~ /^\s*$/) { $param->{'config_hash'}{'listmaster'} =~ s/\s//g; my @emails_provided = split(/,/, $param->{'config_hash'}{'listmaster'}); $number_of_email_provided = $#emails_provided + 1; foreach my $lismaster_address (@emails_provided) { if (Sympa::Tools::Text::valid_email($lismaster_address)) { # Note: 'listmasters' was obsoleted. push @{$param->{'config_hash'}{'listmasters'}}, $lismaster_address; $number_of_valid_email++; } else { $log->syslog( 'err', 'Robot %s config: Listmaster address "%s" is not a valid email', $param->{'config_hash'}{'host'}, $lismaster_address ); } } } else { if ($param->{'main_config'}) { $log->syslog('err', 'Robot %s config: No listmaster defined. This is the main config. It MUST define at least one listmaster. Stopping here' ); return undef; } else { # Note: 'listmasters' was obsoleted. $param->{'config_hash'}{'listmasters'} = $Conf{'listmasters'}; $param->{'config_hash'}{'listmaster'} = $Conf{'listmaster'}; $number_of_valid_email = $#{$param->{'config_hash'}{'listmasters'}}; } } if ($number_of_email_provided > $number_of_valid_email) { $log->syslog( 'err', 'Robot %s config: All the listmasters addresses found were not valid. Out of %s addresses provided, %s only are valid email addresses', $param->{'config_hash'}{'host'}, $number_of_email_provided, $number_of_valid_email ); return undef; } return $number_of_valid_email; } # No longer used. #sub _check_double_url_usage; sub _parse_custom_robot_parameters { my $param = shift; my $csp_tmp_storage = undef; if (defined $param->{'config_hash'}{'custom_robot_parameter'} && ref() ne 'HASH') { foreach my $custom_p ( @{$param->{'config_hash'}{'custom_robot_parameter'}}) { if ($custom_p =~ /(\S+)\s*\;\s*(.+)/) { $csp_tmp_storage->{$1} = $2; } } $param->{'config_hash'}{'custom_robot_parameter'} = $csp_tmp_storage; } } sub _replace_file_value_by_db_value { my $param = shift; my $robot = $param->{'config_hash'}{'robot_name'}; # The name of the default robot is "*" in the database. $robot = '*' if ($param->{'config_hash'}{'robot_name'} eq ''); foreach my $label (keys %db_storable_parameters) { next unless ($robot ne '*' && $valid_robot_key_words{$label} == 1); my $value = get_db_conf($robot, $label); if (defined $value) { $param->{'config_hash'}{$label} = $value; } } } # Stores the config hash binary representation to a file. # Returns 1 or undef if something went wrong. sub _save_binary_cache { my $param = shift; my $lock_fh = Sympa::LockedFile->new($param->{'target_file'}, 2, '>'); unless ($lock_fh) { $log->syslog('err', 'Could not create new lock'); return undef; } eval { Storable::store_fd($param->{'conf_to_save'}, $lock_fh); }; if ($EVAL_ERROR) { $log->syslog( 'err', 'Failed to save the binary config %s. error: %s', $param->{'target_file'}, $EVAL_ERROR ); unless ($lock_fh->close()) { return undef; } return undef; } eval { chown( (getpwnam(Sympa::Constants::USER))[2], (getgrnam(Sympa::Constants::GROUP))[2], $param->{'target_file'} ); }; if ($EVAL_ERROR) { $log->syslog( 'err', 'Failed to change owner of the binary file %s. error: %s', $param->{'target_file'}, $EVAL_ERROR ); unless ($lock_fh->close()) { return undef; } return undef; } unless ($lock_fh->close()) { return undef; } return 1; } # Loads the config hash binary representation from a file an returns it # Returns the hash or undef if something went wrong. sub _load_binary_cache { my $param = shift; my $result = undef; my $lock_fh = Sympa::LockedFile->new($param->{'config_file'}, 2, '<'); unless ($lock_fh) { $log->syslog( 'err', 'Could not create new lock, error was : %s', Sympa::LockedFile::last_error() ); return undef; } eval { $result = Storable::fd_retrieve($lock_fh); }; if ($EVAL_ERROR) { $log->syslog( 'err', 'Failed to load the binary config %s. error: %s', $param->{'config_file'}, $EVAL_ERROR ); unless ($lock_fh->close()) { return undef; } return undef; } ## Release the lock unless ($lock_fh->close()) { return undef; } return $result; } sub _save_config_hash_to_binary { my $param = shift; unless ( _save_binary_cache( { 'conf_to_save' => $param->{'config_hash'}, 'target_file' => $param->{'config_hash'}{'source_file'} . $binary_file_extension } ) ) { $log->syslog( 'err', 'Could not save main config %s', $param->{'config_hash'}{'source_file'} ); } } sub _source_has_not_changed { my $file = shift; my $file_bin = $file . $binary_file_extension; return 1 if -r $file and -r $file_bin and Sympa::Tools::File::get_mtime($file) < Sympa::Tools::File::get_mtime($file_bin); return 0; } sub _store_source_file_name { my $param = shift; $param->{'config_hash'}{'source_file'} = $param->{'config_file'}; } # FXIME:Use Sympa::search_fullpath(). sub _get_config_file_name { my $param = shift; my $config_file; if ($param->{'robot'}) { $config_file = $Conf{'etc'} . '/' . $param->{'robot'} . '/' . $param->{'file'}; } else { $config_file = $Conf{'etc'} . '/' . $param->{'file'}; } $config_file = Sympa::Constants::DEFAULTDIR . '/' . $param->{'file'} unless (-f $config_file); return $config_file; } sub _create_robot_like_config_for_main_robot { return if (defined $Conf::Conf{'robots'}{$Conf::Conf{'domain'}}); my $main_conf_no_robots = Sympa::Tools::Data::dup_var(\%Conf); delete $main_conf_no_robots->{'robots'}; _remove_unvalid_robot_entry( {'config_hash' => $main_conf_no_robots, 'quiet' => 1}); $Conf{'robots'}{$Conf{'domain'}} = $main_conf_no_robots; } sub _get_parameters_names_by_category { my $param_by_categories; my $current_category; foreach my $entry (@Sympa::ConfDef::params) { unless ($entry->{'name'}) { $current_category = $entry->{'gettext_id'}; } else { $param_by_categories->{$current_category}{$entry->{'name'}} = 1; } } return $param_by_categories; } =over 4 =item _load_wwsconf ( FILE ) Load WWSympa configuration file. =back =cut sub _load_wwsconf { my $param = shift; my $config_hash = $param->{'config_hash'}; my $config_file = get_wwsympa_conf(); return 0 unless -f $config_file; # this file is optional. ## Old params my %old_param = ( 'alias_manager' => 'No more used, using ' . $config_hash->{'alias_manager'}, 'wws_path' => 'No more used', 'icons_url' => 'No more used. Using static_content/icons instead.', 'robots' => 'Not used anymore. Robots are fully described in their respective robot.conf file.', 'task_manager_pidfile' => 'No more used', 'bounced_pidfile' => 'No more used', 'archived_pidfile' => 'No more used', ); ## Valid params my %default_conf = map { $_->{'name'} => $_->{'default'} } grep { exists $_->{'file'} and $_->{'file'} eq 'wwsympa.conf' } @Sympa::ConfDef::params; my $conf = \%default_conf; my $fh; unless (open $fh, '<', $config_file) { $log->syslog('err', 'Unable to open %s', $config_file); return undef; } while (<$fh>) { next if /^\s*\#/; if (/^\s*(\S+)\s+(.+)$/i) { my ($k, $v) = ($1, $2); $v =~ s/\s*$//; if (exists $conf->{$k}) { $conf->{$k} = $v; } elsif (defined $old_param{$k}) { $log->syslog('err', 'Parameter %s in %s no more supported: %s', $k, $config_file, $old_param{$k}); } else { $log->syslog('err', 'Unknown parameter %s in %s', $k, $config_file); } } next; } close $fh; ## Check binaries and directories if ($conf->{'arc_path'} && (!-d $conf->{'arc_path'})) { $log->syslog('err', 'No web archives directory: %s', $conf->{'arc_path'}); } if ($conf->{'bounce_path'} && (!-d $conf->{'bounce_path'})) { $log->syslog( 'err', 'Missing directory "%s" (defined by "bounce_path" parameter)', $conf->{'bounce_path'} ); } if ($conf->{'mhonarc'} && (!-x $conf->{'mhonarc'})) { $log->syslog('err', 'MHonArc is not installed or %s is not executable', $conf->{'mhonarc'}); } ## set default $conf->{'log_facility'} ||= $config_hash->{'syslog'}; foreach my $k (keys %$conf) { $config_hash->{$k} = $conf->{$k}; } $wwsconf = $conf; return $wwsconf; } # MOVED: Use Sympa::Tools::WWW::update_css(). #sub update_css; # lazy loading on demand my %mime_types; # Old name: Sympa::Tools::WWW::get_mime_type(). # FIXME: This would be moved to such as Site package. sub get_mime_type { my $type = shift; %mime_types = _load_mime_types() unless %mime_types; return $mime_types{$type}; } # Old name: Sympa::Tools::WWW::load_mime_types(). sub _load_mime_types { my %types = (); my @localisation = ( Sympa::search_fullpath('*', 'mime.types'), '/etc/mime.types', '/usr/local/apache/conf/mime.types', '/etc/httpd/conf/mime.types', ); foreach my $loc (@localisation) { my $fh; next unless $loc and open $fh, '<', $loc; foreach my $line (<$fh>) { next if $line =~ /^\s*\#/; chomp $line; my ($k, $v) = split /\s+/, $line, 2; next unless $k and $v and $v =~ /\S/; my @extensions = split /\s+/, $v; # provides file extention, given the content-type if (@extensions) { $types{$k} = $extensions[0]; } foreach my $ext (@extensions) { $types{$ext} = $k; } } close $fh; return %types; } return; } 1; sympa-6.2.24/src/lib/Sympa/0000755000175000017500000000000013216651447014331 5ustar rackerackesympa-6.2.24/src/lib/Sympa/SharedDocument.pm0000644000175000017500000010532613216651447017603 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::SharedDocument; use strict; use warnings; use English qw(-no_match_vars); use File::Find qw(); use POSIX qw(); use Sympa; use Conf; use Sympa::Language; use Sympa::Scenario; use Sympa::Tools::Data; use Sympa::Tools::File; use Sympa::Tools::Text; # Hash of the icons linked with a type of file. my %icons = ( 'unknown' => 'unknown.png', 'folder' => 'folder.png', 'current_folder' => 'folder.open.png', 'application' => 'unknown.png', 'octet-stream' => 'binary.png', 'audio' => 'sound1.png', 'image' => 'image2.png', 'text' => 'text.png', 'video' => 'movie.png', 'father' => 'back.png', 'sort' => 'down.png', 'url' => 'link.png', 'left' => 'left.png', 'right' => 'right.png', ); # Creates a new object. sub new { my $class = shift; my $list = shift; my $path = shift; my %options = @_; die 'bug in logic. Ask developer' unless ref $list eq 'Sympa::List'; my $paths; if (ref $path eq 'ARRAY') { $paths = $path; } elsif (defined $path and length $path) { $paths = [split m{/+}, $path]; } else { $paths = []; } unless (@$paths) { return $class->_new_root($list); } else { my $parent_paths = [@$paths]; my $name = pop @$parent_paths; return undef unless defined $name and length $name and $name !~ /\A[.]+\z/ and $name !~ /\A[.]desc(?:[.]|\z)/; my $parent = $class->new($list, $parent_paths); return undef unless $parent; #FIXME: At present, conversion by qencode_filename() / # qdecode_filename() may not be bijective. So we take the first one # of (possiblly multiple) matching paths insted of taking encoded one. my ($self) = $parent->get_children(%options, name => $name); return $self; } } sub _new_root { my $class = shift; my $list = shift; my $status; if (-e $list->{'dir'} . '/shared') { $status = 'exist'; } elsif (-e $list->{'dir'} . '/pending.shared') { $status = 'deleted'; } else { $status = 'none'; } bless { context => $list, fs_name => '', fs_path => $list->{'dir'} . '/shared', name => '', paths => [], status => $status, type => 'root', } => $class; } sub _new_child { my $self = shift; my $fs_name = shift; my %options = @_; # Document isn't a description file. # It exists. # It has non-zero size. return undef if $fs_name =~ /\A[.]+\z/ or $fs_name =~ /\A[.]desc(?:[.]|\z)/; return undef unless -e $self->{fs_path} . '/' . $fs_name; unless (exists $options{allow_empty} and $options{allow_empty}) { return undef unless -s $self->{fs_path} . '/' . $fs_name; } my $child = bless { context => $self->{context}, parent => $self } => (ref $self); my $stem; if ($fs_name =~ /\A[.](.*)[.]moderate\z/) { $stem = $1; $child->{moderate} = 1; } else { $stem = $fs_name; } $child->{fs_name} = $fs_name; $child->{fs_path} = $self->{fs_path} . '/' . $fs_name; $child->{name} = Sympa::Tools::Text::qdecode_filename($stem); $child->{paths} = [@{$self->{paths}}, $child->{name}]; $child->{file_extension} = $1 if $stem =~ /[.](\w+)\z/; $child->{type} = (-d $child->{fs_path}) ? 'directory' : ($child->{file_extension} and $child->{file_extension} eq 'url') ? 'url' : 'file'; if (exists $options{name}) { return undef if $child->{name} ne $options{name}; } if (exists $options{moderate}) { return undef if $child->{moderate} xor $options{moderate}; } ## Check access control #check_access_control($child, $param); # Date. $child->{date_epoch} = Sympa::Tools::File::get_mtime($child->{fs_path}); # Size of the doc. $child->{size} = (-s $child->{fs_path}) / 1000; # Load .desc file unless root directory. my %desc = $child->_load_desc; if (%desc) { $child->{serial_desc} = $desc{serial_desc}; $child->{owner} = $desc{email}; $child->{title} = $desc{title}; $child->{scenario} = {read => $desc{read}, edit => $desc{edit}}; } if (exists $options{owner}) { return undef unless defined $child->{owner}; return undef if $child->{owner} ne $options{owner}; } # File, directory or URL ? my $robot_id = $self->{context}->{'domain'}; if ($child->{type} eq 'url') { $child->{icon} = _get_icon($robot_id, 'url'); if (open my $fh, $child->{fs_path}) { my $url = <$fh>; close $fh; chomp $url; $child->{url} = $url; } if ($child->{name} =~ /\A(.+)[.]url\z/) { $child->{label} = $1; } } elsif ($child->{type} eq 'file') { if ($child->{file_extension} and grep { lc $child->{file_extension} eq $_ } qw(htm html)) { # HTML. $child->{mime_type} = 'text/html'; $child->{html} = 1; $child->{icon} = _get_icon($robot_id, 'text'); } elsif (my $type = Conf::get_mime_type($child->{file_extension} || '')) { $child->{mime_type} = lc $type; # Type of the icon. my $mimet; if (lc $type eq 'application/octet-stream') { $mimet = 'octet-stream'; } else { ($mimet) = split m{/}, $type; } $child->{icon} = _get_icon($robot_id, $mimet) || _get_icon($robot_id, 'unknown'); } else { # Unknown file type. $child->{icon} = _get_icon($robot_id, 'unknown'); } } else { # Directory. $child->{icon} = _get_icon($robot_id, 'folder'); } $child; } sub _load_desc { my $self = shift; my $desc_file = $self->_desc_file; return unless $desc_file and -e $desc_file; my %desc = _load_desc_file($desc_file); $desc{serial_desc} = Sympa::Tools::File::get_mtime($desc_file); return %desc; } # Gets path of property description on physical filesystem. sub _desc_file { my $self = shift; return (-d $self->{fs_path}) ? ($self->{fs_path} . '/.desc') : ($self->{parent}->{fs_path} . '/.desc.' . $self->{fs_name}); } # Old name: Sympa::Tools::WWW::get_desc_file(). #FIXME: Generalize parsing. #FIXME: Lock file. sub _load_desc_file { my $file = shift; my $line; my %hash; open my $fh, '<', $file or return; #FIXME: Check errors. while ($line = <$fh>) { if ($line =~ /^title\s*$/) { # Title of the document while ($line = <$fh>) { last if ($line =~ /^\s*$/); $line =~ /^\s*(\S.*\S)\s*/; $hash{'title'} = $hash{'title'} . $1 . " "; } } if ($line =~ /^creation\s*$/) { # Creation of the document. while ($line = <$fh>) { last if ($line =~ /^\s*$/); if ($line =~ /^\s*email\s*(\S*)\s*/) { $hash{'email'} = $1; } if ($line =~ /^\s*date_epoch\s*(\d*)\s*/) { $hash{'date'} = $1; } } } if ($line =~ /^access\s*$/) { # Access scenarios for the document. while ($line = <$fh>) { last if ($line =~ /^\s*$/); if ($line =~ /^\s*read\s*(\S*)\s*/) { $hash{'read'} = $1; } if ($line =~ /^\s*edit\s*(\S*)\s*/) { $hash{'edit'} = $1; } } } } close $fh; return %hash; } # Old name: Sympa::Tools::WWW::get_icon(). sub _get_icon { my $robot = shift || '*'; my $type = shift; return undef unless defined $icons{$type}; return Conf::get_robot_conf($robot, 'static_content_url') . '/icons/' . $icons{$type}; } sub as_hashref { my $self = shift; my %hash = %$self; $hash{context} = { name => $self->{context}->{'name'}, host => $self->{context}->{'admin'}{'host'}, }; $hash{parent} = $self->{parent}->as_hashref if $self->{parent}; $hash{paths} = [@{$self->{paths}}]; # Special items. # The i18n'ed date. $hash{date} = Sympa::Language->instance->gettext_strftime('%d %b %Y %H:%M:%S', localtime $self->{date_epoch}) if defined $self->{date_epoch}; # Path components with trailing slash. $hash{paths_d} = [@{$self->{paths}}]; push @{$hash{paths_d}}, '' if grep { $self->{type} eq $_ } qw(root directory); my @ancestors; my $p = $self->{parent}; while ($p) { unshift @ancestors, { name => $p->{name}, paths => $p->{paths}, paths_d => [@{$p->{paths}}, ''], type => $p->{type}, }; $p = $p->{parent}; } $hash{ancestors} = [@ancestors]; return {%hash}; } # Old name: Sympa::List::create_shared(). sub create { my $self = shift; unless ($self->{type} eq 'root') { $ERRNO = POSIX::EINVAL(); return undef; } return undef unless CORE::mkdir $self->{fs_path}, 0777; $self->{status} = 'exist'; return 1; } sub create_child { my $self = shift; my $new_name = shift; my %options = @_; $options{type} ||= 'directory'; if (not Sympa::SharedDocument::valid_name($new_name)) { $ERRNO = POSIX::EINVAL(); return undef; } my $new_fs_name = $options{moderate} ? '.' . Sympa::Tools::Text::qencode_filename($new_name) . '.moderate' : Sympa::Tools::Text::qencode_filename($new_name); my $new_fs_path = $self->{fs_path} . '/' . $new_fs_name; my $new_desc_file = ($options{type} eq 'directory') ? $new_fs_path . '/.desc' : $self->{fs_path} . '/.desc.' . $new_fs_name; if ($options{type} eq 'directory') { return undef unless mkdir $new_fs_path, 0777; } else { my $fh; return undef unless open $fh, '>', $new_fs_path; if (exists $options{content} and defined $options{content}) { print $fh $options{content}; } close $fh; } # Creation of a default description file my $fh; return undef unless open $fh, '>', $new_desc_file; print $fh "title\n"; print $fh " \n"; print $fh "\n"; print $fh "creation\n"; print $fh " date_epoch " . time . "\n"; print $fh " email $options{owner}\n"; print $fh "\n"; print $fh "access\n"; print $fh " read $options{scenario}->{read}\n"; print $fh " edit $options{scenario}->{edit}\n"; print $fh "\n"; close $fh; return $self->_new_child($new_fs_name, allow_empty => 1); } sub delete { my $self = shift; unless ($self->{type} eq 'root') { $ERRNO = POSIX::EINVAL(); return undef; } my $list = $self->{context}; return undef unless CORE::rename $self->{fs_path}, $list->{'dir'} . '/pending.shared'; $self->{status} = 'deleted'; return 1; } sub count_children { my $self = shift; my $dh; return undef unless opendir $dh, $self->{fs_path}; my @children = grep { !/\A[.]+\z/ and !/\A[.]desc(?:[.]|\z)/ } sort readdir $dh; closedir $dh; return scalar @children; } sub get_children { my $self = shift; my %options = @_; my $dh; return unless opendir $dh, $self->{fs_path}; #FIXME: Report error. my @children = sort { _by_order($options{order_by}) } grep {$_} map { $self->_new_child($_, %options) } grep { !/\A[.]+\z/ and !/\A[.]desc(?:[.]|\z)/ } sort readdir $dh; closedir $dh; return @children; } # Function which sorts a hash of documents # Sort by various parameters # Old name: by_order() in wwsympa.fcgi. sub _by_order { my $order = shift || 'order_by_doc'; if ($order eq 'order_by_doc') { $a->{name} cmp $b->{name} || $b->{date_epoch} <=> $a->{date_epoch}; } elsif ($order eq 'order_by_author') { $a->{owner} cmp $b->{owner} || $b->{date_epoch} <=> $a->{date_epoch}; } elsif ($order eq 'order_by_size') { $a->{size} <=> $b->{size} || $b->{date_epoch} <=> $a->{date_epoch}; } elsif ($order eq 'order_by_date') { $b->{date_epoch} <=> $a->{date_epoch} || $a->{name} cmp $b->{name}; } else { $a->{name} cmp $b->{name}; } } # OBSOLETED. Never used. sub dump { my $self = shift; my $fd = shift; Sympa::Tools::Data::dump_var($self, 0, $fd); } # OBSOLETED. No longer used. sub dup { my $self = shift; my $copy = {}; foreach my $k (keys %$self) { $copy->{$k} = $self->{$k}; } return $copy; } sub count_moderated_descendants { my $self = shift; return undef unless -d $self->{fs_path}; my $count = 0; File::Find::find( sub { $count++ if !/\A[.]desc([.]|\z)/ and /\A[.].*[.]moderate\z/; }, $self->{fs_path} ); return $count; } # Old name: Sympa::List::get_shared_moderated(). sub get_moderated_descendants { my $self = shift; return unless -e $self->{fs_path}; my @moderated = $self->_get_moderated_descendants; wantarray ? @moderated : \@moderated; } # Old name: Sympa::List::sort_dir_to_get_mod(). sub _get_moderated_descendants { my $self = shift; my @moderated; foreach my $child ($self->get_children) { push @moderated, $child if $child->{moderate}; push @moderated, $child->_get_moderated_descendants if $child->{type} eq 'directory'; } return @moderated; } # Returns a hash with privileges in read, edit, control. ## Regulars # read(/) = default (config list) # edit(/) = default (config list) # control(/) = not defined # read(A/B)= (read(A) && read(B)) || # (author(A) || author(B)) # edit = idem read # control (A/B) : author(A) || author(B) # + (set owner A/B) if (empty directory && # control A) # Arguments: # (\%mode,$path) # if mode->{'read'} control access only for read # if mode->{'edit'} control access only for edit # if mode->{'control'} control access only for control # return the hash ( # $result{'may'}{'read'} == $result{'may'}{'edit'} == $result{'may'}{'control'} if is_author else : # $result{'may'}{'read'} = 0 or 1 (right or not) # $result{'may'}{'edit'} = 0(not may edit) or 0.5(may edit with moderation) or 1(may edit ) : it is not a boolean anymore # $result{'may'}{'control'} = 0 or 1 (right or not) # $result{'reason'}{'read'} = string for authorization_reject.tt2 when may_read == 0 # $result{'reason'}{'edit'} = string for authorization_reject.tt2 when may_edit == 0 # $result{'scenario'}{'read'} = scenario name for the document # $result{'scenario'}{'edit'} = scenario name for the document # Old name: d_access_control() in wwsympa.fcgi, # Sympa::SharedDocument::check_access_control(). sub get_privileges { my $self = shift; my %options = @_; my $mode = $options{mode} || ''; my $sender = $options{sender}; my $auth_method = $options{auth_method}; my $scenario_context = $options{scenario_context} || {}; my $list = $self->{context}; # Result my %result; $result{'reason'} = {}; my $mode_read = (0 <= index $mode, 'read'); my $mode_edit = (0 <= index $mode, 'edit'); my $mode_control = (0 <= index $mode, 'control'); # Control for editing my $may_read = 1; my $why_not_read = ''; my $may_edit = 1; my $why_not_edit = ''; my $is_author = 0; # <=> $may_control # First check privileges on the root shared directory. $result{'scenario'}{'read'} = $list->{'admin'}{'shared_doc'}{'d_read'}{'name'}; $result{'scenario'}{'edit'} = $list->{'admin'}{'shared_doc'}{'d_edit'}{'name'}; # Privileged owner has all privileges. if (Sympa::is_listmaster($list, $sender) or $list->is_admin('privileged_owner', $sender)) { $result{'may'}{'read'} = 1; $result{'may'}{'edit'} = 1; $result{'may'}{'control'} = 1; return %result; } # if not privileged owner if ($mode_read) { my $result = Sympa::Scenario::request_action($list, 'shared_doc.d_read', $auth_method, $scenario_context); my $action; if (ref($result) eq 'HASH') { $action = $result->{'action'}; $why_not_read = $result->{'reason'}; } $may_read = ($action =~ /\Ado_it\b/i); } if ($mode_edit) { my $result = Sympa::Scenario::request_action($list, 'shared_doc.d_edit', $auth_method, $scenario_context); my $action; if (ref($result) eq 'HASH') { $action = $result->{'action'}; $why_not_edit = $result->{'reason'}; } $action ||= ''; # edit = 0, 0.5 or 1 $may_edit = ($action =~ /\Ado_it\b/i) ? 1 : ($action =~ /\Aeditor\b/i) ? 0.5 : 0; $why_not_edit = '' if $may_edit; } # Only authenticated users can edit files. unless ($sender) { $may_edit = 0; $why_not_edit = 'not_authenticated'; } #if ($mode_control) { # $result{'may'}{'control'} = 0; #} my $current = $self; while ($current and @{$current->{paths}}) { if ($current->{scenario}) { if ($mode_read) { my $result = Sympa::Scenario::request_action( $list, 'shared_doc.d_read', $auth_method, { %$scenario_context, scenario => $current->{scenario}{read} } ); my $action; if (ref($result) eq 'HASH') { $action = $result->{'action'}; $why_not_read = $result->{'reason'}; } $may_read = $may_read && ($action =~ /\Ado_it\b/i); $why_not_read = '' if $may_read; } if ($mode_edit) { my $result = Sympa::Scenario::request_action( $list, 'shared_doc.d_edit', $auth_method, { %$scenario_context, scenario => $current->{scenario}{edit} } ); my $action_edit; if (ref($result) eq 'HASH') { $action_edit = $result->{'action'}; $why_not_edit = $result->{'reason'}; } $action_edit ||= ''; # $may_edit = 0, 0.5 or 1 my $may_action_edit = ($action_edit =~ /\Ado_it\b/i) ? 1 : ($action_edit =~ /\Aeditor\b/i) ? 0.5 : 0; $may_edit = !($may_edit and $may_action_edit) ? 0 : ($may_edit == 0.5 or $may_action_edit == 0.5) ? 0.5 : 1; $why_not_edit = '' if $may_edit; } # Only authenticated users can edit files. unless ($sender) { $may_edit = 0; $why_not_edit = 'not_authenticated'; } $is_author = $is_author || (($sender || 'nobody') eq $current->{owner}); unless (defined $result{'scenario'}{'read'}) { $result{scenario}{read} = $current->{scenario}{read}; $result{scenario}{edit} = $current->{scenario}{edit}; } # Author has all privileges. if ($is_author) { $result{'may'}{'read'} = 1; $result{'may'}{'edit'} = 1; $result{'may'}{'control'} = 1; return %result; } } $current = $current->{parent}; } if ($mode_read) { $result{'may'}{'read'} = $may_read; $result{'reason'}{'read'} = $why_not_read; } if ($mode_edit) { $result{'may'}{'edit'} = $may_edit; $result{'reason'}{'edit'} = $why_not_edit; } #if ($mode_control) { # $result{'may'}{'control'} = 0; #} return %result; } # Returns the mode of editing included in $action : 0, 0.5 or 1 # Old name: Sympa::Tools::WWW::find_edit_mode(). # No longer used. #sub _find_edit_mode { # my $action = shift; # # my $result; # if ($action =~ /editor/i) { # $result = 0.5; # } elsif ($action =~ /do_it/i) { # $result = 1; # } else { # $result = 0; # } # return $result; #} # Returns the mode of editing : 0, 0.5 or 1 : # do the merging between 2 args of right access edit : "0" > "0.5" > "1" # instead of a "and" between two booleans : the most restrictive right is # imposed # Old name: Sympa::Tools::WWW::merge_edit(). # No longer used. #sub _merge_edit { # my $arg1 = shift; # my $arg2 = shift; # my $result; # # if ($arg1 == 0 || $arg2 == 0) { # $result = 0; # } elsif ($arg1 == 0.5 || $arg2 == 0.5) { # $result = 0.5; # } else { # $result = 1; # } # return $result; #} # Old name: Sympa::List::get_shared_size(). sub get_size { my $self = shift; return undef unless grep { $self->{type} eq $_ } qw(root directory); return 0 unless -d $self->{fs_path}; return Sympa::Tools::File::get_dir_size($self->{fs_path}); } sub install { my $self = shift; unless ($self->{moderate} and -e $self->{fs_path}) { $ERRNO = POSIX::ENOENT(); return undef; } my $new_fs_name; if ($self->{fs_name} =~ /\A[.](.+)[.]moderate\z/) { $new_fs_name = $1; } else { $ERRNO = POSIX::ENOENT(); return undef; } my $new_fs_path = $self->{parent}->{fs_path} . '/' . $new_fs_name; my $desc_file = $self->_desc_file; my $new_desc_file = (-d $self->{fs_path}) ? ($new_fs_path . '/.desc') : ($self->{parent}->{fs_path} . '/.desc.' . $new_fs_name); # Rename the old file in .old if exists. if (-e $new_fs_path) { return undef unless CORE::rename $new_fs_path, $new_fs_path . '.old'; if (-e $new_desc_file) { return undef unless CORE::rename $new_desc_file, $new_desc_file . '.old'; } } return undef unless CORE::rename $self->{fs_path}, $new_fs_path; if (-e $desc_file) { return undef unless CORE::rename $desc_file, $new_desc_file; } $self->{fs_path} = $new_fs_path; $self->{fs_name} = $new_fs_name; delete $self->{moderate}; return 1; } sub rename { my $self = shift; my $new_name = shift; if ($self->{type} eq 'root') { $ERRNO = POSIX::EPERM(); return undef; } if (not Sympa::SharedDocument::valid_name($new_name) or ($self->{type} eq 'url' and $new_name !~ /[.]url\z/)) { $ERRNO = POSIX::EINVAL(); return undef; } my $new_fs_name; if ($self->{moderate}) { $new_fs_name = '.' . Sympa::Tools::Text::qencode_filename($new_name) . '.moderate'; } else { $new_fs_name = Sympa::Tools::Text::qencode_filename($new_name); } my $new_fs_path = $self->{parent}->{fs_path} . '/' . $new_fs_name; my $new_paths = [@{$self->{paths}}[0 .. ($#{$self->{paths}} - 1)], $new_name]; return undef unless CORE::rename $self->{fs_path}, $new_fs_path; # Rename description file. unless ($self->{type} eq 'directory') { my $desc_file = $self->_desc_file; my $new_desc_file = $self->{parent}->{fs_path} . '/.desc.' . $new_fs_name; if (-e $desc_file) { return undef unless CORE::rename $desc_file, $new_desc_file; } } @{$self}{qw(fs_name fs_path name paths)} = ($new_fs_name, $new_fs_path, $new_name, $new_paths); return 1; } sub restore { my $self = shift; unless ($self->{type} eq 'root') { $ERRNO = POSIX::EINVAL(); return undef; } my $list = $self->{context}; return undef unless CORE::rename $list->{'dir'} . '/pending.shared', $self->{fs_path}; $self->{status} = 'exist'; return 1; } sub rmdir { my $self = shift; unless ($self->{type} eq 'directory' and -d $self->{fs_path}) { $ERRNO = POSIX::ENOTDIR(); return undef; } if ($self->count_children) { $ERRNO = POSIX::EEXIST(); return undef; } if (-e $self->_desc_file) { return undef unless CORE::unlink $self->_desc_file; } CORE::rmdir $self->{fs_path}; } #FIXME:Generalize serialization. #FIXME:Lock file. sub save_description { my $self = shift; $self->{title} = '' unless defined $self->{title}; my $fh; return undef unless open $fh, '>', $self->_desc_file; print $fh "title\n"; printf $fh " %s\n", $self->{title}; print $fh "\n"; print $fh "access\n"; printf $fh " read %s\n", $self->{scenario}{read}; printf $fh " edit %s\n", $self->{scenario}{edit}; print $fh "\n"; print $fh "creation\n"; printf $fh " date_epoch %s\n", $self->{date_epoch}; printf $fh " email %s\n", $self->{owner}; print $fh "\n"; close $fh; $self->{serial_desc} = Sympa::Tools::File::get_mtime($self->_desc_file); return 1; } sub unlink { my $self = shift; if (grep { $self->{type} eq $_ } qw(root directory)) { $ERRNO = POSIX::EPERM(); return undef; } return undef unless CORE::unlink $self->{fs_path}; my $desc_file = $self->_desc_file; if (-e $desc_file) { return undef unless CORE::unlink $desc_file; } return 1; } sub valid_name { my $new_name = shift; return undef if not defined $new_name or $new_name !~ /\S/ or $new_name =~ /\A[.]/ or 0 <= index($new_name, '/') or $new_name =~ /[<>\\\*\$\[\]\n]/ or $new_name =~ /[~#\[\]]$/; return 1; } # Old name: tools::escape_docname(). # DEPRECATED. No longer used. #sub escape_docname; sub get_id { shift->{fs_path}; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::SharedDocument - Shared document repository and its nodes =head1 SYNOPSIS use Sympa::SharedDocument; $shared = Sympa::SharedDocument->new($list, $path); %access = $shared->get_privileges('read', $email, 'md5', {...}); @children = $shared->get_children; $parent = $shared->{parent}; =head1 DESCRIPTION L implements shared document repository of lists. =head2 Methods =over =item new ( $list, [ $path, [ allow_empty =E 1 ] ] ) I. Creates new instance. Parameters: =over =item $list A L instance. =item $path String to determine path or arrayref of path components. The path is relative to repository root. =item allow_empty =E 1 Don't omit files with zero size. =back Returns: If $path is empty or not specified, returns new instance of repository root; {status} attribute will be set. If $path is not empty and the path exists, returns new instance of node. Otherwise returns false value. =item as_hashref ( ) I. Casts the instance to hashref. Parameters: None. Returns: A hashref including attributes of instance (see L) and following special items: =over =item {ancestors} Arrayref of hashrefs including some attributes of all ancestor nodes. =item {context} Hashref including name and host of the list. =item {date} Localized form of {date_epoch}. =item {parent} Hashref including attributes of parent node recursively. =item {paths_d} Same as {paths} but, if the node is a directory, includes additional empty component at the end. This is useful when the path created by join() should be followed by additional "/" character. =back =item count_children ( ) I. Returns number of child nodes. =item count_moderated_descendants ( ) I. Returns number of nodes waiting for moderation. =item create_child ( $name, owner =E $email, scenario =E $scenario, type =E $type, [ content => $content ] ) I. Creates child node and returns it. TBD. =item get_children ( [ moderate =E boolean ], [ name =E $name ], [ order_by =E $order ], [ owner =E $email ], [ allow_empty =E 1 ] ) I. Gets child nodes. Parameters: =over =item moderate =E boolean =item name =E $name =item owner =E $email Filters results. =item order_by =E $order Sorts results. $order may be one of C<'order_by_doc'> (by name of nodes), C<'order_by_author'> (by owner), C<'order_by_size'> (by size), C<'order_by_date'> (by modification time). Default is ordering by names. =item allow_empty =E 1 Don't omit nodes with zero size. =back Returns: (Possiblly empty) list of child nodes. =item get_moderated_descendants ( ) I. Returns the list of nodes waiting for moderation. Parameters: None. Returns: In array context, a list of nodes. In scalar context, an arrayref of them. =item get_privileges ( mode =E $mode, sender =E $sender, auth_method =E $auth_method, scenario_context =E $scenario_context ) I. Gets privileges of a user on the node. TBD. =item get_size ( ) I. Gets total size under current node. =item install ( ) I. Approves (install) file if it was held for moderation. Returns: True value. If installation failed, returns false value and sets $ERRNO ($!). =item rename ( $new_name ) I. Renames file or directory. Parameters: =over =item $new_name The name to be renamed to. =back Returns: True value. If renaming failed, returns false value and sets $ERRNO ($!). =item rmdir ( ) I. Removes directory from repository. Directory must be empty. Returns: True value. If removal failed, returns false value and sets $ERRNO ($!). =item save_description ( ) I. Creates or updates property description of the node. =item unlink ( ) I. Removes file from repository. Returns: True value. If removal failed, returns false value and sets $ERRNO ($!). =item get_id ( ) I. Returns unique identifier of instance. =back =head3 Methods for repository root =over =item create ( ) I. Creates document repository on physical filesystem. =item delete ( ) I. Deletes document repository. =item restore ( ) I. Restores deleted document repository. =back =head2 Functions =over =item valid_name ( $new_name ) I. Check if the name is allowed for directory and file. Note: This should be used with name of newly created node. Existing files and directories may have the name not allowed by this function. =back =head2 Attributes Instance of L may have following attributes. =over =item {context} I. Instance of L class the shared document repository belongs to. =item {date_epoch} I. Modification time of node in Unix time. =item {file_extension} File extension if any. =item {fs_name} I. Name of node on physical filesystem, i.e. the last part of {fs_path}. =item {fs_path} I. Full path of node on physical filesystem. =item {html} Only in HTML file. True value will be set. =item {icon} URL to icon. =item {label} Only in bookmark file. Label to be shown in hyperlink. =item {mime_type} Only in regular file. MIME content type of the file if it is known. =item {moderate} Set if node is held for moderation. =item {name} I. Name of node accessible by users, i.e. the last item of {paths}. =item {owner} Owner (author) of node, given by property description. =item {parent} Parent node if any. L instance. =item {paths} I. Arrayref to all path components of node accessible by users. =item {scenario}{read} =item {scenario}{edit} Scenario names to define privileges. These may be given by property description. =item {serial_desc} Modification time of property description in Unix time. Available if property description exists. =item {size} Size of file. =item {status} I. Status of repository: C<'exist'>, C<'deleted'> or C<'none'>. =item {title} Description of node, given by property description. =item {type} I. Type of node. C<'root'> (the root of repository), C<'directory'> (directory), C<'url'> (bookmark file) or C<'file'> (other file). =item {url} Only in bookmark file. URL to be linked. =back =head1 FILES =over =item I/shared/ Root of repository. =item I<... path>/I Directory or file. =item I<... path>/.I.moderate Moderated directory or file. =item I<... path>/I/.desc =item I<... path>/.desc.I =item I<... path>/.desc..I.moderate Property description of directories or files, not moderated or moderated. =back Note: The path components ("I" above) are encoded to the format suitable to physical filesystem. Such conversion will be hidden behind object methods. =head1 SEE ALSO L, L, L. =head1 HISTORY L module appeared on Sympa 5.2b.2. Rewritten L began to provide OO interface on Sympa 6.2.17. =cut sympa-6.2.24/src/lib/Sympa/Spool/0000755000175000017500000000000013216651447015425 5ustar rackerackesympa-6.2.24/src/lib/Sympa/Spool/Auth.pm0000644000175000017500000001101013216651447016655 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Spool::Auth; use strict; use warnings; use Conf; use Sympa::Tools::Text; use base qw(Sympa::Spool); sub _directories { return {directory => $Conf::Conf{'queuesubscribe'},}; } sub _filter { my $self = shift; my $metadata = shift; # Decode e-mail. $metadata->{email} = Sympa::Tools::Text::decode_filesystem_safe($metadata->{email}) if $metadata and $metadata->{email}; 1; } sub _filter_pre { my $self = shift; my $metadata = shift; # Encode e-mail. $metadata->{email} = Sympa::Tools::Text::encode_filesystem_safe($metadata->{email}) if $metadata and $metadata->{email}; 1; } use constant _generator => 'Sympa::Request'; sub _glob_pattern { shift->{_pattern} } use constant _marshal_format => '%ld,%s@%s_%s,%s,%s'; use constant _marshal_keys => [qw(date localpart domainpart KEYAUTH email action)]; use constant _marshal_regexp => qr{\A(\d+),([^\s\@]+)\@([-.\w]+)_([\da-f]+),([^\s,]*),(\w+)\z}; use constant _store_key => 'keyauth'; sub new { my $class = shift; my %options = @_; my $self = $class->SUPER::new(%options); # Build glob pattern using encoded e-mail. if ($self) { my $opts = {%options}; $self->_filter_pre($opts); $self->{_pattern} = Sympa::Spool::build_glob_pattern($self->_marshal_format, $self->_marshal_keys, %$opts); } $self; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Auth - Spool for held requests waiting for moderation =head1 SYNOPSIS use Sympa::Spool::Auth; my $spool = Sympa::Spool::Auth->new; my $request = Sympa::Request->new(...); $spool->store($request); my $spool = Sympa::Spool::Auth->new( context => $list, action => 'add'); my $size = $spool->size; my $spool = Sympa::Spool::Auth->new( context => $list, keyauth => $id, action => 'add'); my ($request, $handle) = $spool->next; $spool->remove($handle); =head1 DESCRIPTION L implements the spool for held requests waiting for moderation. =head2 Methods See also L. =over =item new ( [ context =E $list ], [ action =E $action ], [ keyauth =E $id ], [ email =E $email ]) =item next ( [ no_lock =E 1 ] ) If the pairs describing metadatas are specified, contents returned by next() are filtered by them. Order of items returned by next() is controlled by time of submission. =item quarantine ( ) Does nothing. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {action} Action requested. C<'add'> etc. =item {date} Unix time when the request was submitted. =item {email} E-mail of user who submitted the request, or target e-mail of the request. =item {keyauth} Authentication key generated automatically when the request is stored to spool. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queuesubscribe Directory path of held request spool. Note: Named such by historical reason. =back =head1 SEE ALSO L, L, L, L. =head1 HISTORY L appeared on Sympa 6.2.10. It was renamed to L on Sympa 6.2.13. =cut sympa-6.2.24/src/lib/Sympa/Spool/Held.pm0000644000175000017500000000633113216651447016642 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # # 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, see . package Sympa::Spool::Held; use strict; use warnings; use Conf; use base qw(Sympa::Spool); sub _directories { return {directory => $Conf::Conf{'queueauth'},}; } use constant _generator => 'Sympa::Message'; sub _glob_pattern { shift->{_pattern} } use constant _marshal_format => '%s@%s_%s'; use constant _marshal_keys => [qw(localpart domainpart AUTHKEY)]; use constant _marshal_regexp => qr{\A([^\s\@]+)\@([-.\w]+)_([\da-f]+)\z}; use constant _store_key => 'authkey'; sub new { my $class = shift; my %options = @_; my $self = $class->SUPER::new(%options); $self->{_pattern} = Sympa::Spool::build_glob_pattern($self->_marshal_format, $self->_marshal_keys, %options); $self; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Held - Spool for held messages waiting for confirmation =head1 SYNOPSIS use Sympa::Spool::Held; my $spool = Sympa::Spool::Held->new; my $authkey = $spool->store($message); my $spool = Sympa::Spool::Held->new(context => $list, authkey => $authkey); my ($message, $handle) = $spool->next; =head1 DESCRIPTION L implements the spool for held messages waiting for confirmation. =head2 Methods See also L. =over =item new ( [ context =E $list ], [ authkey =E $authkey ] ) =item next ( [ no_lock =E 1 ] ) If the pairs describing metadatas are specified, contents returned by next() are filtered by them. =item quarantine ( ) Does nothing. =item store ( $message, [ original =E $original ] ) If storing succeeded, returns authentication key. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {authkey} Authentication key generated automatically when the message is stored to spool. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queueauth Directory path of held message spool. Note: Named such by historical reason. =back =head1 SEE ALSO L, L, L, L. =head1 HISTORY L appeared on Sympa 6.2.8. =cut sympa-6.2.24/src/lib/Sympa/Spool/Incoming.pm0000644000175000017500000001003613216651447017526 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Spool::Incoming; use strict; use warnings; use Conf; use Sympa::Tools::File; use base qw(Sympa::Spool); sub _directories { return { directory => $Conf::Conf{'queue'}, bad_directory => $Conf::Conf{'queue'} . '/bad', }; } use constant _generator => 'Sympa::Message'; use constant _marshal_format => '%s@%s.%ld.%ld,%d'; use constant _marshal_keys => [qw(localpart domainpart date PID RAND)]; use constant _marshal_regexp => qr{\A([^\s\@]+)(?:\@([\w\.\-]+))?\.(\d+)\.(\w+)(?:,.*)?\z}; sub _filter { my $self = shift; my $metadata = shift; return undef unless $metadata; # - z and Z are a null priority, so file stay in queue and are # processed only if renamed by administrator return 0 if lc($metadata->{priority} || '') eq 'z'; # - Lazily seek highest priority: Messages with lower priority than # those already found are skipped. if (length($metadata->{priority} || '')) { return 0 if $self->{_highest_priority} lt $metadata->{priority}; $self->{_highest_priority} = $metadata->{priority}; } return 1; } sub _init { my $self = shift; $self->{_highest_priority} = 'z'; } sub _load { my $self = shift; my $metadatas = $self->SUPER::_load(); my %mtime = map { ($_ => Sympa::Tools::File::get_mtime($self->{directory} . '/' . $_)) } @$metadatas; return [sort { $mtime{$a} <=> $mtime{$b} } @$metadatas]; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Incoming - Spool for incoming messages =head1 SYNOPSIS use Sympa::Spool::Incoming; my $spool = Sympa::Spool::Incoming->new; $spool->store($message); my ($message, $handle) = $spool->next; =head1 DESCRIPTION L implements the spool for incoming messages. =head2 Methods See also L. =over =item next ( [ no_filter =E 1 ], [ no_lock =E 1 ] ) Order is controlled by modification time of file and delivery date, then, if C is I set, messages with possiblly higher priority are chosen and messages with lowest priority (C or C) are skipped. =item store ( $message, [ original =E $original ] ) In most cases, queue(8) program stores messages to incoming spool. Daemon such as sympa_automatic(8) uses this method to store messages. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {date} Unix time when the message would be delivered. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queue Directory path of incoming spool. =back =head1 SEE ALSO L, L, L, L. =head1 HISTORY L appeared on Sympa 6.2.5. =cut sympa-6.2.24/src/lib/Sympa/Spool/Bounce.pm0000644000175000017500000000525013216651447017200 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Spool::Bounce; use strict; use warnings; use Conf; use base qw(Sympa::Spool::Incoming); sub _directories { return { directory => $Conf::Conf{'queuebounce'}, bad_directory => $Conf::Conf{'queuebounce'} . '/bad', }; } use constant _filter => 1; use constant _init => 1; 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Bounce - Spool for incoming bounce messages =head1 SYNOPSIS use Sympa::Spool::Bounce; my $spool = Sympa::Spool::Bounce->new; my ($message, $handle) = $spool->next; =head1 DESCRIPTION L implements the spool for incoming bounce messages. =head2 Methods See also L. =over =item next ( ) Order is controlled by modification time of files and delivery date. =item store ( $message, [ original =E $original ] ) In most cases, bouncequeue(8) program stores messages to bounce spool. This method is not used in ordinal case. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {date} Unix time when the message would be delivered. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queuebounce Directory path of bounce spool. =back =head1 SEE ALSO L, L, L, L. =head1 HISTORY L appeared on Sympa 6.2.6. =cut sympa-6.2.24/src/lib/Sympa/Spool/Digest/0000755000175000017500000000000013216651447016644 5ustar rackerackesympa-6.2.24/src/lib/Sympa/Spool/Digest/Collection.pm0000644000175000017500000000626113216651447021302 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Spool::Digest::Collection; use strict; use warnings; use Conf; use Sympa::Tools::File; use base qw(Sympa::Spool); sub _directories { return {directory => $Conf::Conf{'queuedigest'},}; } sub _filter { my $self = shift; my $metadata = shift; $metadata && ref $metadata->{context} eq 'Sympa::List'; } use constant _generator => 'Sympa::Spool::Digest'; use constant _is_collection => 1; sub _load { my $self = shift; my $metadatas = $self->SUPER::_load(); my %mtime = map { ($_ => Sympa::Tools::File::get_mtime($self->{directory} . '/' . $_)) } @$metadatas; return [sort { $mtime{$a} <=> $mtime{$b} } @$metadatas]; } use constant _marshal_format => '%s@%s'; use constant _marshal_keys => [qw(localpart domainpart)]; use constant _marshal_regexp => qr{\A([^\s\@]+)(?:\@([\w\.\-]+))?\z}; 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Digest::Collection - Collection of digest spools =head1 SYNOPSIS use Sympa::Spool::Digest::Collection; my $collection = Sympa::Spool::Digest::Collection->new; my ($spool, $handle) = $collection->next; =head1 DESCRIPTION L implements the collection of L instances. =head2 Methods See also L. =over =item next ( ) Returns next instance of L. Order is controlled by modification times of spool directories. Spool directory is locked to prevent processing by multiple processes. =item quarantine ( ) Does nothing. =item remove ( $handle ) Trys to remove directory of spool. If succeeded, returns true value. Otherwise returns false value. =item store ( ) Does nothing. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queuedigest Parent directory path of digest spools. =back =head1 SEE ALSO L, L. =head1 HISTORY L appeared on Sympa 6.2.6. =cut sympa-6.2.24/src/lib/Sympa/Spool/Automatic.pm0000644000175000017500000000556713216651447017726 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Spool::Automatic; use strict; use warnings; use Conf; use base qw(Sympa::Spool::Incoming); sub _directories { return { directory => $Conf::Conf{'queueautomatic'}, bad_directory => $Conf::Conf{'queueautomatic'} . '/bad', }; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Automatic - Spool for incoming messages in automatic spool =head1 SYNOPSIS use Sympa::Spool::Automatic; my $spool = Sympa::Spool::Automatic->new; my ($message, $handle) = $spool->next; =head1 DESCRIPTION L implements the spool for incoming messages in automatic spool. =head2 Methods See also L. =over =item next ( [ no_filter =E 1 ], [ no_lock =E 1 ] ) I. Order is controlled by modification time of files and delivery date, then, if C is I set, messages with possiblly higher priority are chosen and messages with lowest priority (C or C) are skipped. =item store ( $message, [ original =E $original ] ) In most cases, familyqueue(8) program stores messages to automatic spool. This method is not used in ordinal case. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {date} Unix time when the message would be delivered. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queueautomatic Directory path of list creation spool. =back =head1 SEE ALSO L, L, L. =head1 HISTORY L appeared on Sympa 6.2.6. =cut sympa-6.2.24/src/lib/Sympa/Spool/Archive.pm0000644000175000017500000000565313216651447017355 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Spool::Archive; use strict; use warnings; use English qw(-no_match_vars); use Conf; use base qw(Sympa::Spool); sub _directories { return { directory => $Conf::Conf{'queueoutgoing'}, bad_directory => $Conf::Conf{'queueoutgoing'} . '/bad', }; } use constant _generator => 'Sympa::Message'; use constant _marshal_format => '%d.%f.%s@%s,%ld,%d'; use constant _marshal_keys => [qw(date TIME localpart domainpart PID RAND)]; use constant _marshal_regexp => qr{\A(\d+)\.(\d+\.\d+)\.([^\s\@]*)\@([\w\.\-*]*),(\d+),(\d+)}; 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Archive - Spool for messages waiting for archiving =head1 SYNOPSIS use Sympa::Spool::Archive; my $spool = Sympa::Spool::Archive->new; $spool->store($message); my ($message, $handle) = $spool->next; =head1 DESCRIPTION L implements the spool for messages waiting for archiving. =head2 Methods See also L. =over =item next ( ) Order is controlled by delivery date, then by reception date. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {date} Unix time when the message would be delivered. =item {time} Unix time in floating point number when the message was stored. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queueoutgoing Directory path of archive spool. Note: Named such by historical reason. =back =head1 SEE ALSO L, L, L, L. =head1 HISTORY L appeared on Sympa 6.2. =cut sympa-6.2.24/src/lib/Sympa/Spool/Digest.pm0000644000175000017500000001131113216651447017177 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Spool::Digest; use strict; use warnings; use Conf; use base qw(Sympa::Spool); sub new { my $class = shift; my %options = @_; return undef unless ref $options{context} eq 'Sympa::List'; $class->SUPER::new(%options); } sub _directories { my $self = shift; my %options = @_; my $list = ref($self) ? $self->{context} : $options{context}; die 'bug in logic. Ask developer' unless ref $list eq 'Sympa::List'; return { parent_directory => $Conf::Conf{'queuedigest'}, directory => $list->get_digest_spool_dir, bad_directory => $list->get_digest_spool_dir . '/bad', }; } use constant _generator => 'Sympa::Message'; sub _init { my $self = shift; my $status = shift; unless ($status) { # Get earliest time of messages in the spool. my $metadatas = $self->_load || []; my $metadata; while (my $marshalled = shift @$metadatas) { $metadata = $self->unmarshal($marshalled); last if $metadata; } $self->{time} = $metadata ? $metadata->{time} : undef; $self->{_metadatas} = undef; # Rewind cache. } return 1; } use constant _marshal_format => '%ld.%f,%ld,%d'; use constant _marshal_keys => [qw(date TIME PID RAND)]; use constant _marshal_regexp => qr{\A(\d+)\.(\d+\.\d+)(?:,.*)?\z}; sub next { my $self = shift; my ($message, $handle) = $self->SUPER::next(); if ($message) { # Assign context which is not given by metadata. $message->{context} = $self->{context}; } return ($message, $handle); } # Old name: Sympa::List::store_digest(). sub store { my $self = shift; my $message = shift->dup; # Delete original message ID because it can be anonymized. delete $message->{message_id}; return $self->SUPER::store($message); } sub get_id { my $self = shift; if ($self->{context}) { if (ref $self->{context} eq 'Sympa::List') { return $self->{context}->get_id; } else { return $self->{context}; } } else { return ''; } } 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Digest - Spool for messages waiting for digest sending =head1 SYNOPSIS use Sympa::Spool::Digest; my $spool = Sympa::Spool::Digest->new(context => $list); $spool->store($message); my ($message, $handle) = $spool->next; =head1 DESCRIPTION L implements the spool for messages waiting for digest sending. =head2 Methods See also L. =over =item new ( context =E $list ) Creates new instance of L related to the list $list. =item next ( ) Order is controlled by delivery date, then by reception date. =back =head2 Properties See also L. =over =item {time} Earliest time of messages in the spool, or C. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {date} Unix time when the message was delivered. =item {time} Unix time in floating point number when the message was stored. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queuedigest Parent directory path of digest spools. =back =head1 SEE ALSO L, L, L, L. =head1 HISTORY L appeared on Sympa 6.2.6. =cut sympa-6.2.24/src/lib/Sympa/Spool/Moderation.pm0000644000175000017500000001361013216651447020065 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # # 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, see . package Sympa::Spool::Moderation; use strict; use warnings; use Conf; use Sympa::Archive; # for html_format() use Sympa::Tools::File; use base qw(Sympa::Spool::Held); sub _directories { return { directory => $Conf::Conf{'queuemod'}, html_root_directory => $Conf::Conf{'viewmail_dir'}, html_base_directory => $Conf::Conf{'viewmail_dir'} . '/mod', }; } sub _load { my $self = shift; my $metadatas = $self->SUPER::_load(); my %mtime = map { ($_ => Sympa::Tools::File::get_mtime($self->{directory} . '/' . $_)) } @$metadatas; return [sort { $mtime{$a} <=> $mtime{$b} } @$metadatas]; } use constant _marshal_format => '%s@%s_%s%s'; use constant _marshal_keys => [qw(localpart domainpart AUTHKEY validated)]; use constant _marshal_regexp => qr{\A([^\s\@]+)\@([-.\w]+)_([\da-f]+)(.distribute)?\z}; sub remove { my $self = shift; my $handle = shift; my %options = @_; if ($options{action}) { die 'bug in logic. Ask developer' unless $options{action} eq 'distribute'; return 1 if $handle->basename =~ /[.]distribute\z/; return $handle->rename( $self->{directory} . '/' . $handle->basename . '.distribute'); } else { return $self->SUPER::remove($handle); } } sub html_remove { my $self = shift; my $message = shift; Sympa::Tools::File::remove_dir( join('/', $self->{html_base_directory}, $message->{context}->get_id, $message->{authkey}) ) if $message and $message->{authkey} and ref $message->{context} eq 'Sympa::List'; return; } sub size { scalar grep { !/[.]distribute\z/ } @{shift->_load || []}; } sub html_store { my $self = shift; my $message = shift; my $modkey = shift; if ($modkey and $modkey =~ /\A\w+\z/) { # Prepare HTML view of this message. # Note: 6.2a.32 or earlier stored HTML view into modqueue. # 6.2b has dedicated directory specified by viewmail_dir parameter. my $list_id = $message->{context}->get_id; my $listname = $message->{context}->{'name'}; Sympa::Archive::html_format( $message, destination_dir => join('/', $self->{html_base_directory}, $list_id, $modkey), attachment_url => ['viewmod', $listname, $modkey] ); } return; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::Spool::Moderation - Spool for held messages waiting for moderation =head1 SYNOPSIS use Sympa::Spool::Moderation; my $spool = Sympa::Spool::Moderation->new; my $modkey = $spool->store($message); my $spool = Sympa::Spool::Moderation->new(context => $list, authkey => $modkey); my ($message, $handle) = $spool->next; $spool->remove($handle, action => 'distribute'); $spool->remove($handle); =head1 DESCRIPTION L implements the spool for held messages waiting for moderation. =head2 Methods See also L. =over =item new ( [ context =E $list ], [ authkey =E $modkey ] ) =item next ( [ no_lock =E 1 ] ) If the pairs describing metadatas are specified, contents returned by next() are filtered by them. =item quarantine ( ) Does nothing. =item remove ( $handle, [ action => 'distribute' ] ) If action is specified, rename message file to add it as extension, instead of removing message file. Otherwise, removes message file. =item size ( ) Returns number of messages in the spool except which have extension. =item store ( $message, [ original =E $original ] ) If storing succeeded, returns moderation key. =back =head2 Methods specific to this module =over =item html_remove ( $metadata ) I. TBD. Parameters: =over =item $metadata Hashref or message containing metadata. At least C and C are required. =back Returns: None. =item html_store ( $message, $modkey ) I. Caches HTML view of message. Parameters: =over =item $message Message to be stored. =item $modkey Moderation key. =back Returns: None. =back =head2 Context and metadata See also L. This class particularly gives following metadata: =over =item {authkey} Moderation key generated automatically when the message is stored into spool. =item {validated} Keeps a string representing extension, if message has been renamed using remove() with option. =back =head1 CONFIGURATION PARAMETERS Following site configuration parameters in sympa.conf will be referred. =over =item queuemod Directory path of moderation spool. =item viewmail_dir Root directory path of directories where HTML view of messages are cached. =back =head1 SEE ALSO L, L, L, L. =head1 HISTORY L appeared on Sympa 6.2.8. =cut sympa-6.2.24/src/lib/Sympa/ListOpt.pm0000644000175000017500000003201213216651447016263 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::ListOpt; use strict; use warnings; use Sympa::Language; my $language = Sympa::Language->instance; # List parameter values except for parameters below. my %list_option = ( # reply_to_header.apply 'forced' => {'gettext_id' => 'overwrite Reply-To: header field'}, 'respect' => {'gettext_id' => 'preserve existing header field'}, # reply_to_header.value 'sender' => {'gettext_id' => 'sender'}, # reply_to_header.value, include_remote_sympa_list.cert 'list' => {'gettext_id' => 'list'}, # include_ldap_2level_query.select2, include_ldap_2level_query.select1, # include_ldap_query.select, reply_to_header.value, dmarc_protection.mode 'all' => {'gettext_id' => 'all'}, # reply_to_header.value 'other_email' => {'gettext_id' => 'other email address'}, # msg_topic_keywords_apply_on 'subject' => {'gettext_id' => 'subject field'}, 'body' => {'gettext_id' => 'message body'}, 'subject_and_body' => {'gettext_id' => 'subject and body'}, # bouncers_level2.notification, bouncers_level2.action, # bouncers_level1.notification, bouncers_level1.action, # spam_protection, dkim_signature_apply_on, web_archive_spam_protection, # dmarc_protection.mode 'none' => {'gettext_id' => 'do nothing'}, # bouncers_level2.notification, bouncers_level1.notification, # welcome_return_path, remind_return_path, rfc2369_header_fields, # archive.mail_access 'owner' => {'gettext_id' => 'owner'}, # bouncers_level2.notification, bouncers_level1.notification 'listmaster' => {'gettext_id' => 'listmaster'}, # bouncers_level2.action, bouncers_level1.action 'remove_bouncers' => {'gettext_id' => 'remove bouncing users'}, 'notify_bouncers' => {'gettext_id' => 'send notify to bouncing users'}, # pictures_feature, dkim_feature, merge_feature, # inclusion_notification_feature, tracking.delivery_status_notification, # tracking.message_disposition_notification 'on' => {'gettext_id' => 'enabled'}, 'off' => {'gettext_id' => 'disabled'}, # include_remote_sympa_list.cert 'robot' => {'gettext_id' => 'robot'}, # include_ldap_2level_query.select2, include_ldap_2level_query.select1, # include_ldap_query.select 'first' => {'gettext_id' => 'first entry'}, # include_ldap_2level_query.select2, include_ldap_2level_query.select1 'regex' => {'gettext_id' => 'entries matching regular expression'}, # include_ldap_2level_query.scope2, include_ldap_2level_query.scope1, # include_ldap_query.scope 'base' => {'gettext_id' => 'base'}, 'one' => {'gettext_id' => 'one level'}, 'sub' => {'gettext_id' => 'subtree'}, # include_ldap_query.use_tls, include_ldap_2level_query.use_tls, # include_ldap_ca.use_tls, include_ldap_2level_ca.use_tls 'starttls' => {'gettext_id' => 'use STARTTLS'}, 'ldaps' => {'gettext_id' => 'use LDAPS (LDAP over TLS)'}, ## include_ldap_2level_query.use_ssl, include_ldap_query.use_ssl #'yes' => {'gettext_id' => 'yes'}, #'no' => {'gettext_id' => 'no'}, # include_ldap_2level_query.ssl_version, include_ldap_query.ssl_version 'sslv2' => {'gettext_id' => 'SSL version 2'}, 'sslv3' => {'gettext_id' => 'SSL version 3'}, 'tlsv1' => {'gettext_id' => 'TLS version 1'}, 'tlsv1_1' => {'gettext_id' => 'TLS version 1.1'}, 'tlsv1_2' => {'gettext_id' => 'TLS version 1.2'}, # editor.reception, owner_include.reception, owner.reception, # editor_include.reception 'mail' => {'gettext_id' => 'receive notification email'}, 'nomail' => {'gettext_id' => 'no notifications'}, # editor.visibility, owner_include.visibility, owner.visibility, # editor_include.visibility 'conceal' => {'gettext_id' => 'concealed from list menu'}, 'noconceal' => {'gettext_id' => 'listed on the list menu'}, # welcome_return_path, remind_return_path 'unique' => {'gettext_id' => 'bounce management'}, # owner_include.profile, owner.profile 'privileged' => {'gettext_id' => 'privileged owner'}, 'normal' => {'gettext_id' => 'normal owner'}, # priority '0' => {'gettext_id' => '0 - highest priority'}, '9' => {'gettext_id' => '9 - lowest priority'}, 'z' => {'gettext_id' => 'queue messages only'}, # spam_protection, web_archive_spam_protection 'at' => {'gettext_id' => 'replace @ characters'}, 'javascript' => {'gettext_id' => 'use JavaScript'}, # msg_topic_tagging 'required_sender' => {'gettext_id' => 'required to post message'}, 'required_moderator' => {'gettext_id' => 'required to distribute message'}, # msg_topic_tagging, custom_attribute.optional 'optional' => {'gettext_id' => 'optional'}, # custom_attribute.optional 'required' => {'gettext_id' => 'required'}, # custom_attribute.type 'string' => {'gettext_id' => 'string'}, 'text' => {'gettext_id' => 'multi-line text'}, 'integer' => {'gettext_id' => 'number'}, 'enum' => {'gettext_id' => 'set of keywords'}, # footer_type 'mime' => {'gettext_id' => 'add a new MIME part'}, 'append' => {'gettext_id' => 'append to message body'}, # archive.mail_access 'open' => {'gettext_id' => 'open'}, 'closed' => {'gettext_id' => 'closed'}, 'private' => {'gettext_id' => 'subscribers only'}, 'public' => {'gettext_id' => 'public'}, ## ## user_data_source ## 'database' => {'gettext_id' => 'RDBMS'}, ## 'file' => {'gettext_id' => 'include from local file'}, ## 'include' => {'gettext_id' => 'include from external source'}, ## 'include2' => {'gettext_id' => 'general datasource'}, # rfc2369_header_fields 'help' => {'gettext_id' => 'help'}, 'subscribe' => {'gettext_id' => 'subscription'}, 'unsubscribe' => {'gettext_id' => 'unsubscription'}, 'post' => {'gettext_id' => 'posting address'}, 'archive' => {'gettext_id' => 'list archive'}, # dkim_signature_apply_on 'md5_authenticated_messages' => {'gettext_id' => 'authenticated by password'}, 'smime_authenticated_messages' => {'gettext_id' => 'authenticated by S/MIME signature'}, 'dkim_authenticated_messages' => {'gettext_id' => 'authenticated by DKIM signature'}, 'editor_validated_messages' => {'gettext_id' => 'approved by editor'}, 'any' => {'gettext_id' => 'any messages'}, # archive.period 'day' => {'gettext_id' => 'daily'}, 'week' => {'gettext_id' => 'weekly'}, 'month' => {'gettext_id' => 'monthly'}, 'quarter' => {'gettext_id' => 'quarterly'}, 'year' => {'gettext_id' => 'yearly'}, # web_archive_spam_protection 'cookie' => {'gettext_id' => 'use HTTP cookie'}, # verp_rate '100%' => {'gettext_id' => '100% - always'}, '0%' => {'gettext_id' => '0% - never'}, # archive_crypted_msg 'original' => {'gettext_id' => 'original messages'}, 'decrypted' => {'gettext_id' => 'decrypted messages'}, # tracking.message_disposition_notification 'on_demand' => {'gettext_id' => 'on demand'}, # dmarc_protection.mode 'dkim_signature' => {'gettext_id' => 'DKIM signature exists'}, 'dmarc_any' => {'gettext_id' => 'DMARC policy exists'}, 'dmarc_reject' => {'gettext_id' => 'DMARC policy suggests rejection'}, 'dmarc_quarantine' => {'gettext_id' => 'DMARC policy suggests quarantine'}, 'domain_regex' => {'gettext_id' => 'domain matching regular expression'}, # dmarc_protection.phrase 'display_name' => {'gettext_id' => '"Name"'}, 'name_and_email' => {'gettext_id' => '"Name" (e-mail)'}, 'name_via_list' => {'gettext_id' => '"Name" (via List)'}, 'name_email_via_list' => {'gettext_id' => '"Name" (e-mail via List)'}, 'list_for_email' => {'gettext_id' => '"List" (on behalf of e-mail)'}, 'list_for_name' => {'gettext_id' => '"List" (on behalf of Name)'}, ); # Values for subscriber reception mode. my %reception_mode = ( 'mail' => {'gettext_id' => 'standard (direct reception)'}, 'digest' => {'gettext_id' => 'digest MIME format'}, 'digestplain' => {'gettext_id' => 'digest plain text format'}, 'summary' => {'gettext_id' => 'summary mode'}, 'notice' => {'gettext_id' => 'notice mode'}, 'txt' => {'gettext_id' => 'text-only mode'}, 'urlize' => {'gettext_id' => 'urlize mode'}, 'nomail' => {'gettext_id' => 'no mail'}, 'not_me' => {'gettext_id' => 'not receiving your own posts'} ); # Values for subscriber visibility mode. my %visibility_mode = ( 'noconceal' => {'gettext_id' => 'listed in the list review page'}, 'conceal' => {'gettext_id' => 'concealed'} ); # Values for list status. my %list_status = ( 'open' => {'gettext_id' => 'in operation'}, 'pending' => {'gettext_id' => 'list not yet activated'}, 'error_config' => {'gettext_id' => 'erroneous configuration'}, 'family_closed' => {'gettext_id' => 'closed family instance'}, 'closed' => {'gettext_id' => 'closed list'}, ); # Old name: Sympa::List::get_option_title(). # Old name: Sympa::ListOpt::get_title(). sub get_option_description { my $that = shift; my $option = shift; my $type = shift || ''; my $withval = shift || 0; my $title = undef; if ($type eq 'dayofweek') { if ($option =~ /\A[0-9]+\z/) { $title = [ split /:/, $language->gettext( 'Sunday:Monday:Tuesday:Wednesday:Thursday:Friday:Saturday' ) ]->[$option % 7]; } } elsif ($type eq 'lang') { $language->push_lang; if ($language->set_lang($option)) { $title = $language->native_name; } $language->pop_lang; } elsif ($type eq 'listtopic' or $type eq 'listtopic:leaf') { my $robot_id; if (ref $that eq 'Sympa::List') { $robot_id = $that->{'domain'}; } elsif ($that and $that ne '*') { $robot_id = $that; } else { $robot_id = '*'; } if ($type eq 'listtopic') { $title = Sympa::Robot::topic_get_title($robot_id, $option); } else { $title = [Sympa::Robot::topic_get_title($robot_id, $option)]->[-1]; } } elsif ($type eq 'password') { return '*' x length($option); # return } elsif ($type eq 'unixtime') { $title = $language->gettext_strftime('%d %b %Y at %H:%M:%S', localtime $option); } else { my $map = { 'reception' => \%reception_mode, 'visibility' => \%visibility_mode, 'status' => \%list_status, }->{$type} || \%list_option; my $t = $map->{$option} || {}; if ($t->{gettext_id}) { $title = $language->gettext($t->{gettext_id}); $title =~ s/^\s+//; $title =~ s/\s+$//; } } if (defined $title) { return sprintf '%s (%s)', $title, $option if $withval; return $title; } return $option; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::ListOpt - Definition of list configuration parameter values =head1 DESCRIPTION L gives information about options used for values of list configuration. =head2 Function =over =item get_option_description ( $that, $value, [ $type, [ $withval ] ] ) I. Gets i18n-ed title of option. Language context must be set in advance (See L). Parameters: =over =item $that Context, instance of L, Robot or Site. =item $value Value of option. =item $type Type of option: field_type (see L) or other (list config option, default). =item $withval Adds value of option to returned title. =back Returns: I18n-ed title of option value. =back =head1 SEE ALSO L, L. =head1 HISTORY L appeared on Sympa 6.2.13. =cut sympa-6.2.24/src/lib/Sympa/Alarm.pm0000644000175000017500000001670513216651447015734 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # # 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, see . package Sympa::Alarm; use strict; use warnings; use Sympa; use Sympa::Bulk; use Conf; use Sympa::Log; use Sympa::Mailer; use Sympa::Message::Template; use base qw(Class::Singleton); my $log = Sympa::Log->instance; # Constructor for Class::Singleton. sub _new_instance { my $class = shift; bless { use_bulk => undef, _stack => {}, } => $class; } sub store { my $self = shift; my $message = shift; my $rcpt = shift; my %options = @_; my $mailer = $self->{use_bulk} ? Sympa::Bulk->new : Sympa::Mailer->instance; my $operation = $options{operation}; my $robot_id; if (ref $message->{context} eq 'Sympa::List') { $robot_id = $message->{context}->{'domain'}; } elsif ($message->{context} and $message->{context} ne '*') { $robot_id = $message->{context}; } else { $robot_id = '*'; } $self->{_stack}->{$robot_id}{$operation}{'first'} = time unless $self->{_stack}->{$robot_id}{$operation}{'first'}; $self->{_stack}->{$robot_id}{$operation}{'counter'}++; $self->{_stack}->{$robot_id}{$operation}{'last'} = time; if ($self->{_stack}->{$robot_id}{$operation}{'counter'} > 3) { my @rcpts = ref $rcpt ? @$rcpt : ($rcpt); # stack if too much messages w/ same code $log->syslog('info', 'Stacking message about "%s" for %s (%s)', $operation, join(', ', @rcpts), $robot_id) unless $operation eq 'logs_failed'; foreach my $rcpt (@rcpts) { push @{$self->{_stack}->{$robot_id}{$operation}{'messages'}{$rcpt} }, $message->as_string; } return 1; } else { # Overwrite envelope sender $message->{envelope_sender} = Sympa::get_address($robot_id, 'owner'); #FIXME: Priority would better to be '0', isn't it? $message->{priority} = Conf::get_robot_conf($robot_id, 'sympa_priority'); return $mailer->store($message, $rcpt); } } sub flush { my $self = shift; my %options = @_; my $mailer = $self->{use_bulk} ? Sympa::Bulk->new : Sympa::Mailer->instance; my $purge = $options{purge}; foreach my $robot_id (keys %{$self->{_stack}}) { foreach my $operation (keys %{$self->{_stack}->{$robot_id}}) { my $first_age = time - $self->{_stack}->{$robot_id}{$operation}{'first'}; my $last_age = time - $self->{_stack}->{$robot_id}{$operation}{'last'}; # not old enough to send and first not too old next unless $purge or $last_age > 30 or $first_age > 60; next unless $self->{_stack}->{$robot_id}{$operation}{'messages'}; my %messages = %{$self->{_stack}->{$robot_id}{$operation}{'messages'}}; $log->syslog( 'info', 'Got messages about "%s" (%s)', $operation, join(', ', keys %messages) ); ##### bulk send foreach my $rcpt (keys %messages) { my $param = { to => $rcpt, auto_submitted => 'auto-generated', operation => $operation, notification_messages => $messages{$rcpt}, boundary => '----------=_' . Sympa::unique_message_id($robot_id) }; $log->syslog('info', 'Send messages to %s', $rcpt); # Skip DB access because DB is not accessible $rcpt = [$rcpt] if $operation eq 'missing_dbd' or $operation eq 'no_db' or $operation eq 'db_restored'; my $message = Sympa::Message::Template->new( context => $robot_id, template => 'listmaster_groupednotifications', rcpt => $rcpt, data => $param ); unless ($message) { $log->syslog( 'notice', 'Unable to send template "listmaster_groupnotification" to %s listmaster %s', $robot_id, $rcpt ) unless $operation eq 'logs_failed'; return undef; } unless (defined $mailer->store($message, $rcpt)) { $log->syslog( 'notice', 'Unable to send template "listmaster_groupnotification" to %s listmaster %s', $robot_id, $rcpt ) unless $operation eq 'logs_failed'; return undef; } } $log->syslog('info', 'Cleaning stacked notifications'); delete $self->{_stack}->{$robot_id}{$operation}; } } return 1; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::Alarm - Spool on memory for listmaster notification =head1 SYNOPSIS use Sympa::Alarm; my $alarm = Sympa::Alarm->instance; $alarm->store($message, $rcpt, $operation); $alarm->flush(); $alarm->flush(purge => 1); =head1 DESCRIPTION L implements on-memory spool for listmaster notification. =head2 Methods =over =item instance ( ) I. Creates a singleton instance of L object. Returns: A new L instance, or undef for failure. =item store ( $message, $rcpt, operation => $operation ) I. Stores a message of a operation to spool. Parameters: =over =item $message L object to be stored. =item $rcpt Arrayref or scalar. Recipient of notification. =item operation => $operation A string specifys tag of the message. =back Returns: True value if succeed, otherwise C. =item flush ( [ purge => $purge ] ) I. Sends compiled messages in spool. If true value is given as optional argument, all messages in spool will be sent. =back =head2 Attribute The instance of L has following attribute. =over =item {use_bulk} If set to be true, messages to be sent will be stored into spool instead of being stored to sendmail. Default is false. =back =head1 HISTORY Feature to compile notification to listmaster in group appeared on Sympa 6.2. L appeared on Sympa 6.2. =cut sympa-6.2.24/src/lib/Sympa/SOAP/0000755000175000017500000000000013216651447015073 5ustar rackerackesympa-6.2.24/src/lib/Sympa/SOAP/Transport.pm0000644000175000017500000000777513216651447017445 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::SOAP::Transport; use strict; use warnings; use English qw(-no_match_vars); use SOAP::Transport::HTTP; use Sympa::Log; use Sympa::Session; use Sympa::Tools::WWW; # 'base' pragma doesn't work here our @ISA = qw(SOAP::Transport::HTTP::FCGI); my $log = Sympa::Log->instance; sub new { my $class = shift; return $class if ref $class; my %options = @_; my $self = $class->SUPER::new(); $self->{_ss_birthday} = [stat $PROGRAM_NAME]->[9] if $PROGRAM_NAME; $self->{_ss_cookie_expire} = $options{cookie_expire} || 0; $self; } sub request { my $self = shift; if (my $request = $_[0]) { # Select appropriate robot. $ENV{'SYMPA_ROBOT'} = Sympa::Tools::WWW::get_robot('soap_url_local', 'soap_url'); my $session; ## Existing session or new one if (Sympa::Session::get_session_cookie($ENV{'HTTP_COOKIE'})) { $session = Sympa::Session->new( $ENV{'SYMPA_ROBOT'}, { 'cookie' => Sympa::Session::get_session_cookie( $ENV{'HTTP_COOKIE'} ) } ); } else { $session = Sympa::Session->new($ENV{'SYMPA_ROBOT'}, {}); $session->store() if (defined $session); ## Note that id_session changes each time it is saved in the DB $session->renew() if (defined $session); } delete $ENV{'USER_EMAIL'}; if (defined $session) { $ENV{'SESSION_ID'} = $session->{'id_session'}; if ($session->{'email'} ne 'nobody') { $ENV{'USER_EMAIL'} = $session->{'email'}; } } } $self->SUPER::request(@_); } sub response { my $self = shift; if (my $response = $_[0]) { if (defined $ENV{'SESSION_ID'}) { my $cookie = Sympa::Session::soap_cookie2($ENV{'SESSION_ID'}, $ENV{'SERVER_NAME'}, $self->{_ss_cookie_expire}); $response->headers->push_header('Set-Cookie2' => $cookie); } } $self->SUPER::request(@_); } ## Redefine FCGI's handle subroutine sub handle { my $self = shift->new; my ($r1, $r2); my $fcgirq = $self->{_fcgirq}; while (($r1 = $fcgirq->Accept()) >= 0) { $r2 = $self->SOAP::Transport::HTTP::CGI::handle; # Exit if script itself has changed. my $birthday = $self->{_ss_birthday}; if (defined $birthday and $PROGRAM_NAME) { my $age = [stat $PROGRAM_NAME]->[9]; if (defined $age and $birthday != $age) { $log->syslog( 'notice', 'Exiting because %s has changed since FastCGI server started', $PROGRAM_NAME ); exit(0); } } } return undef; } 1; sympa-6.2.24/src/lib/Sympa/DatabaseDescription.pm0000644000175000017500000012674613216651447020617 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::DatabaseDescription; use strict; use warnings; use Sympa::Constants; my $email_struct = sprintf 'varchar(%d)', Sympa::Constants::EMAIL_LEN(); my $family_struct = sprintf 'varchar(%d)', Sympa::Constants::FAMILY_LEN(); my $list_struct = sprintf 'varchar(%d)', Sympa::Constants::LIST_LEN(); my $robot_struct = sprintf 'varchar(%d)', Sympa::Constants::ROBOT_LEN(); my $list_id_struct = sprintf 'varchar(%d)', Sympa::Constants::LIST_LEN() + 1 + Sympa::Constants::ROBOT_LEN(); my %full_db_struct = ( 'subscriber_table' => { 'fields' => { 'user_subscriber' => { 'struct' => $email_struct, 'doc' => 'email of subscriber', 'primary' => 1, 'not_null' => 1, 'order' => 1 }, 'list_subscriber' => { 'struct' => $list_struct, 'doc' => 'list name of a subscription', 'primary' => 1, 'not_null' => 1, 'order' => 2 }, 'robot_subscriber' => { 'struct' => $robot_struct, 'doc' => 'robot (domain) of the list', 'primary' => 1, 'not_null' => 1, 'order' => 3 }, 'reception_subscriber' => { 'struct' => 'varchar(20)', 'doc' => 'reception format option of subscriber (digest, summary, etc.)', 'order' => 4, }, 'suspend_subscriber' => { 'struct' => 'int(1)', 'doc' => 'boolean set to 1 if subscription is suspended', 'order' => 5, }, 'suspend_start_date_subscriber' => { 'struct' => 'int(11)', 'doc' => 'the Unix time when message reception is suspended', 'order' => 6, }, 'suspend_end_date_subscriber' => { 'struct' => 'int(11)', 'doc' => 'the Unix time when message reception should be restored', 'order' => 7, }, 'bounce_subscriber' => { 'struct' => 'varchar(35)', 'doc' => 'FIXME', 'order' => 8, }, 'bounce_score_subscriber' => { 'struct' => 'smallint(6)', 'doc' => 'FIXME', 'order' => 9, }, 'bounce_address_subscriber' => { 'struct' => $email_struct, 'doc' => 'FIXME', 'order' => 10, }, 'date_subscriber' => { 'struct' => 'datetime', 'doc' => 'date of subscription', 'not_null' => 1, 'order' => 11, }, 'update_subscriber' => { 'struct' => 'datetime', 'doc' => 'the latest date where subscription is confirmed by subscriber', 'order' => 12, }, 'comment_subscriber' => { 'struct' => 'varchar(150)', 'doc' => 'free form name', 'order' => 13, }, 'number_messages_subscriber' => { 'struct' => 'int(5)', 'doc' => 'the number of message the subscriber sent', 'not_null' => 1, 'order' => 5, 'order' => 14, }, 'visibility_subscriber' => { 'struct' => 'varchar(20)', 'doc' => 'FIXME', 'order' => 15, }, 'topics_subscriber' => { 'struct' => 'varchar(200)', 'doc' => 'topic subscription specification', 'order' => 16, }, 'subscribed_subscriber' => { 'struct' => 'int(1)', 'doc' => 'boolean set to 1 if subscriber comes from ADD or SUB', 'order' => 17, }, 'included_subscriber' => { 'struct' => 'int(1)', 'doc' => 'boolean, set to 1 is subscriber comes from an external datasource. Note that included_subscriber and subscribed_subscriber can both value 1', 'order' => 18, }, 'include_sources_subscriber' => { 'struct' => 'varchar(50)', 'doc' => 'comma separated list of datasource that contain this subscriber', 'order' => 19, }, 'custom_attribute_subscriber' => { 'struct' => 'text', 'doc' => 'FIXME', 'order' => 20, }, }, 'doc' => 'This table store subscription, subscription option etc.', 'order' => 1, }, 'user_table' => { 'fields' => { 'email_user' => { 'struct' => $email_struct, 'doc' => 'email of user', 'primary' => 1, 'not_null' => 1, 'order' => 1, }, 'gecos_user' => { 'struct' => 'varchar(150)', 'doc' => 'display name of user', 'order' => 3, }, 'password_user' => { 'struct' => 'varchar(40)', 'doc' => 'password are stored as fringer print', 'order' => 2, }, 'last_login_date_user' => { 'struct' => 'int(11)', 'doc' => 'Unix time of last login, printed in login result for security purpose', 'order' => 4, }, 'last_login_host_user' => { 'struct' => 'varchar(60)', 'doc' => 'host of last login, printed in login result for security purpose', 'order' => 5, }, 'wrong_login_count_user' => { 'struct' => 'int(11)', 'doc' => 'login attempt count, used to prevent brute force attack', 'order' => 6, }, 'last_active_date_user' => { 'struct' => 'int(11)', 'doc' => 'the last Unix time when this user was confirmed their activity by purge_user_table task', 'order' => 7, }, 'cookie_delay_user' => { 'struct' => 'int(11)', 'doc' => 'FIXME', 'order' => 8, }, 'lang_user' => { 'struct' => 'varchar(10)', 'doc' => 'user language preference', 'order' => 9, }, 'attributes_user' => { 'struct' => 'text', 'doc' => 'FIXME', 'order' => 10, }, 'data_user' => { 'struct' => 'text', 'doc' => 'FIXME', 'order' => 11, }, }, 'doc' => 'The user_table is mainly used to manage login from web interface. A subscriber may not appear in the user_table if he never log through the web interface.', 'order' => 2, }, #'bulkspool_table' => { # 'fields' => { # 'messagekey_bulkspool' => { # 'struct' => 'varchar(33)', # 'doc' => 'primary key', # 'primary' => 1, # 'not_null' => 1, # 'order' => 1, # }, # 'message_bulkspool' => { # 'struct' => 'longtext', # 'doc' => 'message as string b64 encoded', # 'order' => 2, # }, # #'messageid_bulkspool' => { # # 'struct' => 'varchar(300)', # # 'doc' => 'stored to list spool content faster', # # 'order' => 4, # #}, # 'lock_bulkspool' => { # 'struct' => 'int(1)', # 'doc' => # 'when set to 1, this field prevents Sympa from processing the message', # 'order' => 5, # }, # #'dkim_privatekey_bulkspool' => { # # 'struct' => 'varchar(2000)', # # 'doc' => # # 'DKIM parameter stored for bulk daemon because bulk ignore list parameters, private key to sign message', # # 'order' => 6, # #}, # #'dkim_selector_bulkspool' => { # # 'struct' => 'varchar(50)', # # 'doc' => # # 'DKIM parameter stored for bulk daemon because bulk ignore list parameters, DKIM selector to sign message', # # 'order' => 7, # #}, # #'dkim_d_bulkspool' => { # # 'struct' => 'varchar(50)', # # 'doc' => # # 'DKIM parameter stored for bulk daemon because bulk ignore list parameters, the d DKIM parameter', # # 'order' => 8, # #}, # #'dkim_i_bulkspool' => { # # 'struct' => $email_struct, # # 'doc' => # # 'DKIM parameter stored for bulk daemon because bulk ignore list parameters, DKIM i signature parameter', # # 'order' => 9, # #}, # }, # 'doc' => 'This table contains the messages to be sent by bulk.pl', # 'order' => 3, #}, #'bulkmailer_table' => { # 'fields' => { # 'messagekey_bulkmailer' => { # 'struct' => 'varchar(80)', # 'doc' => # 'A pointer to a message in spool_table.It must be a value of a line in table spool_table with same value as messagekey_bulkspool', # 'primary' => 1, # 'not_null' => 1, # 'order' => 1, # }, # 'packetid_bulkmailer' => { # 'struct' => 'varchar(33)', # 'doc' => 'An id for the packet', # 'primary' => 1, # 'not_null' => 1, # 'order' => 2, # }, # #'messageid_bulkmailer' => { # # 'struct' => 'varchar(200)', # # 'doc' => 'The message Id', # # 'order' => 3, # #}, # ##FIXME: column name is "recEipients_bulkmailer" # 'receipients_bulkmailer' => { # 'struct' => 'text', # 'doc' => # 'the comma separated list of recipient email for this message', # 'order' => 4, # }, # #'returnpath_bulkmailer' => { # # 'struct' => $email_struct, # # 'doc' => # # 'the return path value that must be set when sending the message', # # 'order' => 5, # #}, # 'robot_bulkmailer' => { # 'struct' => $robot_struct, # 'doc' => '', # 'order' => 6, # }, # 'listname_bulkmailer' => { # 'struct' => $list_struct, # 'doc' => '', # 'order' => 7, # }, # #'verp_bulkmailer' => { # # 'struct' => 'int(1)', # # 'doc' => # # 'A boolean to specify if VERP is requiered, in this case return_path will be formatted using VERP form', # # 'order' => 8, # #}, # #'tracking_bulkmailer' => { # # 'struct' => "enum('mdn','dsn')", # # 'doc' => 'Is DSN or MDN requiered when sending this message?', # # 'order' => 9, # #}, # #'merge_bulkmailer' => { # # 'struct' => 'int(1)', # # 'doc' => # # 'Boolean, if true, the message is to be parsed as a TT2 template foreach recipient', # # 'order' => 10, # #}, # 'priority_message_bulkmailer' => { # 'struct' => 'smallint(10)', # 'doc' => 'FIXME', # 'order' => 11, # }, # 'priority_packet_bulkmailer' => { # 'struct' => 'smallint(10)', # 'doc' => 'FIXME', # 'order' => 12, # }, # 'reception_date_bulkmailer' => { # 'struct' => 'double', # 'doc' => 'The date where the message was received', # 'order' => 13, # }, # 'delivery_date_bulkmailer' => { # 'struct' => 'int(11)', # 'doc' => 'The date the message was sent', # 'order' => 14, # }, # 'lock_bulkmailer' => { # 'struct' => 'varchar(30)', # 'doc' => # 'A lock. It is set as process-number @ hostname so multiple bulkmailer can handle this spool', # 'order' => 15, # }, # 'tag_bulkmailer' => { # 'struct' => 'varchar(10)', # 'doc' => 'Additional tag used to sort packets', # 'order' => 16, # }, # }, # 'doc' => # 'storage of recipients with a ref to a message in spool_table. So a very simple process can distribute them', # 'order' => 4, #}, 'exclusion_table' => { 'fields' => { 'list_exclusion' => { # "family:" and family name. 'struct' => sprintf( 'varchar(%d)', Sympa::Constants::FAMILY_LEN() + 7 ), 'doc' => 'FIXME', 'order' => 1, 'primary' => 1, 'not_null' => 1, }, 'robot_exclusion' => { 'struct' => $robot_struct, 'doc' => 'FIXME', 'order' => 2, 'primary' => 1, 'not_null' => 1, }, 'user_exclusion' => { 'struct' => $email_struct, 'doc' => 'FIXME', 'order' => 3, 'primary' => 1, 'not_null' => 1, }, 'family_exclusion' => { 'struct' => $family_struct, 'doc' => 'FIXME', 'primary' => 1, 'order' => 4, }, 'date_exclusion' => { 'struct' => 'int(11)', 'doc' => 'FIXME', 'order' => 5, }, }, 'doc' => 'Exclusion table is used in order to manage unsubscription for subsceriber included from an external data source.', 'order' => 5, }, 'inclusion_table' => { 'fields' => { 'target_inclusion' => { 'struct' => $list_id_struct, 'doc' => 'list ID of including list', 'order' => 1, 'primary' => 1, 'not_null' => 1, }, 'role_inclusion' => { 'struct' => "enum('member','owner','editor')", 'doc' => 'role of included user', 'order' => 2, 'primary' => 1, 'not_null' => 1, }, 'source_inclusion' => { 'struct' => $list_id_struct, 'doc' => 'list ID of included list', 'order' => 3, 'primary' => 1, 'not_null' => 1, }, 'update_epoch_inclusion' => { 'struct' => 'int(11)', 'doc' => 'the date this entry was created or updated', 'order' => 4, }, }, 'doc' => 'Inclusion table is used in order to manage lists included from / including subscribers of other lists.', 'order' => 4, }, 'session_table' => { 'fields' => { 'id_session' => { 'struct' => 'varchar(30)', 'doc' => 'the identifier of the database record', 'primary' => 1, 'not_null' => 1, 'order' => 1, }, 'prev_id_session' => { 'struct' => 'varchar(30)', 'doc' => 'previous identifier of the database record', 'order' => 2, }, 'start_date_session' => { 'struct' => 'int(11)', 'doc' => 'the date when the session was created', 'not_null' => 1, 'order' => 3, }, 'date_session' => { 'struct' => 'int(11)', 'doc' => 'Unix time of the last use of this session. It is used in order to expire old sessions', 'not_null' => 1, 'order' => 4, }, 'refresh_date_session' => { 'struct' => 'int(11)', 'doc' => 'Unix time of the last refresh of this session. It is used in order to refresh available sessions', 'order' => 5, }, 'remote_addr_session' => { 'struct' => 'varchar(60)', 'doc' => 'the IP address of the computer from which the session was created', 'order' => 6, }, 'robot_session' => { 'struct' => $robot_struct, 'doc' => 'the virtual host in which the session was created', 'order' => 7, }, 'email_session' => { 'struct' => $email_struct, 'doc' => 'the email associated to this session', 'order' => 8, }, 'hit_session' => { 'struct' => 'int(11)', 'doc' => 'the number of hit performed during this session. Used to detect crawlers', 'order' => 9, }, 'data_session' => { 'struct' => 'text', 'doc' => 'parameters attached to this session that don\'t have a dedicated column in the database', 'order' => 10, }, }, 'doc' => 'Management of HTTP session.', 'order' => 6, }, 'one_time_ticket_table' => { 'fields' => { 'ticket_one_time_ticket' => { 'struct' => 'varchar(30)', 'doc' => 'FIXME', 'primary' => 1, 'order' => 1, }, 'email_one_time_ticket' => { 'struct' => $email_struct, 'doc' => 'FIXME', 'order' => 2, }, 'robot_one_time_ticket' => { 'struct' => $robot_struct, 'doc' => 'FIXME', 'order' => 3, }, 'date_one_time_ticket' => { 'struct' => 'int(11)', 'doc' => 'FIXME', 'order' => 4, }, 'data_one_time_ticket' => { 'struct' => 'varchar(200)', 'doc' => 'FIXME', 'order' => 5, }, 'remote_addr_one_time_ticket' => { 'struct' => 'varchar(60)', 'doc' => 'FIXME', 'order' => 6, }, 'status_one_time_ticket' => { 'struct' => 'varchar(60)', 'doc' => 'FIXME', 'order' => 7, }, }, 'doc' => 'One time ticket are random value used for authentication challenge. A ticket is associated with a context which look like a session.', 'order' => 7, }, 'notification_table' => { 'fields' => { 'pk_notification' => { 'struct' => 'bigint(20)', 'doc' => 'autoincrement key', 'autoincrement' => 1, 'primary' => 1, 'not_null' => 1, 'order' => 1, }, 'message_id_notification' => { 'struct' => 'varchar(100)', 'doc' => 'initial message-id. This field is used to search DSN and MDN related to a particular message', 'order' => 2, }, 'recipient_notification' => { 'struct' => $email_struct, 'doc' => 'email address of recipient for which a DSN or MDN was received', 'order' => 3, }, 'reception_option_notification' => { 'struct' => 'varchar(20)', 'doc' => 'the subscription option of the subscriber when the related message was sent to the list. Useful because some recipient may have option such as //digest// or //nomail//', 'order' => 4, }, 'status_notification' => { 'struct' => 'varchar(100)', 'doc' => 'value of notification', 'order' => 5, }, 'arrival_date_notification' => { 'struct' => 'varchar(80)', 'doc' => 'reception date of latest DSN or MDN', 'order' => 6, }, 'arrival_epoch_notification' => { 'struct' => 'int(11)', 'doc' => 'reception date of latest DSN or MDN', 'order' => 7, }, 'type_notification' => { 'struct' => "enum('DSN', 'MDN')", 'doc' => 'type of the notification (DSN or MDN)', 'order' => 8, }, 'list_notification' => { 'struct' => $list_struct, 'doc' => 'the listname the message was issued for', 'order' => 9, }, 'robot_notification' => { 'struct' => $robot_struct, 'doc' => 'the robot the message is related to', 'order' => 10, }, 'date_notification' => { 'struct' => 'int(11)', 'doc' => 'FIXME', 'not_null' => 1, 'order' => 11, }, }, 'doc' => 'Used for message tracking feature. If the list is configured for tracking, outgoing messages include a delivery status notification request and optionally a message disposition notification request. When DSN and MDN are received by Sympa, they are stored in this table in relation with the related list and message ID.', 'order' => 8, }, 'logs_table' => { 'fields' => { #'id_logs' => { # 'struct' => 'bigint(20)', # 'doc' => 'unique log\'s identifier', # 'primary' => 1, # 'not_null' => 1, # 'order' => 1, #}, 'user_email_logs' => { 'struct' => $email_struct, 'doc' => 'e-mail address of the message sender or email of identified web interface user (or soap user)', 'order' => 2, }, 'date_logs' => { 'struct' => 'int(11)', 'doc' => 'date when the action was executed', 'not_null' => 1, 'order' => 3, }, 'usec_logs' => { 'struct' => 'int(6)', 'doc' => 'subsecond in microsecond when the action was executed', 'order' => 3.5, }, 'robot_logs' => { 'struct' => $robot_struct, 'doc' => 'name of the robot in which context the action was executed', 'order' => 4, }, 'list_logs' => { 'struct' => $list_struct, 'doc' => 'name of the mailing-list in which context the action was executed', 'order' => 5, }, 'action_logs' => { 'struct' => 'varchar(50)', 'doc' => 'name of the Sympa subroutine which initiated the log', 'not_null' => 1, 'order' => 6, }, 'parameters_logs' => { 'struct' => 'varchar(100)', 'doc' => 'comma-separated list of parameters. The amount and type of parameters can differ from an action to another', 'order' => 7, }, 'target_email_logs' => { 'struct' => $email_struct, 'doc' => 'e-mail address (if any) targeted by the message', 'order' => 8, }, 'msg_id_logs' => { 'struct' => 'varchar(255)', 'doc' => 'identifier of the message which triggered the action', 'order' => 9, }, 'status_logs' => { 'struct' => 'varchar(10)', 'doc' => 'exit status of the action. If it was an error, it is likely that the error_type_logs field will contain a description of this error', 'not_null' => 1, 'order' => 10, }, 'error_type_logs' => { 'struct' => 'varchar(150)', 'doc' => 'name of the error string - if any - issued by the subroutine', 'order' => 11, }, 'client_logs' => { 'struct' => 'varchar(100)', 'doc' => 'IP address of the client machine from which the message was sent', 'order' => 12, }, 'daemon_logs' => { 'struct' => 'varchar(10)', 'doc' => 'name of the Sympa daemon which ran the action', 'not_null' => 1, 'order' => 13, }, }, 'doc' => 'Each important event is stored in this table. List owners and listmaster can search entries in this table using web interface.', 'order' => 9, }, 'stat_table' => { 'fields' => { #'id_stat' => { # 'struct' => 'bigint(20)', # 'doc' => 'FIXME', # 'order' => 1, # 'primary' => 1, # 'not_null' => 1, #}, 'date_stat' => { 'struct' => 'int(11)', 'doc' => 'FIXME', 'order' => 2, 'not_null' => 1, }, 'email_stat' => { 'struct' => $email_struct, 'doc' => 'FIXME', 'order' => 3, }, 'operation_stat' => { 'struct' => 'varchar(50)', 'doc' => 'FIXME', 'order' => 4, 'not_null' => 1, }, 'list_stat' => { 'struct' => $list_struct, 'doc' => 'FIXME', 'order' => 5, }, 'daemon_stat' => { 'struct' => 'varchar(20)', 'doc' => 'FIXME', 'order' => 6, }, 'user_ip_stat' => { 'struct' => 'varchar(100)', 'doc' => 'FIXME', 'order' => 7, }, 'robot_stat' => { 'struct' => $robot_struct, 'doc' => 'FIXME', 'order' => 8, 'not_null' => 1, }, 'parameter_stat' => { 'struct' => 'varchar(50)', 'doc' => 'FIXME', 'order' => 9, }, 'read_stat' => { 'struct' => 'tinyint(1)', 'doc' => 'FIXME', 'order' => 10, 'not_null' => 1, }, }, 'doc' => 'Statistics item are stored in this table, Sum average and so on are stored in stat_counter_table.', 'order' => 10, }, 'stat_counter_table' => { 'fields' => { #'id_counter' => { # 'struct' => 'bigint(20)', # 'doc' => 'FIXME', # 'order' => 1, # 'primary' => 1, # 'not_null' => 1, #}, 'beginning_date_counter' => { 'struct' => 'int(11)', 'doc' => 'FIXME', 'order' => 2, 'not_null' => 1, }, 'end_date_counter' => { 'struct' => 'int(11)', 'doc' => 'FIXME', 'order' => 1, }, 'data_counter' => { 'struct' => 'varchar(50)', 'doc' => 'FIXME', 'not_null' => 1, 'order' => 3, }, 'robot_counter' => { 'struct' => $robot_struct, 'doc' => 'FIXME', 'not_null' => 1, 'order' => 4, }, 'list_counter' => { 'struct' => $list_struct, 'doc' => 'FIXME', 'order' => 5, }, #'variation_counter' => { # 'struct' => 'int', # 'doc' => 'FIXME', # 'order' => 6, #}, #'total_counter' => { # 'struct' => 'int', # 'doc' => 'FIXME', # 'order' => 7, #}, 'count_counter' => { 'struct' => 'int', 'doc' => 'FIXME', 'order' => 8, }, }, 'doc' => 'Used in conjunction with stat_table for users statistics.', 'order' => 11, }, 'admin_table' => { 'fields' => { 'user_admin' => { 'struct' => $email_struct, 'primary' => 1, 'not_null' => 1, 'doc' => 'list admin email', 'order' => 1, }, 'list_admin' => { 'struct' => $list_struct, 'primary' => 1, 'not_null' => 1, 'doc' => 'list name', 'order' => 2, }, 'robot_admin' => { 'struct' => $robot_struct, 'primary' => 1, 'not_null' => 1, 'doc' => 'list domain', 'order' => 3, }, 'role_admin' => { 'struct' => "enum('listmaster','owner','editor')", 'primary' => 1, 'doc' => 'a role of this user for this list (editor, owner or listmaster which a kind of list owner too)', 'order' => 4, }, 'profile_admin' => { 'struct' => "enum('privileged','normal')", 'doc' => 'privilege level for this owner, value //normal// or //privileged//. The related privilege are listed in editlist.conf. ', 'order' => 5, }, 'date_admin' => { 'struct' => 'datetime', 'doc' => 'date this user become a list admin', 'not_null' => 1, 'order' => 6, }, 'update_admin' => { 'struct' => 'datetime', 'doc' => 'last update timestamp', 'order' => 7, }, 'reception_admin' => { 'struct' => 'varchar(20)', 'doc' => 'email reception option for list management messages', 'order' => 8, }, 'visibility_admin' => { 'struct' => 'varchar(20)', 'doc' => 'admin user email can be hidden in the list web page description', 'order' => 9, }, 'comment_admin' => { 'struct' => 'varchar(150)', 'doc' => 'FIXME', 'order' => 10, }, 'subscribed_admin' => { 'struct' => 'int(1)', 'doc' => 'set to 1 if user is list admin by definition in list config file', 'order' => 11, }, 'included_admin' => { 'struct' => 'int(1)', 'doc' => 'set to 1 if user is admin by an external data source', 'order' => 12, }, 'include_sources_admin' => { 'struct' => 'varchar(50)', 'doc' => 'name of external datasource', 'order' => 13, }, 'info_admin' => { 'struct' => 'varchar(150)', 'doc' => 'private information usually dedicated to listmasters who needs some additional information about list owners', 'order' => 14, }, }, 'doc' => 'This table is an internal cash where list admin roles are stored. It is just a cash and it does not need to be saved. You may remove its content if needed. It will just make next Sympa startup slower.', 'order' => 12, }, 'netidmap_table' => { 'fields' => { 'netid_netidmap' => { 'struct' => 'varchar(100)', 'doc' => 'FIXME', 'primary' => 1, 'not_null' => 1, 'order' => 1, }, 'serviceid_netidmap' => { 'struct' => 'varchar(100)', 'doc' => 'FIXME', 'primary' => 1, 'not_null' => 1, 'order' => 2, }, 'email_netidmap' => { 'struct' => $email_struct, 'doc' => 'FIXME', 'order' => 4, }, 'robot_netidmap' => { 'struct' => $robot_struct, 'doc' => 'FIXME', 'primary' => 1, 'not_null' => 1, 'order' => 3, }, }, 'order' => 13, 'doc' => 'FIXME', }, 'conf_table' => { 'fields' => { 'robot_conf' => { 'struct' => $robot_struct, 'doc' => 'FIXME', 'primary' => 1, 'order' => 1, }, 'label_conf' => { 'struct' => 'varchar(80)', 'doc' => 'FIXME', 'primary' => 1, 'order' => 2, }, 'value_conf' => { 'struct' => 'varchar(300)', 'doc' => 'the value of parameter //label_conf// of robot //robot_conf//.', 'order' => 3, }, }, 'doc' => 'FIXME', 'order' => 14, }, 'list_table' => { 'fields' => { ## Identification 'name_list' => => { 'struct' => $list_struct, 'doc' => 'name of the list', 'order' => 1, 'primary' => 1, 'not_null' => 1, }, 'robot_list' => { 'struct' => $robot_struct, 'doc' => 'name of the robot (domain) the list belongs to', 'order' => 2, 'primary' => 1, 'not_null' => 1, }, ## basic profile 'family_list' => { 'struct' => $family_struct, 'doc' => 'name of the family the list belongs to', 'order' => 3, }, 'status_list' => { 'struct' => "enum('open','closed','pending','error_config','family_closed')", 'doc' => 'status of the list', 'order' => 4, }, 'creation_email_list' => { 'struct' => $email_struct, 'doc' => 'email of user who created the list', 'order' => 5, }, 'creation_epoch_list' => { 'struct' => 'int(11)', 'doc' => 'UNIX time when the list was created', 'order' => 6, }, 'update_email_list' => { 'struct' => $email_struct, 'doc' => 'email of user who updated the list', 'order' => 7, }, 'update_epoch_list' => { 'struct' => 'int(11)', 'doc' => 'UNIX time when the list was updated', 'order' => 8, }, ## Other indices to help searching lists 'searchkey_list' => { 'struct' => 'varchar(255)', 'doc' => 'case-folded list subject to help searching', 'order' => 10, }, 'web_archive_list' => { 'struct' => 'tinyint(1)', 'doc' => 'if the list has archives', 'order' => 11, }, 'topics_list' => { 'struct' => 'varchar(255)', 'doc' => 'topics of the list, separated and enclosed by commas', 'order' => 12, }, ## total cache 'total_list' => { 'struct' => 'int(7)', 'doc' => 'estimated number of subscribers', 'order' => 90, }, # ## cache management # 'cache_epoch_list' => { # 'struct' => 'int(11)', # 'doc' => 'UNIX time of cache entry', # 'order' => 98, # }, # ## admin cache # 'config_list' => { # 'struct' => 'mediumblob', # 'doc' => 'Serialized list config', # 'order' => 99, # }, }, 'doc' => 'The list_table holds cached list config and some items to help searching lists.', 'order' => 18, }, ); sub full_db_struct { return %full_db_struct; } # OBSOLETED. Use Sympa::DatabaseManager::_db_struct(). #sub db_struct; sub not_null { my %not_null; my %full_db_struct = full_db_struct(); foreach my $table (keys %full_db_struct) { foreach my $field (keys %{$full_db_struct{$table}{'fields'}}) { $not_null{'$field'} = $full_db_struct{$table}{'fields'}{$field}{'not_null'}; } } return %not_null; } sub autoincrement { my %autoincrement; my %full_db_struct = full_db_struct(); foreach my $table (keys %full_db_struct) { foreach my $field (keys %{$full_db_struct{$table}{'fields'}}) { $autoincrement{$table} = $field if ( $full_db_struct{$table}{'fields'}{$field}{'autoincrement'}); } } return %autoincrement; } sub primary { my %primary; my %full_db_struct = full_db_struct(); foreach my $table (keys %full_db_struct) { my @primarykey; foreach my $field (keys %{$full_db_struct{$table}{'fields'}}) { push(@primarykey, $field) if ($full_db_struct{$table}{'fields'}{$field}{'primary'}); } $primary{$table} = \@primarykey; } return %primary; } ## List the required INDEXES ## 1st key is the concerned table ## 2nd key is the index name ## the table lists the field on which the index applies our %indexes = ( 'admin_table' => {'admin_user_index' => ['user_admin']}, 'subscriber_table' => {'subscriber_user_index' => ['user_subscriber']}, 'stat_table' => {'stats_user_index' => ['email_stat']} ); # table indexes that can be removed during upgrade process our @former_indexes = ( 'user_subscriber', 'list_subscriber', 'subscriber_idx', 'admin_idx', 'netidmap_idx', 'user_admin', 'list_admin', 'role_admin', 'admin_table_index', 'logs_table_index', 'netidmap_table_index', 'subscriber_table_index', 'user_index' ); 1; __END__ =encoding utf-8 =head1 NAME Sympa::DatabaseDescription - Dafinition of core database structure =head1 DESCRIPTION This module keeps structure of database used by Sympa software. =head2 Functions =over =item full_db_struct () I. Returns a heshref containing definitions of all tables. Each item has the name of table as key and definition as value. Each definition is hashref containig following keys: =over =item fields See below. =item doc Description of the table. =item order TBD. =back C item is hasref which may contain following items. =over =item struct Column data types. Definitions are based on MySQL. Following types are recognized: =over =item varchar(I) Text with length up to I. I must be lower than 2^16 - 2. =item int(1) Boolean, 1 or 0. =item int(11) Unix time. =item int(I) Integer with columns up to I, with its value from -2^31 to 2^31 - 1. =item tinyint Integer, -2^7 to 2^7 - 1. =item smallint Integer, -2^15 to 2^15 - 1. =item bigint Integer, -2^63 to 2^63 - 1. =item double IEEE floating point number, 8 bytes. =item enum Keyword with length up to 20 o. =item text Text with length up to 500 o. =item longtext Text with length up to 2^32 - 4 o. =item datetime Timestamp. =item mediumblob Binary data with length up to 2^24 - 3 o. =back =item doc Description of the field. =item primary If this is true, primary key consists of this field. =item not_null If this is true, Null value is not allowed. Note that fields included in primary key always don't allow Null value. =back =item db_struct () This function was OBSOLETED. =item not_null () I. TBD. =item autoincrement () I. TBD. =item primary () I. TBD. =back =head1 SEE ALSO L, L. =head1 HISTORY L was introduced behind the veil on Sympa 6.1. It began to be referred overtly as a part of Sympa Database Manager (SDM) on Sympa 6.2. =cut sympa-6.2.24/src/lib/Sympa/CommandDef.pm0000644000175000017500000002337313216651447016674 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # # 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, see . package Sympa::CommandDef; use strict; use warnings; use Conf; use Sympa::Regexps; my $_email_re = Sympa::Regexps::addrspec(); our %comms = ( add => { cmd_regexp => qr'add'i, arg_regexp => qr{(\S+)\s+($_email_re)(?:\s+(.+))?\s*\z}, arg_keys => [qw(localpart email gecos)], cmd_format => 'ADD %s %s %s', }, auth => { cmd_regexp => qr'auth'i, arg_regexp => qr'(\w+)\s+(.+)', arg_keys => [qw(keyauth cmd)], cmd_format => 'AUTH %s %s', }, confirm => { cmd_regexp => qr'con|confirm'i, arg_regexp => qr'(\w+)\s*\z', arg_keys => [qw(authkey)], cmd_format => 'CONFIRM %s', }, del => { cmd_regexp => qr'del|delete'i, arg_regexp => qr{(\S+)\s+($_email_re)\s*}, arg_keys => [qw(localpart email)], cmd_format => 'DEL %s %s', }, distribute => { cmd_regexp => qr'dis|distribute'i, arg_regexp => qr'(\S+)\s+(\w+)\s*\z', arg_keys => [qw(localpart authkey)], cmd_format => 'DISTRIBUTE %s %s', }, get => { cmd_regexp => qr'get'i, arg_regexp => qr'(\S+)\s+(.+)', arg_keys => [qw(localpart arc)], cmd_format => 'GET %s %s', }, help => {cmd_regexp => qr'hel|help|sos'i, cmd_format => 'HELP',}, info => { cmd_regexp => qr'inf|info'i, arg_regexp => qr'(.+)', arg_keys => [qw(localpart)], cmd_format => 'INFO %s', }, index => { cmd_regexp => qr'ind|index'i, arg_regexp => qr'(.+)', arg_keys => [qw(localpart)], cmd_format => 'INDEX %s', }, invite => { cmd_regexp => qr'inv|invite'i, arg_regexp => qr{(\S+)\s+($_email_re)(?:\s+(.+))?\s*\z}, arg_keys => [qw(localpart email gecos)], cmd_format => 'INVITE %s %s %s', }, last => { cmd_regexp => qr'las|last'i, arg_regexp => qr'(.+)', arg_keys => [qw(localpart)], cmd_format => 'LAST %s', }, lists => {cmd_regexp => qr'lis|lists?'i, cmd_format => 'LISTS',}, modindex => { cmd_regexp => qr'mod|modindex|modind'i, arg_regexp => qr'(\S+)', arg_keys => [qw(localpart)], cmd_format => 'MODINDEX %s', }, finished => {cmd_regexp => qr'qui|quit|end|stop|-'i,}, reject => { cmd_regexp => qr'rej|reject'i, arg_regexp => qr'(\S+)\s+(\w+)\s*\z', arg_keys => [qw(localpart authkey)], cmd_format => 'REJECT %s %s', }, remind => { cmd_regexp => qr'rem|remind'i, arg_regexp => qr'([^\s\@]+)(?:\@([-.\w]+))?\s*\z', arg_keys => [qw(localpart domainpart)], cmd_format => 'REMIND %1$s', filter => sub { my $r = shift; if ($r->{domainpart}) { my $host; if (ref $r->{context} eq 'Sympa::List') { $host = $r->{context}->{'admin'}{'host'}; } else { $host = Conf::get_robot_conf($r->{context}, 'host'); } return undef unless lc $r->{domainpart} eq $host; } $r; }, }, global_remind => { cmd_regexp => qr'(?:rem|remind)\s+[*]'i, cmd_format => 'REMIND *', }, review => { cmd_regexp => qr'rev|review|who'i, arg_regexp => qr'(.+)', arg_keys => [qw(localpart)], cmd_format => 'REVIEW %s', }, set => { cmd_regexp => qr'set'i, arg_regexp => qr'(\S+)\s+(?:(digest|digestplain|nomail|normal|not_me|each|mail|summary|notice|txt|html|urlize)|(conceal|noconceal))\s*\z'i, arg_keys => [qw(localpart reception visibility)], cmd_format => 'SET %s %s%s', filter => sub { my $r = shift; $r->{email} = $r->{sender}; if ($r->{reception}) { $r->{reception} = lc $r->{reception}; # SET EACH is a synonym for SET MAIL. $r->{reception} = 'mail' if grep { $r->{reception} eq $_ } qw(each eachmail nodigest normal); } if ($r->{visibility}) { $r->{visibility} = lc $r->{visibility}; } $r; }, }, global_set => { cmd_regexp => qr'set\s+[*]'i, arg_regexp => qr'(?:(digest|digestplain|nomail|normal|not_me|each|mail|summary|notice|txt|html|urlize)|(conceal|noconceal))\s*\z'i, arg_keys => [qw(reception visibility)], cmd_format => 'SET * %s%s', filter => sub { my $r = shift; $r->{email} = $r->{sender}; if ($r->{reception}) { $r->{reception} = lc $r->{reception}; # SET EACH is a synonym for SET MAIL. $r->{reception} = 'mail' if grep { $r->{reception} eq $_ } qw(each eachmail nodigest normal); } if ($r->{visibility}) { $r->{visibility} = lc $r->{visibility}; } $r; }, }, stats => { cmd_regexp => qr'sta|stats'i, arg_regexp => qr'(.+)', arg_keys => [qw(localpart)], cmd_format => 'STATS %s', }, subscribe => { cmd_regexp => qr'sub|subscribe'i, arg_regexp => qr'(\S+)(?:\s+(.+))?\s*\z', arg_keys => [qw(localpart gecos)], cmd_format => 'SUB %s %s', filter => sub { my $r = shift; $r->{email} = $r->{sender}; $r; }, }, signoff => { cmd_regexp => qr'sig|signoff|uns|unsub|unsubscribe'i, arg_regexp => qr{([^\s\@]+)(?:\@([-.\w]+))?(?:\s+($_email_re))?\z}, arg_keys => [qw(localpart domainpart email)], cmd_format => sub { my $r = shift; return ($r->{sender} and $r->{sender} eq $r->{email}) ? 'SIG %s' : 'SIG %1$s %3$s'; }, filter => sub { my $r = shift; # email is defined if command is "unsubscribe ". $r->{email} ||= $r->{sender}; if ($r->{domainpart}) { my $host; if (ref $r->{context} eq 'Sympa::List') { $host = $r->{context}->{'admin'}{'host'}; } else { $host = Conf::get_robot_conf($r->{context}, 'host'); } return undef unless lc $r->{domainpart} eq $host; } $r; }, }, global_signoff => { cmd_regexp => qr'(?:sig|signoff|uns|unsub|unsubscribe)\s+[*]'i, arg_regexp => qr{($_email_re)?\z}, arg_keys => [qw(email)], cmd_format => sub { my $r = shift; return ($r->{sender} and $r->{sender} eq $r->{email}) ? 'SIG *' : 'SIG * %s'; }, filter => sub { my $r = shift; # email is defined if command is "unsubscribe * ". $r->{email} ||= $r->{sender}; $r; }, }, verify => { cmd_regexp => qr'ver|verify'i, arg_regexp => qr'(.+)', arg_keys => [qw(localpart)], cmd_format => 'VERIFY %s', }, which => {cmd_regexp => qr'whi|which|status'i, cmd_format => 'WHICH',}, ); 1; __END__ =encoding utf-8 =head1 NAME Sympa::CommandDef - Definition of mail commands =head1 SYNOPSIS TBD =head1 DESCRIPTION This module keeps definition of mail commands. =head2 Global variable =over =item %comms This hash defines format of mail commands. It is used for decoding and encoding between command lines and internal request objects. Key is the name of action which is given as C parameter to constructor of L. Note that not all sort of requests are defined. Value is the hashref. Each item of hashrefs accepts the following keywords : =over =item cmd_regexp A regexp matching command. Note that C modifier is necessary. =item arg_regexp A regexp matching command line arguments. Note that C modifier may be needed. =item arg_keys An arrayref of parameter names mapping command line to attribute. C<'localpart'> is special: If it is contained, C attribute of resulting request object is an instance of L class. =item cmd_format A string to format command line using attributes. If this item is code reference, it will be called with request object and returned value will be used as format string. =item filter A coderef to perform additional checking. It is called with request object and, if it returns false value, decoding will fail. =back =back =head1 SEE ALSO L, L. =head1 HISTORY L appeared on Sympa 6.2.13. =cut sympa-6.2.24/src/lib/Sympa/Message.pm0000644000175000017500000036217713216651447016273 0ustar rackeracke# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level # directory of this distribution and at # . # # 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, see . package Sympa::Message; use strict; use warnings; use DateTime; use Encode qw(); use English; # FIXME: drop $PREMATCH usage use HTML::TreeBuilder; use Mail::Address; use MIME::Charset; use MIME::EncWords; use MIME::Entity; use MIME::Parser; use MIME::Tools; use Scalar::Util qw(); use Text::LineFold; use URI::Escape qw(); BEGIN { eval 'use Crypt::SMIME'; } BEGIN { eval 'use Net::DNS'; } use Sympa; use Conf; use Sympa::Constants; use Sympa::HTML::FormatText; use Sympa::HTMLSanitizer; use Sympa::Language; use Sympa::Log; use Sympa::Scenario; use Sympa::Spool; use Sympa::Template; use Sympa::Tools::Data; use Sympa::Tools::File; use Sympa::Tools::Password; use Sympa::Tools::SMIME; use Sympa::Tools::Text; use Sympa::User; my $language = Sympa::Language->instance; my $log = Sympa::Log->instance; sub new { $log->syslog('debug2', '(%s, ...)', @_); my $class = shift; my $serialized = shift; my $self = bless {@_} => $class; unless (defined $serialized and length $serialized) { $log->syslog('err', 'Empty message'); return undef; } # Get attributes from pseudo-header fields at the top of serialized # message. Note that field names are case-sensitive. pos($serialized) = 0; while ($serialized =~ /\G(X-Sympa-[-\w]+): (.*?)\n(?![ \t])/cgs) { my ($k, $v) = ($1, $2); next unless length $v; if ($k eq 'X-Sympa-To') { $self->{'rcpt'} = join ',', split(/\s*,\s*/, $v); } elsif ($k eq 'X-Sympa-Checksum') { # To migrate format <= 6.2a.40 $self->{'checksum'} = $v; } elsif ($k eq 'X-Sympa-Family') { $self->{'family'} = $v; } elsif ($k eq 'X-Sympa-From') { # Compatibility. Use Return-Path: $self->{'envelope_sender'} = $v; } elsif ($k eq 'X-Sympa-Auth-Level') { # New in 6.2a.41 if ($v eq 'md5') { $self->{'md5_check'} = 1; } else { $log->syslog('err', 'Unknown authentication level "%s", ignored', $v); } } elsif ($k eq 'X-Sympa-Message-ID') { # New in 6.2a.41 $self->{'message_id'} = $v; } elsif ($k eq 'X-Sympa-Sender') { # New in 6.2a.41 $self->{'sender'} = $v; } elsif ($k eq 'X-Sympa-Display-Name') { # New in 6.2a.41 $self->{'gecos'} = $v; } elsif ($k eq 'X-Sympa-Shelved') { # New in 6.2a.41 $self->{'shelved'} = { map { my ($ak, $av) = split /=/, $_, 2; ($ak => ($av || 1)) } split(/\s*;\s*/, $v) }; } elsif ($k eq 'X-Sympa-Spam-Status') { # New in 6.2a.41 $self->{'spam_status'} = $v; } else { $log->syslog('err', 'Unknown attribute information: "%s: %s"', $k, $v); } } # Ignore Unix From_ $serialized =~ /\GFrom (.*?)\n(?![ \t])/cgs; # Get envelope sender from Return-Path:. # If old style X-Sympa-From: has been found, omit Return-Path:. # # We trust in "Return-Path:" header field only at the top of message # to prevent forgery. See CAVEAT. if ($serialized =~ /\GReturn-Path: (.*?)\n(?![ \t])/cgs and not exists $self->{'envelope_sender'}) { my $addr = $1; if ($addr =~ /<>/) { # special: null envelope sender $self->{'envelope_sender'} = '<>'; } else { my @addrs = Mail::Address->parse($addr); if (@addrs and Sympa::Tools::Text::valid_email($addrs[0]->address)) { $self->{'envelope_sender'} = $addrs[0]->address; } } } # Strip attributes. substr($serialized, 0, pos $serialized) = ''; # Check if message is parsable. my $parser = MIME::Parser->new; $parser->output_to_core(1); $parser->tmp_dir($Conf::Conf{'tmpdir'}); my $entity = $parser->parse_data(\$serialized); unless ($entity) { $log->syslog('err', 'Unable to parse message'); return undef; } my $hdr = $entity->head; my ($dummy, $body_string) = split /(?:\A|\n)\r?\n/, $serialized, 2; $self->{_head} = $hdr; $self->{_body} = $body_string; $self->{_entity_cache} = $entity; $self->{'size'} = length $serialized; unless (exists $self->{'sender'} and defined $self->{'sender'}) { ($self->{'sender'}, $self->{'gecos'}) = $self->_get_sender_email; } ## Store decoded subject and its original charset my $subject = $hdr->get('Subject'); if (defined $subject and $subject =~ /\S/) { my @decoded_subject = MIME::EncWords::decode_mimewords($subject); $self->{'subject_charset'} = 'US-ASCII'; foreach my $token (@decoded_subject) { unless ($token->[1]) { # don't decode header including raw 8-bit bytes. if ($token->[0] =~ /[^\x00-\x7F]/) { $self->{'subject_charset'} = undef; last; } next; } my $cset = MIME::Charset->new($token->[1]); # don't decode header encoded with unknown charset. unless ($cset->decoder) { $self->{'subject_charset'} = undef; last; } unless ($cset->output_charset eq 'US-ASCII') { $self->{'subject_charset'} = $token->[1]; } } } else { $self->{'subject_charset'} = undef; } if ($self->{'subject_charset'}) { chomp $subject; $self->{'decoded_subject'} = MIME::EncWords::decode_mimewords($subject, Charset => 'UTF-8'); } else { if (defined $subject) { chomp $subject; $subject =~ s/(\r\n|\r|\n)(?=[ \t])//g; $subject =~ s/\r\n|\r|\n/ /g; } $self->{'decoded_subject'} = $subject; } ## TOPICS my $topics; if ($topics = $hdr->get('X-Sympa-Topic')) { $self->{'topic'} = $topics; } # Message ID unless (exists $self->{'message_id'}) { $self->{'message_id'} = _get_message_id($self); } return $self; } # Tentative: removed when refactoring finished. sub new_from_file { my $class = shift; my $file = shift; open my $fh, '<', $file or return undef; my $serialized = do { local $RS; <$fh> }; close $fh; my $self = $class->new($serialized, @_) or return undef; $self->{'filename'} = $file; # Get file date unless (exists $self->{'date'}) { $self->{'date'} = Sympa::Tools::File::get_mtime($file); } return $self; } ## Get sender of the message according to header fields specified by ## 'sender_headers' parameter. ## FIXME: S/MIME signer may not be same as the sender given by this function. sub _get_sender_email { my $self = shift; my $hdr = $self->{_head}; my $sender = undef; my $gecos = undef; foreach my $field (split /[\s,]+/, $Conf::Conf{'sender_headers'}) { if (lc $field eq 'return-path') { ## Try to get envelope sender if ( $self->{'envelope_sender'} and $self->{'envelope_sender'} ne '<>') { $sender = lc($self->{'envelope_sender'}); } } elsif ($hdr->get($field)) { ## Try to get message header. ## On "Resent-*:" headers, the first occurrence must be used (see ## RFC 5322 3.6.6). ## FIXME: Though "From:" can occur multiple times, only the first ## one is detected. my $addr = $hdr->get($field, 0); # get the first one my @sender_hdr = Mail::Address->parse($addr); if (@sender_hdr and $sender_hdr[0]->address) { $sender = lc($sender_hdr[0]->address); my $phrase = $sender_hdr[0]->phrase; if (defined $phrase and length $phrase) { $gecos = MIME::EncWords::decode_mimewords($phrase, Charset => 'UTF-8'); # Eliminate hostile characters. $gecos =~ s/(\r\n|\r|\n)(?=[ \t])//g; $gecos =~ s/[\0\r\n]+//g; } last; } } last if defined $sender; } unless (defined $sender) { #$log->syslog('debug3', 'No valid sender address'); return; } unless (Sympa::Tools::Text::valid_email($sender)) { $log->syslog('err', 'Invalid sender address "%s"', $sender); return; } return ($sender, $gecos); } # Note that this must be called after decrypting message # FIXME: Also check Resent-Message-ID:. sub _get_message_id { my $self = shift; return Sympa::Tools::Text::canonic_message_id( $self->{_head}->get('Message-Id', 0)); } # Old names: (part of) mail::mail_file(), mail::parse_tt2_messageasstring(), # List::send_file(), List::send_global_file(). # Moved to: Sympa::Message::Template::new(). #sub new_from_template; sub dup { my $self = shift; my $clone = {}; foreach my $key (sort keys %$self) { my $val = $self->{$key}; next unless defined $val; unless (Scalar::Util::blessed($val)) { $clone->{$key} = Sympa::Tools::Data::dup_var($val); } elsif ($val->can('dup') and !$val->isa('Sympa::List')) { $clone->{$key} = $val->dup; } else { $clone->{$key} = $val; } } return bless $clone => ref($self); } sub to_string { my $self = shift; my %options = @_; my $serialized = ''; if (ref $self->{'rcpt'} eq 'ARRAY' and @{$self->{'rcpt'}}) { $serialized .= sprintf "X-Sympa-To: %s\n", join(',', @{$self->{'rcpt'}}); } elsif (defined $self->{'rcpt'} and length $self->{'rcpt'}) { $serialized .= sprintf "X-Sympa-To: %s\n", join(',', split(/\s*,\s*/, $self->{'rcpt'})); } if (defined $self->{'checksum'}) { $serialized .= sprintf "X-Sympa-Checksum: %s\n", $self->{'checksum'}; } if (defined $self->{'family'}) { $serialized .= sprintf "X-Sympa-Family: %s\n", $self->{'family'}; } if (defined $self->{'md5_check'} and length $self->{'md5_check'}) { # New in 6.2a.41 $serialized .= sprintf "X-Sympa-Auth-Level: %s\n", 'md5'; } if (defined $self->{'message_id'}) { # New in 6.2a.41 $serialized .= sprintf "X-Sympa-Message-ID: %s\n", $self->{'message_id'}; } if (defined $self->{'sender'}) { # New in 6.2a.41 $serialized .= sprintf "X-Sympa-Sender: %s\n", $self->{'sender'}; } if (defined $self->{'gecos'} and length $self->{'gecos'}) { # New in 6.2a.41 $serialized .= sprintf "X-Sympa-Display-Name: %s\n", $self->{'gecos'}; } if (%{$self->{'shelved'} || {}}) { # New in 6.2a.41 $serialized .= sprintf "X-Sympa-Shelved: %s\n", join( '; ', map { my $v = $self->{shelved}{$_}; ("$v" eq '1') ? $_ : sprintf('%s=%s', $_, $v); } grep { $self->{shelved}{$_} } sort keys %{$self->{shelved}} ); } if (defined $self->{'spam_status'}) { # New in 6.2a.41. $serialized .= sprintf "X-Sympa-Spam-Status: %s\n", $self->{'spam_status'}; } # This terminates pseudo-header part for attributes. unless (defined $self->{'envelope_sender'}) { $serialized .= "Return-Path: \n"; } $serialized .= $self->as_string(%options); return $serialized; } sub add_header { my $self = shift; $self->{_head}->add(@_); delete $self->{_entity_cache}; # Clear entity cache. } sub delete_header { my $self = shift; $self->{_head}->delete(@_); delete $self->{_entity_cache}; # Clear entity cache. } sub replace_header { my $self = shift; $self->{_head}->replace(@_); delete $self->{_entity_cache}; # Clear entity cache. } sub head { shift->{_head}; } # NOTE: As this processes is needed for incoming messages only, it would be # moved to incoming pipeline class.. sub check_spam_status { my $self = shift; my $robot_id = (ref $self->{context} eq 'Sympa::List') ? $self->{context}->{'domain'} : $self->{context}; my $spam_status = Sympa::Scenario::request_action($robot_id || $Conf::Conf{'domain'}, 'spam_status', 'smtp', {'message' => $self}); if (defined $spam_status) { if (ref($spam_status) eq 'HASH') { $self->{'spam_status'} = $spam_status->{'action'}; } else { $self->{'spam_status'} = $spam_status; } } else { $self->{'spam_status'} = 'unknown'; } } my $has_mail_dkim_textwrap; BEGIN { eval 'use Mail::DKIM::Signer'; # This doesn't export $VERSION. eval 'use Mail::DKIM::TextWrap'; $has_mail_dkim_textwrap = !$EVAL_ERROR; # Mail::DKIM::Signer prior to 0.38 doesn't import this. eval 'use Mail::DKIM::PrivateKey'; } # Old name: tools::dkim_sign() which took string and returned string. sub dkim_sign { $log->syslog('debug', '(%s)', @_); my $self = shift; my %options = @_; my $dkim_d = $options{'dkim_d'}; my $dkim_i = $options{'dkim_i'}; my $dkim_selector = $options{'dkim_selector'}; my $dkim_privatekey = $options{'dkim_privatekey'}; unless ($dkim_selector) { $log->syslog('err', "DKIM selector is undefined, could not sign message"); return undef; } unless ($dkim_privatekey) { $log->syslog('err', "DKIM key file is undefined, could not sign message"); return undef; } unless ($dkim_d) { $log->syslog('err', "DKIM d= tag is undefined, could not sign message"); return undef; } unless ($Mail::DKIM::Signer::VERSION) { $log->syslog('err', "Failed to load Mail::DKIM::Signer Perl module, ignoring DKIM signature" ); return undef; } unless ($has_mail_dkim_textwrap) { $log->syslog('err', "Failed to load Mail::DKIM::TextWrap Perl module, signature will not be pretty" ); } # DKIM::PrivateKey does never allow armour texts nor newlines. Strip them. my $privatekey_string = join '', grep { !/^---/ and $_ } split /\r\n|\r|\n/, $dkim_privatekey; my $privatekey = Mail::DKIM::PrivateKey->load(Data => $privatekey_string); unless ($privatekey) { $log->syslog('err', 'Can\'t create Mail::DKIM::PrivateKey'); return undef; } # create a signer object my $dkim = Mail::DKIM::Signer->new( Algorithm => "rsa-sha1", Method => "relaxed", Domain => $dkim_d, Selector => $dkim_selector, Key => $privatekey, ($dkim_i ? (Identity => $dkim_i) : ()), ); unless ($dkim) { $log->syslog('err', 'Can\'t create Mail::DKIM::Signer'); return undef; } # $new_body will store the body as fed to Mail::DKIM to reuse it # when returning the message as string. Line terminators must be # normalized with CRLF. my $msg_as_string = $self->as_string; $msg_as_string =~ s/\r?\n/\r\n/g; $msg_as_string =~ s/\r?\z/\r\n/ unless $msg_as_string =~ /\n\z/; $dkim->PRINT($msg_as_string); unless ($dkim->CLOSE) { $log->syslog('err', 'Cannot sign (DKIM) message'); return undef; } my ($dummy, $new_body) = split /\r\n\r\n/, $msg_as_string, 2; $new_body =~ s/\r\n/\n/g; # Signing is done. Rebuilding message as string with original body # and new headers. # Note that DKIM-Signature: field should be prepended to the header. $self->add_header('DKIM-Signature', $dkim->signature->as_string, 0); $self->{_body} = $new_body; delete $self->{_entity_cache}; # Clear entity cache. return $self; } BEGIN { eval 'use Mail::DKIM::Verifier'; } sub check_dkim_signature { my $self = shift; return unless $Mail::DKIM::Verifier::VERSION; my $robot_id = (ref $self->{context} eq 'Sympa::List') ? $self->{context}->{'domain'} : $self->{context}; return unless Sympa::Tools::Data::smart_eq( Conf::get_robot_conf($robot_id || '*', 'dkim_feature'), 'on'); my $dkim; unless ($dkim = Mail::DKIM::Verifier->new()) { $log->syslog('err', 'Could not create Mail::DKIM::Verifier'); return; } # Line terminators must be normalized with CRLF. my $msg_as_string = $self->as_string; $msg_as_string =~ s/\r?\n/\r\n/g; $msg_as_string =~ s/\r?\z/\r\n/ unless $msg_as_string =~ /\n\z/; $dkim->PRINT($msg_as_string); unless ($dkim->CLOSE) { $log->syslog('err', 'Cannot verify signature of (DKIM) message'); return; } #FIXME: Identity of signatures would be checked. foreach my $signature ($dkim->signatures) { if ($signature->result_detail eq 'pass') { $self->{'dkim_pass'} = 1; return; } } delete $self->{'dkim_pass'}; } # Old name: tools::remove_invalid_dkim_signature() which takes a message as # string and outputs idem without signature if invalid. sub remove_invalid_dkim_signature { $log->syslog('debug2', '(%s)', @_); my $self = shift; return unless $self->get_header('DKIM-Signature'); $self->check_dkim_signature; unless ($self->{'dkim_pass'}) { $log->syslog('info', 'DKIM signature of message %s is invalid, removing', $self); $self->delete_header('DKIM-Signature'); } } sub as_entity { my $self = shift; unless (defined $self->{_entity_cache}) { die 'Bug in logic. Ask developer' unless $self->{_head}; my $string = $self->{_head}->as_string . "\n" . (defined $self->{_body} ? $self->{_body} : ''); my $parser = MIME::Parser->new(); $parser->output_to_core(1); $parser->tmp_dir($Conf::Conf{'tmpdir'}); $self->{_entity_cache} = $parser->parse_data(\$string); } return $self->{_entity_cache}; } sub set_entity { my $self = shift; my $entity = shift; return undef unless $entity; my $orig = $self->as_entity->as_string; my $new = $entity->as_string; if ($orig ne $new) { $self->{_head} = $entity->head; $self->{_body} = $entity->body_as_string; $self->{_entity_cache} = $entity; # Also update entity cache. } return $entity; } sub as_string { my $self = shift; my %options = @_; die 'Bug in logic. Ask developer' unless $self->{_head}; return $self->{'orig_msg_as_string'} if $options{'original'} and $self->{'smime_crypted'}; my $return_path = ''; if (defined $self->{'envelope_sender'}) { my $val = $self->{'envelope_sender'}; $val = "<$val>" unless $val eq '<>'; $return_path = sprintf "Return-Path: %s\n", $val; } return $return_path . $self->{_head}->as_string . "\n" . (defined $self->{_body} ? $self->{_body} : ''); } sub body_as_string { my $self = shift; return $self->{_body}; } sub header_as_string { my $self = shift; return $self->{_head}->as_string; } sub get_header { my $self = shift; my $field = shift; my $sep = shift; die sprintf 'Second argument is not index but separator: "%s"', $sep if defined $sep and Scalar::Util::looks_like_number($sep); my $hdr = $self->{_head}; if (defined $sep or wantarray) { my @values = grep {s/\A$field\s*:\s*//i} split /\n(?![ \t])/, $hdr->as_string(); if (defined $sep) { return undef unless @values; return join $sep, @values; } return @values; } else { my $value = $hdr->get($field, 0); chomp $value if defined $value; return $value; } } # Old name: tools::decode_header() which can take Message, MIME::Entity, # MIME::Head or Mail::Header object as argument. sub get_decoded_header { my $self = shift; my $tag = shift; my $sep = shift; my $head = $self->head; if (defined $sep) { my @values = $head->get($tag); return undef unless scalar @values; foreach my $val (@values) { $val = MIME::EncWords::decode_mimewords($val, Charset => 'UTF-8'); chomp $val; } return join $sep, @values; } else { my $val = $head->get($tag); return undef unless defined $val; $val = MIME::EncWords::decode_mimewords($val, Charset => 'UTF-8'); chomp $val; return $val; } } # Dump the Message object # Currently not used. sub dump { my ($self, $output) = @_; # my $output ||= \*STDERR; my $old_output = select; select $output; foreach my $key (keys %{$self}) { if (ref($self->{$key}) eq 'MIME::Entity') { printf "%s =>\n", $key; $self->{$key}->print; } else { printf "%s => %s\n", $key, $self->{$key}; } } select $old_output; return 1; } ## Add topic and put header X-Sympa-Topic # OBSOLETED. No longer used. sub add_topic { my ($self, $topic) = @_; $self->{'topic'} = $topic; $self->add_header('X-Sympa-Topic', $topic); } ## Get topic # OBSOLETED. No longer used. sub get_topic { my ($self) = @_; if (defined $self->{'topic'}) { return $self->{'topic'}; } else { return ''; } } sub clean_html { my $self = shift; my $robot = (ref $self->{context} eq 'Sympa::List') ? $self->{context}->{'domain'} : $self->{context}; my $entity = $self->as_entity->dup; if ($entity = _fix_html_part($entity, $robot)) { $self->set_entity($entity); return 1; } return 0; } sub _fix_html_part { my $entity = shift; my $robot = shift; return $entity unless $entity; my $eff_type = $entity->head->mime_type || ''; # Use real content-type. if ($entity->parts) { my @newparts = (); foreach my $part ($entity->parts) { push @newparts, _fix_html_part($part, $robot); } $entity->parts(\@newparts); } elsif ($eff_type eq 'text/html') { my $bodyh = $entity->bodyhandle; # Encoded body or null body won't be modified. return $entity if !$bodyh or $bodyh->is_encoded; my $body = $bodyh->as_string; # Re-encode parts to UTF-8, since StripScripts cannot handle texts # with some charsets (ISO-2022-*, UTF-16*, ...) correctly. my $cset = MIME::Charset->new( $entity->head->mime_attr('Content-Type.Charset') || ''); unless ($cset->decoder) { # Charset is unknown. Detect 7-bit charset. my ($dummy, $charset) = MIME::Charset::body_encode($body, '', Detect7Bit => 'YES'); $cset = MIME::Charset->new($charset) if $charset; } if ( $cset->decoder and $cset->as_string ne 'UTF-8' and $cset->as_string ne 'US-ASCII') { $cset->encoder('UTF-8'); $body = $cset->encode($body); $entity->head->mime_attr('Content-Type.Charset', 'UTF-8'); } my $filtered_body = Sympa::HTMLSanitizer->new($robot)->sanitize_html($body); my $io = $bodyh->open("w"); unless (defined $io) { $log->syslog('err', 'Failed to save message: %m'); return undef; } $io->print($filtered_body); $io->close; $entity->sync_headers(Length => 'COMPUTE') if $entity->head->get('Content-Length'); } return $entity; } # Old name: tools::smime_decrypt() which took MIME::Entity object and list, # and won't modify Message object. sub smime_decrypt { $log->syslog('debug2', '(%s)', @_); my $self = shift; return 0 unless $Crypt::SMIME::VERSION; my $key_passwd = $Conf::Conf{'key_passwd'}; $key_passwd = '' unless defined $key_passwd; my $content_type = lc($self->{_head}->mime_attr('Content-Type') || ''); unless ( ( $content_type eq 'application/pkcs7-mime' or $content_type eq 'application/x-pkcs7-mime' ) and !Sympa::Tools::Data::smart_eq( $self->{_head}->mime_attr('Content-Type.smime-type'), qr/signed-data/i ) ) { return 0; } #FIXME: an empty "context" parameter means mail to sympa@, listmaster@... my ($certs, $keys) = Sympa::Tools::SMIME::find_keys($self->{context} || '*', 'decrypt'); unless (defined $certs and @$certs) { $log->syslog('err', 'Unable to decrypt message: missing certificate file'); return undef; } my ($msg_string, $entity); # Try all keys/certs until one decrypts. while (my $certfile = shift @$certs) { my $keyfile = shift @$keys; $log->syslog('debug', 'Trying decrypt with certificate %s, key %s', $certfile, $keyfile); my ($cert, $key); if (open my $fh, '<', $certfile) { $cert = do { local $RS; <$fh> }; close $fh; } if (open my $fh, '<', $keyfile) { $key = do { local $RS; <$fh> }; close $fh; } my $smime = Crypt::SMIME->new(); if (length $key_passwd) { eval { $smime->setPrivateKey($key, $cert, $key_passwd) } or next; } else { eval { $smime->setPrivateKey($key, $cert) } or next; } $msg_string = eval { $smime->decrypt($self->as_string); }; last if defined $msg_string; } unless (defined $msg_string) { $log->syslog('err', 'Message could not be decrypted'); return undef; } my $parser = MIME::Parser->new; $parser->output_to_core(1); $parser->tmp_dir($Conf::Conf{'tmpdir'}); $entity = $parser->parse_data($msg_string); unless (defined $entity) { $log->syslog('err', 'Message could not be decrypted'); return undef; } my ($dummy, $body_string) = split /(?:\A|\n)\r?\n/, $msg_string, 2; my $head = $entity->head; # Now remove headers from $msg_string. # Keep for each header defined in the incoming message but undefined in # the decrypted message, add this header in the decrypted form. my $predefined_headers; foreach my $header ($head->tags) { $predefined_headers->{lc $header} = 1 if $head->get($header); } foreach my $header (split /\n(?![ \t])/, $self->header_as_string) { next unless $header =~ /^([^\s:]+)\s*:\s*(.*)$/s; my ($tag, $val) = ($1, $2); $head->add($tag, $val) unless $predefined_headers->{lc $tag}; } # Some headers from the initial message should not be restored # Content-Disposition and Content-Transfer-Encoding if the result is # multipart $head->delete('Content-Disposition') if $self->get_header('Content-Disposition'); if (Sympa::Tools::Data::smart_eq( $head->mime_attr('Content-Type'), qr/multipart/i ) ) { $head->delete('Content-Transfer-Encoding') if $self->get_header('Content-Transfer-Encoding'); } # We should be the sender and/or the listmaster $self->{'smime_crypted'} = 'smime_crypted'; $self->{'orig_msg_as_string'} = $self->as_string; $self->{_head} = $head; $self->{_body} = $body_string; delete $self->{_entity_cache}; # Clear entity cache. $log->syslog('debug', 'Message has been decrypted'); return $self; } # Old name: tools::smime_encrypt() which returns stringified message. sub smime_encrypt { $log->syslog('debug2', '(%s, %s)', @_); my $self = shift; my $email = shift; my $msg_header = $self->{_head}; my $certfile; my $entity; my $base = $Conf::Conf{'ssl_cert_dir'} . '/' . Sympa::Tools::Text::escape_chars($email); if (-f $base . '@enc') { $certfile = $base . '@enc'; } else { $certfile = $base; } unless (-r $certfile) { $log->syslog('notice', 'Unable to encrypt message to %s (missing certificate %s)', $email, $certfile); return undef; } my $cert; if (open my $fh, '<', $certfile) { $cert = do { local $RS; <$fh> }; close $fh; } # encrypt the incoming message parse it. my $smime = Crypt::SMIME->new(); #FIXME: Add intermediate CA certificates if any. $smime->setPublicKey($cert); # don't; cf RFC2633 3.1. netscape 4.7 at least can't parse encrypted # stuff that contains a whole header again... since MIME::Tools has # got no function for this, we need to manually extract only the MIME # headers... #XXX$msg_header->print(\*MSGDUMP); #XXXprintf MSGDUMP "\n%s", $msg_body; my $dup_head = $msg_header->dup(); foreach my $t ($dup_head->tags()) { $dup_head->delete($t) unless $t =~ /^(mime|content)-/i; } #FIXME: is $self->body_as_string respect base64 number of char per line ?? my $msg_string = eval { $smime->encrypt($dup_head->as_string . "\n" . $self->body_as_string); }; unless (defined $msg_string) { $log->syslog('err', 'Unable to S/MIME encrypt message: %s', $EVAL_ERROR); return undef; } ## Get as MIME object my $parser = MIME::Parser->new; $parser->output_to_core(1); $parser->tmp_dir($Conf::Conf{'tmpdir'}); unless ($entity = $parser->parse_data($msg_string)) { $log->syslog('notice', 'Unable to parse message'); return undef; } my ($dummy, $body_string) = split /\n\r?\n/, $msg_string, 2; # foreach header defined in the incomming message but undefined in # the crypted message, add this header in the crypted form. my $predefined_headers; foreach my $header ($entity->head->tags) { $predefined_headers->{lc $header} = 1 if $entity->head->get($header); } foreach my $header (split /\n(?![ \t])/, $msg_header->as_string) { next unless $header =~ /^([^\s:]+)\s*:\s*(.*)$/s; my ($tag, $val) = ($1, $2); $entity->head->add($tag, $val) unless $predefined_headers->{lc $tag}; } $self->{_head} = $entity->head; $self->{_body} = $body_string; delete $self->{_entity_cache}; # Clear entity cache. return $self; } # Old name: tools::smime_sign(). sub smime_sign { $log->syslog('debug2', '(%s)', @_); my $self = shift; my $list = $self->{context}; my $key_passwd = $Conf::Conf{'key_passwd'}; $key_passwd = '' unless defined $key_passwd; #FIXME return 1 unless $list; my ($certfile, $keyfile) = Sympa::Tools::SMIME::find_keys($list, 'sign'); my $signed_msg; ## Keep a set of header fields ONLY ## OpenSSL only needs content type & encoding to generate a ## multipart/signed msg my $dup_head = $self->head->dup; foreach my $field ($dup_head->tags) { next if $field =~ /^(content-type|content-transfer-encoding)$/i; $dup_head->delete($field); } my ($cert, $key); if (open my $fh, '<', $certfile) { $cert = do { local $RS; <$fh> }; close $fh; } if (open my $fh, '<', $keyfile) { $key = do { local $RS; <$fh> }; close $fh; } my $smime = Crypt::SMIME->new(); #FIXME: Add intermediate CA certificates if any. if (length $key_passwd) { $smime->setPrivateKey($key, $cert, $key_passwd); } else { $smime->setPrivateKey($key, $cert); } my $msg_string = eval { $smime->sign($dup_head->as_string . "\n" . $self->body_as_string); }; unless (defined $msg_string) { $log->syslog('err', 'Unable to S/MIME sign message: %s', $EVAL_ERROR); return undef; } my $parser = MIME::Parser->new; $parser->output_to_core(1); $parser->tmp_dir($Conf::Conf{'tmpdir'}); unless ($signed_msg = $parser->parse_data($msg_string)) { $log->syslog('notice', 'Unable to parse message'); return undef; } ## foreach header defined in the incoming message but undefined in the ## crypted message, add this header in the crypted form. my $head = $signed_msg->head; my $predefined_headers; foreach my $header ($head->tags) { $predefined_headers->{lc $header} = 1 if $head->get($header); } foreach my $header (split /\n(?![ \t])/, $self->header_as_string) { next unless $header =~ /^([^\s:]+)\s*:\s*(.*)$/s; my ($tag, $val) = ($1, $2); $head->add($tag, $val) unless $predefined_headers->{lc $tag}; } ## Keeping original message string in addition to updated headers. my ($dummy, $body_string) = split /(?:\A|\n)\r?\n/, $msg_string, 2; $self->{_head} = $head; $self->{_body} = $body_string; delete $self->{_entity_cache}; # Clear entity cache. $self->check_smime_signature; return $self; } # Old name: tools::smime_sign_check() or Message::smime_sign_check() # which won't alter Message object. sub check_smime_signature { $log->syslog('debug2', '(%s)', @_); my $self = shift; return 0 unless $Crypt::SMIME::VERSION; my $content_type = lc($self->{_head}->mime_attr('Content-Type') || ''); unless ( $content_type eq 'multipart/signed' or (( $content_type eq 'application/pkcs7-mime' or $content_type eq 'application/x-pkcs7-mime' ) and Sympa::Tools::Data::smart_eq( $self->{_head}->mime_attr('Content-Type.smime-type'), qr/signed-data/i ) ) ) { return 0; } ## Messages that should not be altered (no footer) $self->{'protected'} = 1; my $sender = $self->{'sender'}; # First step is to check if message signing is OK. my $smime = Crypt::SMIME->new; eval { # Crypt::SMIME >= 0.15 is required. $smime->setPublicKeyStore(grep { defined $_ and length $_ } ($Conf::Conf{'cafile'}, $Conf::Conf{'capath'})); }; unless (eval { $smime->check($self->as_string) }) { $log->syslog('err', '%s: Unable to verify S/MIME signature: %s', $self, $EVAL_ERROR); return undef; } # Second step is to check the signer of message matches the sender. # We need to check which certificate is for our user (CA and intermediate # certs are also included), and look at the purpose: # S/MIME signing and/or S/MIME encryption. #FIXME: A better analyse should be performed to extract the signer email. my %certs; my $signers = Crypt::SMIME::getSigners($self->as_string); foreach my $cert (@{$signers || []}) { my $parsed = Sympa::Tools::SMIME::parse_cert(text => $cert); next unless $parsed; next unless $parsed->{'email'}{lc $sender}; if ($parsed->{'purpose'}{'sign'} and $parsed->{'purpose'}{'enc'}) { $certs{'both'} = $cert; $log->syslog('debug', 'Found a signing + encryption cert'); } elsif ($parsed->{'purpose'}{'sign'}) { $certs{'sign'} = $cert; $log->syslog('debug', 'Found a signing cert'); } elsif ($parsed->{'purpose'}{'enc'}) { $certs{'enc'} = $cert; $log->syslog('debug', 'Found an encryption cert'); } last if $certs{'both'} or ($certs{'sign'} and $certs{'enc'}); } unless ($certs{both} or $certs{sign} or $certs{enc}) { $log->syslog('err', '%s: Could not extract certificate for %s', $self, $sender); return undef; } # OK, now we have the certs, either a combined sign+encryption one # or a pair of single-purpose. save them, as email@addr if combined, # or as email@addr@sign / email@addr@enc for split certs. foreach my $c (keys %certs) { my $filename = "$Conf::Conf{ssl_cert_dir}/" . Sympa::Tools::Text::escape_chars(lc($sender)); if ($c ne 'both') { unlink $filename; # just in case there's an old cert left... $filename .= "\@$c"; } else { unlink("$filename\@enc"); unlink("$filename\@sign"); } $log->syslog('debug', 'Saving %s cert in %s', $c, $filename); my $fh; unless (open $fh, '>', $filename) { $log->syslog('err', 'Unable to create certificate file %s: %m', $filename); return undef; } print $fh $certs{$c}; close $fh; } # TODO: Future version should check if the subject of certificate was part # of the SMIME signature. $self->{'smime_signed'} = 1; $log->syslog('debug3', '%s is signed, signature is checked', $self); ## Il faudrait traiter les cas d'erreur (0 diffĂ©rent de undef) return 1; } # Old name: Bulk::merge_msg() sub personalize { my $self = shift; my $list = shift; my $rcpt = shift || undef; my $data = shift || {}; my $content_type = lc($self->{_head}->mime_attr('Content-Type') || ''); if ( $content_type eq 'multipart/encrypted' or $content_type eq 'multipart/signed' or $content_type eq 'application/pkcs7-mime' or $content_type eq 'application/x-pkcs7-mime') { return 1; } my $entity = $self->as_entity->dup; # Initialize parameters at first only once. $data->{'headers'} ||= {}; my $headers = $entity->head; foreach my $key ( qw/subject x-originating-ip message-id date x-original-to from to thread-topic content-type/ ) { next unless $headers->count($key); my $value = $headers->get($key, 0); chomp $value; $value =~ s/(?:\r\n|\r|\n)(?=[ \t])//g; # unfold $data->{'headers'}{$key} = $value; } $data->{'subject'} = $self->{'decoded_subject'}; unless (defined _merge_msg($entity, $list, $rcpt, $data)) { return undef; } $self->set_entity($entity); return $self; } sub _merge_msg { my $entity = shift; my $list = shift; my $rcpt = shift; my $data = shift; my $enc = $entity->head->mime_encoding; # Parts with nonstandard encodings aren't modified. if ($enc and $enc !~ /^(?:base64|quoted-printable|[78]bit|binary)$/i) { return $entity; } my $eff_type = $entity->effective_type || 'text/plain'; # Signed or encrypted parts aren't modified. if ($eff_type =~ m{^multipart/(signed|encrypted)$}) { return $entity; } if ($entity->parts) { foreach my $part ($entity->parts) { unless (_merge_msg($part, $list, $rcpt, $data)) { $log->syslog('err', 'Failed to personalize message part'); return undef; } } } elsif ($eff_type =~ m{^(?:multipart|message)(?:/|\Z)}i) { # multipart or message types without subparts. return $entity; } elsif (MIME::Tools::textual_type($eff_type)) { my ($charset, $in_cset, $bodyh, $body, $utf8_body); my ($descr) = ($entity->head->get('Content-Description', 0)); chomp $descr if $descr; $descr = MIME::EncWords::decode_mimewords($descr, Charset => 'UTF-8'); $data->{'part'} = { description => $descr, disposition => lc($entity->head->mime_attr('Content-Disposition') || ''), encoding => $enc, type => $eff_type, }; $bodyh = $entity->bodyhandle; # Encoded body or null body won't be modified. if (!$bodyh or $bodyh->is_encoded) { return $entity; } $body = $bodyh->as_string; unless (defined $body and length $body) { return $entity; } ## Detect charset. If charset is unknown, detect 7-bit charset. $charset = $entity->head->mime_attr('Content-Type.Charset'); $in_cset = MIME::Charset->new($charset || 'NONE'); unless ($in_cset->decoder) { $in_cset = MIME::Charset->new(MIME::Charset::detect_7bit_charset($body) || 'NONE'); } unless ($in_cset->decoder) { $log->syslog('err', 'Unknown charset "%s"', $charset); return undef; } $in_cset->encoder($in_cset); # no charset conversion ## Only decodable bodies are allowed. eval { $utf8_body = Encode::encode_utf8($in_cset->decode($body, 1)); }; if ($EVAL_ERROR) { $log->syslog('err', 'Cannot decode by charset "%s"', $charset); return undef; } ## PARSAGE ## my $message_output; unless ( defined( $message_output = personalize_text($utf8_body, $list, $rcpt, $data) ) ) { $log->syslog('err', 'Error merging message'); return undef; } $utf8_body = $message_output; ## Data not encodable by original charset will fallback to UTF-8. my ($newcharset, $newenc); ($body, $newcharset, $newenc) = $in_cset->body_encode(Encode::decode_utf8($utf8_body), Replacement => 'FALLBACK'); unless ($newcharset) { # bug in MIME::Charset? $log->syslog('err', 'Can\'t determine output charset'); return undef; } elsif ($newcharset ne $in_cset->as_string) { $entity->head->mime_attr('Content-Transfer-Encoding' => $newenc); $entity->head->mime_attr('Content-Type.Charset' => $newcharset); ## normalize newline to CRLF if transfer-encoding is BASE64. $body =~ s/\r\n|\r|\n/\r\n/g if $newenc and $newenc eq 'BASE64'; } else { ## normalize newline to CRLF if transfer-encoding is BASE64. $body =~ s/\r\n|\r|\n/\r\n/g if $enc and uc $enc eq 'BASE64'; } ## Save new body. my $io = $bodyh->open('w'); unless ($io and $io->print($body) and $io->close) { $log->syslog('err', 'Can\'t write in Entity: %m'); return undef; } $entity->sync_headers(Length => 'COMPUTE') if $entity->head->get('Content-Length'); return $entity; } return $entity; } # Moved to Sympa::Spindle::AuthorizeMessage::_test_personalize(). #sub test_personalize; # Old name: Bulk::merge_data() sub personalize_text { my $body = shift; my $list = shift; my $rcpt = shift; my $data = shift || {}; die 'Unexpected type of $list' unless ref $list eq 'Sympa::List'; my $listname = $list->{'name'}; my $robot_id = $list->{'domain'}; $data->{'listname'} = $listname; $data->{'robot'} = $robot_id; $data->{'wwsympa_url'} = Conf::get_robot_conf($robot_id, 'wwsympa_url'); my $message_output; my $user = $list->get_list_member($rcpt); if ($user) { $user->{'escaped_email'} = URI::Escape::uri_escape($rcpt); $user->{'friendly_date'} = $language->gettext_strftime("%d %b %Y %H:%M", localtime($user->{'date'})); # this method has been removed because some users may forward # authentication link # $user->{'fingerprint'} = tools::get_fingerprint($rcpt); } $data->{'user'} = $user if $user; # Parse the template in the message : replace the tags and the parameters # by the corresponding values my $template = Sympa::Template->new(undef); return undef unless $template->parse($data, \$body, \$message_output, is_not_template => 1); return $message_output; } sub prepare_message_according_to_mode { my $self = shift; my $mode = shift; my $list = shift; my $robot_id = $list->{'domain'}; if ($mode eq 'nomail' or $mode eq 'summary' or $mode eq 'digest' or $mode eq 'digestplain') { ; } elsif ($mode eq 'notice') { ##Prepare message for notice reception mode my $entity = $self->as_entity->dup; $entity->bodyhandle(undef); $entity->parts([]); $self->set_entity($entity); } elsif ($mode eq 'txt') { ##Prepare message for txt reception mode my $entity = $self->as_entity->dup; if (_as_singlepart($entity, 'text/plain')) { $log->syslog('notice', 'Multipart message changed to singlepart'); } ## Add a footer _decorate_parts($entity, $list); $self->set_entity($entity); } elsif ($mode eq 'urlize') { # Prepare message for urlize reception mode. # Not extract message/rfc822 parts. my $parser = MIME::Parser->new; $parser->extract_nested_messages(0); $parser->extract_uuencode(1); $parser->output_to_core(1); $parser->tmp_dir($Conf::Conf{'tmpdir'}); my $msg_string = $self->as_string; $msg_string =~ s/\AReturn-Path: (.*?)\n(?![ \t])//s; my $entity = $parser->parse_data($msg_string); _urlize_parts($entity, $list, $self->{'message_id'}); ## Add a footer _decorate_parts($entity, $list); $self->set_entity($entity); } else { # 'mail' # Prepare message for normal reception mode, # and add a footer. unless ($self->{'protected'}) { my $entity = $self->as_entity->dup; _decorate_parts($entity, $list); $self->set_entity($entity); } } return $self; } # OBSOLETED. Use prepare_message_according_to_mode('mail'). sub decorate { my $self = shift; return $self->prepare_message_according_to_mode('mail', $self->{context}); } # Old name: # Sympa::List::add_parts() or Message::add_parts(), n.b. not add_part(). sub _decorate_parts { $log->syslog('debug3', '(%s, %s)'); my $entity = shift; my $list = shift; my $type = $list->{'admin'}{'footer_type'}; my $listdir = $list->{'dir'}; my $eff_type = $entity->effective_type || 'text/plain'; ## Signed or encrypted messages won't be modified. if ($eff_type =~ /^multipart\/(signed|encrypted)$/i) { return $entity; } my $header; foreach my $file ( "$listdir/message.header", "$listdir/message.header.mime", $Conf::Conf{'etc'} . '/mail_tt2/message.header', $Conf::Conf{'etc'} . '/mail_tt2/message.header.mime' ) { if (-f $file) { unless (-r $file) { $log->syslog('notice', 'Cannot read %s', $file); next; } $header = $file; last; } } my $footer; foreach my $file ( "$listdir/message.footer", "$listdir/message.footer.mime", $Conf::Conf{'etc'} . '/mail_tt2/message.footer', $Conf::Conf{'etc'} . '/mail_tt2/message.footer.mime' ) { if (-f $file) { unless (-r $file) { $log->syslog('notice', 'Cannot read %s', $file); next; } $footer = $file; last; } } ## No footer/header unless (($footer and -s $footer) or ($header and -s $header)) { return undef; } if ($type eq 'append') { ## append footer/header my ($footer_text, $header_text) = ('', ''); if ($header and -s $header) { open HEADER, $header; $header_text = join '',
; close HEADER; $header_text = '' unless $header_text =~ /\S/; } if ($footer and -s $footer) { open FOOTER, $footer; $footer_text = join '',