pax_global_header00006660000000000000000000000064130346664230014521gustar00rootroot0000000000000052 comment=649963a047ebad59f62b7cd620d6fe4329f392b2 dovecot-antispam-2.0+20170109/000077500000000000000000000000001303466642300155365ustar00rootroot00000000000000dovecot-antispam-2.0+20170109/.gitignore000066400000000000000000000001351303466642300175250ustar00rootroot00000000000000*.o *.so .config *~ dovecot-version.h antispam-version.h dovecot-version *-stamp *.sw[lmnop] dovecot-antispam-2.0+20170109/COPYING000066400000000000000000000432651303466642300166030ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. dovecot-antispam-2.0+20170109/Makefile000066400000000000000000000036401303466642300172010ustar00rootroot00000000000000# include config file if present CONFIG ?= .config -include $(CONFIG) CFLAGSORIG := $(CFLAGS) DOVECOT ?= /usr/include/dovecot -include $(DOVECOT)/dovecot-config INSTALLDIR ?= $(moduledir)/imap # Kill CFLAGS from dovecot-config CFLAGS := $(CFLAGSORIG) # includes/flags we need for building a dovecot plugin INCS += -DHAVE_CONFIG_H INCS += -I$(DOVECOT)/ INCS += -I$(DOVECOT)/src/ INCS += -I$(DOVECOT)/src/lib/ INCS += -I$(DOVECOT)/src/lib-storage/ INCS += -I$(DOVECOT)/src/lib-mail/ INCS += -I$(DOVECOT)/src/lib-imap/ INCS += -I$(DOVECOT)/src/lib-dict/ INCS += -I$(DOVECOT)/src/lib-index/ INCS += -I$(DOVECOT)/src/imap/ # output name LIBRARY_NAME ?= lib90_antispam_plugin.so objs = antispam-storage.o antispam-plugin.o debug.o objs += dspam-exec.o crm114-exec.o pipe.o spool2dir.o signature.o # main make rules LOCALCFLAGS += -fPIC -shared -Wall -Wextra CC ?= cc HOSTCC ?= cc all: $(LIBRARY_NAME) antispam-storage.o: antispam-storage.c antispam-storage-*.c antispam-plugin.h dovecot-version.h @echo " CC " $@ @$(CC) -c $(CFLAGS) $(LOCALCFLAGS) $(INCS) -o $@ $< %.o: %.c antispam-plugin.h dovecot-version.h antispam-version.h @echo " CC " $@ @$(CC) -c $(CFLAGS) $(LOCALCFLAGS) $(INCS) -o $@ $< $(LIBRARY_NAME): $(objs) @echo " LD " $(LIBRARY_NAME) @$(CC) $(CFLAGS) $(LOCALCFLAGS) $(INCS) $(objs) -o $(LIBRARY_NAME) $(LDFLAGS) dovecot-version: dovecot-version.c $(HOSTCC) $(INCS) -o dovecot-version dovecot-version.c dovecot-version.h: dovecot-version ./dovecot-version > dovecot-version.h antispam-version.h: version.sh ./version.sh antispam-version.h clean: rm -f *.so *.o *~ dovecot-version dovecot-version.h antispam-version.h install: all checkinstalldir install -p -m 0755 $(LIBRARY_NAME) $(DESTDIR)$(INSTALLDIR)/ checkinstalldir: @if [ ! -d "$(DESTDIR)$(INSTALLDIR)/" ] ; then \ echo "Installation directory $(DESTDIR)$(INSTALLDIR)/ doesn't exist," ; \ echo "run make install INSTALLDIR=..." ; \ exit 2 ; \ fi dovecot-antispam-2.0+20170109/NOTES000066400000000000000000000005571303466642300163600ustar00rootroot00000000000000 This file contains just some notes/thoughts on various implementation issues for other backends or different things. Full-message availability Pristine retraining or SpamAssassin retraining might need the full message available. This can easily be implemented since the backend is passed each struct mail *mail that is moved. For an example see the pipe backend. dovecot-antispam-2.0+20170109/README000066400000000000000000000000271303466642300164150ustar00rootroot00000000000000Use 'man ./antispam.7' dovecot-antispam-2.0+20170109/antispam-plugin.c000066400000000000000000000215201303466642300210120ustar00rootroot00000000000000/* * antispam plugin for dovecot * * Copyright (C) 2004-2007 Johannes Berg * 2006 Frank Cusack * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License Version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * * based on the original framework http://www.dovecot.org/patches/1.0/copy_plugin.c * * Please see http://johannes.sipsolutions.net/wiki/Projects/dovecot-dspam-integration * for more information on this code. * * Install the plugin in the usual dovecot module location. */ #include #include /* dovecot headers we need */ #include "lib.h" #include "str.h" #include "mail-storage-private.h" #include "antispam-version.h" /* internal stuff we need */ #include "antispam-plugin.h" static char *default_spam_folders[] = { "SPAM", NULL }; enum match_type { MT_REG, MT_PATTERN, MT_PATTERN_IGNCASE, /* keep last */ NUM_MT }; /* lower-case string, but keep modified UTF7 unchanged */ static void lowercase_string(const char *in, char *out) { char ch; while ((ch = *out++ = i_tolower(*in++))) { /* lower case */ if (ch == '&') { /* modified UTF7 -- find end of sequence ('-') */ while ((ch = *out++ = *in++)) { if (ch == '-') break; } } } } static bool mailbox_patternmatch(struct mailbox *box, #if DOVECOT_IS_GE(2,0) const struct mail_namespace *ns, #else struct mail_storage *storage, #endif const char *name, bool lowercase) { const char *boxname; char *lowerboxname; int len; int rc; #if DOVECOT_IS_GE(2,0) if (ns && mailbox_get_namespace(box) != ns) return FALSE; #else if (storage && mailbox_get_storage(box) != storage) return FALSE; #endif T_BEGIN { boxname = mailbox_get_name(box); if (lowercase) { lowerboxname = t_buffer_get(strlen(boxname) + 1); lowercase_string(boxname, lowerboxname); boxname = lowerboxname; } len = strlen(name); if (len && name[len - 1] == '*') { /* any wildcard */ --len; } else { /* compare EOS too */ ++len; } rc = memcmp(name, boxname, len) == 0; } T_END; return rc; } static bool mailbox_patternmatch_case(struct mailbox *box, #if DOVECOT_IS_GE(2,0) const struct mail_namespace *ns, #else struct mail_storage *ns, #endif const char *name) { return mailbox_patternmatch(box, ns, name, FALSE); } static bool mailbox_patternmatch_icase(struct mailbox *box, #if DOVECOT_IS_GE(2,0) const struct mail_namespace *ns, #else struct mail_storage *ns, #endif const char *name) { return mailbox_patternmatch(box, ns, name, TRUE); } static bool _mailbox_equals(struct mailbox *box, #if DOVECOT_IS_GE(2,0) const struct mail_namespace *ns, #else struct mail_storage *ns, #endif const char *name) { return mailbox_equals(box, ns, name); } typedef bool (*match_fn_t)(struct mailbox *, #if DOVECOT_IS_GE(2,0) const struct mail_namespace *, #else struct mail_storage *, #endif const char *); /* match information */ static const struct { const char *human, *suffix; match_fn_t fn; } match_info[NUM_MT] = { [MT_REG] = { .human = "exact match", .suffix = "", .fn = _mailbox_equals, }, [MT_PATTERN] = { .human = "wildcard match", .suffix = "_PATTERN", .fn = mailbox_patternmatch_case, }, [MT_PATTERN_IGNCASE] = { .human = "case-insensitive wildcard match", .suffix = "_PATTERN_IGNORECASE", .fn = mailbox_patternmatch_icase, }, }; static bool mailbox_in_list(struct mailbox *box, char ** const * patterns) { enum match_type i; char **list; if (!patterns) return FALSE; for (i = 0; i < NUM_MT; i++) { list = patterns[i]; if (!list) continue; while (*list) { if (match_info[i].fn(box, #if DOVECOT_IS_GE(2,0) mailbox_get_namespace(box), #else mailbox_get_storage(box), #endif *list)) return TRUE; list++; } } return FALSE; } bool mailbox_is_spam(const struct antispam_config *cfg, struct mailbox *box) { bool ret; ret = mailbox_in_list(box, cfg->spam_folders); debug_verbose(&cfg->dbgcfg, "mailbox_is_spam(%s): %d\n", mailbox_get_name(box), ret); return ret; } bool mailbox_is_trash(const struct antispam_config *cfg, struct mailbox *box) { bool ret; ret = mailbox_in_list(box, cfg->trash_folders); debug_verbose(&cfg->dbgcfg, "mailbox_is_trash(%s): %d\n", mailbox_get_name(box), ret); return ret; } bool mailbox_is_unsure(const struct antispam_config *cfg, struct mailbox *box) { bool ret; ret = mailbox_in_list(box, cfg->unsure_folders); debug_verbose(&cfg->dbgcfg, "mailbox_is_unsure(%s): %d\n", mailbox_get_name(box), ret); return ret; } bool keyword_is_spam(const struct antispam_config *cfg, const char *keyword) { char **k = cfg->spam_keywords; if (!cfg->spam_keywords) return FALSE; while (*k) { if (strcmp(*k, keyword) == 0) return TRUE; k++; } return FALSE; } static int parse_folder_setting(const struct antispam_config *cfg, const char *setting, char ***strings, const char *display_name, const char *(getenv)(const char *env, void *data), void *getenv_data) { const char *tmp; int cnt = 0; enum match_type i; T_BEGIN { for (i = 0; i < NUM_MT; ++i) { tmp = getenv(t_strconcat(setting, match_info[i].suffix, NULL), getenv_data); if (tmp) { strings[i] = p_strsplit(cfg->mem_pool, tmp, ";"); if (i == MT_PATTERN_IGNCASE) { /* lower case the string */ char **list = strings[i]; while (*list) { lowercase_string(*list, *list); ++list; } } } if (strings[i]) { char **iter = strings[i]; while (*iter) { ++cnt; debug(&cfg->dbgcfg, "\"%s\" is %s %s folder\n", *iter, match_info[i].human, display_name); iter++; } } } } T_END; if (!cnt) debug(&cfg->dbgcfg, "no %s folders\n", display_name); return cnt; } struct antispam_config * antispam_setup_config(const char *(getenv)(const char *env, void *data), void *getenv_data) { struct antispam_config *cfg; const char *tmp; char * const *iter; int spam_folder_count; cfg = i_new(struct antispam_config, 1); cfg->mem_pool = pool_alloconly_create("antispam-pool", 1024); if (debug_init(&cfg->dbgcfg, getenv, getenv_data) < 0) goto error; cfg->spam_folders[0] = default_spam_folders; spam_folder_count = parse_folder_setting(cfg, "SPAM", cfg->spam_folders, "spam", getenv, getenv_data); parse_folder_setting(cfg, "UNSURE", cfg->unsure_folders, "unsure", getenv, getenv_data); parse_folder_setting(cfg, "TRASH", cfg->trash_folders, "trash", getenv, getenv_data); tmp = getenv("ALLOW_APPEND_TO_SPAM", getenv_data); if (tmp && strcasecmp(tmp, "yes") == 0) { cfg->can_append_to_spam = TRUE; debug(&cfg->dbgcfg, "allowing APPEND to spam folders"); } tmp = getenv("SPAM_KEYWORDS", getenv_data); if (tmp) cfg->spam_keywords = p_strsplit(cfg->mem_pool, tmp, ";"); if (cfg->spam_keywords) { iter = cfg->spam_keywords; while (*iter) { debug(&cfg->dbgcfg, "\"%s\" is spam keyword\n", *iter); iter++; } } tmp = getenv("BACKEND", getenv_data); if (tmp) { if (strcmp(tmp, "crm114") == 0) cfg->backend = &crm114_backend; else if (strcmp(tmp, "dspam") == 0) cfg->backend = &dspam_backend; else if (strcmp(tmp, "pipe") == 0) cfg->backend = &pipe_backend; else if (strcmp(tmp, "spool2dir") == 0) cfg->backend = &spool2dir_backend; else { debug(&cfg->dbgcfg, "selected invalid backend!\n"); exit(3); } } else { debug(&cfg->dbgcfg, "no backend selected!\n"); goto error; } /* set spam_folders to empty to only allow keywords */ cfg->need_folder_hook = spam_folder_count > 0; cfg->need_keyword_hook = !!cfg->spam_keywords; cfg->backend->init(cfg, getenv, getenv_data); return cfg; error: mempool_unref(&cfg->mem_pool); i_free(cfg); return NULL; } void antispam_free_config(struct antispam_config *cfg) { mempool_unref(&cfg->mem_pool); i_free(cfg); } #if DOVECOT_IS_GE(2,0) void PLUGIN_FUNCTION(init)(struct module *module) { antispam_storage_init(module); } #else void PLUGIN_FUNCTION(init)(void) { antispam_storage_init(); } #endif void PLUGIN_FUNCTION(deinit)(void) { antispam_storage_deinit(); } /* put dovecot version we built against into plugin for checking */ #if DOVECOT_IS_GE(2,2) const char *PLUGIN_FUNCTION(version) = DOVECOT_ABI_VERSION; #else const char *PLUGIN_FUNCTION(version) = PACKAGE_VERSION; #endif dovecot-antispam-2.0+20170109/antispam-plugin.h000066400000000000000000000170141303466642300210220ustar00rootroot00000000000000#ifndef _ANTISPAM_PLUGIN_H #define _ANTISPAM_PLUGIN_H #include "lib.h" #include "str.h" #include "dovecot-version.h" #if DOVECOT_IS_GE(2, 0) #include "imap-client.h" #else #include "client.h" #endif #include "ostream.h" #include "imap-search.h" #include #define __stringify_1(x) #x #define stringify(x) __stringify_1(x) #define __PLUGIN_FUNCTION(name, ioe) \ name ## _plugin_ ## ioe #define _PLUGIN_FUNCTION(name, ioe) \ __PLUGIN_FUNCTION(name, ioe) #define PLUGIN_FUNCTION(ioe) \ _PLUGIN_FUNCTION(antispam, ioe) struct antispam_transaction_context; enum classification { CLASS_NOTSPAM, CLASS_SPAM, }; enum antispam_debug_target { ADT_NONE, ADT_STDERR, ADT_SYSLOG, }; struct signature_config { const char *signature_hdr; bool signature_nosig_ignore; }; struct antispam_debug_config { const char *prefix; enum antispam_debug_target target; int verbose; }; struct antispam_config { /* the selected backend */ struct backend *backend; struct antispam_debug_config dbgcfg; bool can_append_to_spam; bool need_keyword_hook; bool need_folder_hook; bool nosig_ignore; /* * There are three different matches for each folder type * * type usage * ---------------------------- * MT_REG plain strcmp() * MT_PATTERN case sensitive match with possible wildcards * MT_PATTERN_IGNCASE case insensitive match with possible wildcards */ char **trash_folders[3]; // = { NULL, NULL, NULL }; char **spam_folders[3]; // = { default_spam_folders,NULL, NULL }; char **unsure_folders[3]; // = { NULL, NULL, NULL }; char **spam_keywords; const char *signature_hdr; pool_t mem_pool; union { struct { struct signature_config sigcfg; const char *binary; const char *result_header; char **result_bl; int result_bl_num; char **extra_args; int extra_args_num; char **extra_env; int extra_env_num; } dspam; struct { struct signature_config sigcfg; const char *reaver_binary; char **extra_args; int extra_args_num; char **extra_env; int extra_env_num; } crm; struct { char **spam_args; int spam_args_num; char **ham_args; int ham_args_num; const char *pipe_binary;// = "/usr/sbin/sendmail"; const char *tmpdir;// = "/tmp"; char **extra_args; int extra_args_num; } pipe; struct { const char *spamspool, *hamspool; } s2d; }; }; struct backend { void (*init)(struct antispam_config *cfg, const char *(getenv)(const char *env, void *data), void *getenv_data); /* * Handle mail; parameters are * - t: transaction context * - ast: transaction context from backend_start() * - mail: the message * - wanted: the wanted classification determined by the user */ int (*handle_mail)(const struct antispam_config *cfg, struct mailbox_transaction_context *t, struct antispam_transaction_context *ast, struct mail *mail, enum classification wanted); struct antispam_transaction_context *(*start)(const struct antispam_config *cfg, struct mailbox *box); void (*rollback)(const struct antispam_config *cfg, struct antispam_transaction_context *ast); int (*commit)(const struct antispam_config *cfg, struct mailbox_transaction_context *ctx, struct antispam_transaction_context *ast); }; /* signature handling */ struct siglist { struct siglist *next; char *sig; enum classification wanted; }; void signature_init(struct signature_config *cfg, const struct antispam_debug_config *dbgcfg, const char *(getenv)(const char *env, void *data), void *getenv_data); int signature_extract_to_list(const struct signature_config *cfg, struct mailbox_transaction_context *t, struct mail *mail, struct siglist **list, enum classification wanted); int signature_extract(const struct signature_config *cfg, struct mailbox_transaction_context *t, struct mail *mail, const char **signature); void signature_list_free(struct siglist **list); /* possible backends */ extern struct backend crm114_backend; extern struct backend dspam_backend; extern struct backend pipe_backend; extern struct backend spool2dir_backend; int debug_init(struct antispam_debug_config *cfg, const char *(getenv)(const char *env, void *data), void *getenv_data); void debug(const struct antispam_debug_config *cfg, const char *fmt, ...) __attribute__ ((format (printf, 2, 3))); void debugv(const struct antispam_debug_config *cfg, char **args); void debugv_not_stderr(const struct antispam_debug_config *cfg, char **args); void debug_verbose(const struct antispam_debug_config *cfg, const char *fmt, ...) __attribute__ ((format (printf, 2, 3))); bool mailbox_is_spam(const struct antispam_config *cfg, struct mailbox *box); bool mailbox_is_trash(const struct antispam_config *cfg, struct mailbox *box); bool mailbox_is_unsure(const struct antispam_config *cfg, struct mailbox *box); bool keyword_is_spam(const struct antispam_config *cfg, const char *keyword); struct antispam_config * antispam_setup_config(const char *(getenv)(const char *env, void *data), void *getenv_data); void antispam_free_config(struct antispam_config *cfg); /* * Dovecot version compat code */ #if DOVECOT_IS_EQ(1, 0) #define module_arg void #define ATTR_UNUSED __attr_unused__ #define mempool_unref(x) pool_unref(*(x)) #define ME(err) #define str_array_length(x) strarray_length(x) static inline const char *const * get_mail_headers(struct mail *mail, const char *hdr) { return mail_get_headers(mail, hdr); } static inline struct ostream * o_stream_create_from_fd(int fd, pool_t pool) { return o_stream_create_file(fd, pool, 0, TRUE); } static inline int _mail_get_stream(struct mail *mail, struct message_size *hdr_size, struct message_size *body_size, struct istream **stream) { struct istream *res = mail_get_stream(mail, hdr_size, body_size); if (res == NULL) return -1; *stream = res; return 0; } #define mail_get_stream _mail_get_stream #define T_BEGIN \ STMT_START { t_push(); #define T_END \ t_pop(); } STMT_END #elif DOVECOT_IS_EQ(1, 1) #define mempool_unref pool_unref #define module_arg void #define ME(err) MAIL_ERROR_ ##err, static inline const char *const * get_mail_headers(struct mail *mail, const char *hdr) { const char *const *ret; if (mail_get_headers(mail, hdr, &ret) < 0) return NULL; return ret; } static inline struct ostream * o_stream_create_from_fd(int fd, pool_t pool ATTR_UNUSED) { return o_stream_create_fd(fd, 0, TRUE); } #elif DOVECOT_IS_EQ(1, 2) #define mempool_unref pool_unref #define module_arg void #define ME(err) MAIL_ERROR_ ##err, static inline const char *const * get_mail_headers(struct mail *mail, const char *hdr) { const char *const *ret; if (mail_get_headers(mail, hdr, &ret) < 0) return NULL; return ret; } static inline struct ostream * o_stream_create_from_fd(int fd, pool_t pool ATTR_UNUSED) { return o_stream_create_fd(fd, 0, TRUE); } #elif DOVECOT_IS_EQ(2, 0) || DOVECOT_IS_EQ(2, 1) || DOVECOT_IS_EQ(2, 2) #define mempool_unref pool_unref #define module_arg struct module * #define ME(err) MAIL_ERROR_ ##err, static inline const char *const * get_mail_headers(struct mail *mail, const char *hdr) { const char *const *ret; if (mail_get_headers(mail, hdr, &ret) < 0) return NULL; return ret; } static inline struct ostream * o_stream_create_from_fd(int fd, pool_t pool ATTR_UNUSED) { return o_stream_create_fd(fd, 0, TRUE); } #else #error "Building against this dovecot version is not supported" #endif void antispam_storage_init(module_arg); void antispam_storage_deinit(void); #endif /* _ANTISPAM_PLUGIN_H */ dovecot-antispam-2.0+20170109/antispam-storage-1.0.c000066400000000000000000000345241303466642300214640ustar00rootroot00000000000000/* * Storage implementation for antispam plugin * Copyright 2007 Johannes Berg * * Derived from Quota plugin: * Copyright (C) 2005 Timo Sirainen */ #include #include "lib.h" #include "array.h" #include "istream.h" #include "mail-search.h" #include "mail-index.h" #include "mail-storage-private.h" #include "antispam-plugin.h" static void (*antispam_next_hook_mail_storage_created)(struct mail_storage *storage); static struct antispam_config *antispam_cfg; #define ANTISPAM_CONTEXT(obj) \ *((void **)array_idx_modifyable(&(obj)->module_contexts, \ antispam_storage_module_id)) struct antispam_mail_storage { struct mail_storage_vfuncs super; struct antispam *antispam; }; enum mailbox_move_type { MMT_APPEND, MMT_UNINTERESTING, MMT_TO_CLEAN, MMT_TO_SPAM, }; static enum classification move_to_class(enum mailbox_move_type tp) { switch (tp) { case MMT_TO_CLEAN: return CLASS_NOTSPAM; case MMT_TO_SPAM: return CLASS_SPAM; default: i_assert(0); } } struct antispam_mailbox { struct mailbox_vfuncs super; enum mailbox_move_type movetype; /* used to check if copy was implemented with save */ unsigned int save_hack:1; }; struct antispam_mail { struct mail_vfuncs super; }; static unsigned int antispam_storage_module_id = 0; static bool antispam_storage_module_id_set = FALSE; static int antispam_copy(struct mailbox_transaction_context *t, struct mail *mail, enum mail_flags flags, struct mail_keywords *keywords, struct mail *dest_mail) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); struct antispam_transaction_context *ast = ANTISPAM_CONTEXT(t); struct mail *copy_dest_mail; int ret; bool src_trash, dst_trash; if (dest_mail != NULL) copy_dest_mail = dest_mail; else copy_dest_mail = mail_alloc(t, MAIL_FETCH_PHYSICAL_SIZE, NULL); i_assert(mail->box); asbox->save_hack = FALSE; asbox->movetype = MMT_UNINTERESTING; if (mailbox_is_unsure(antispam_cfg, t->box)) { mail_storage_set_error(t->box->storage, "Cannot copy to unsure folder"); return -1; } src_trash = mailbox_is_trash(antispam_cfg, mail->box); dst_trash = mailbox_is_trash(antispam_cfg, t->box); debug_verbose(&antispam_cfg->dbgcfg, "mail copy: from trash: %d, to trash: %d\n", src_trash, dst_trash); if (!src_trash && !dst_trash) { bool src_spam = mailbox_is_spam(antispam_cfg, mail->box); bool dst_spam = mailbox_is_spam(antispam_cfg, t->box); bool src_unsu = mailbox_is_unsure(antispam_cfg, mail->box); debug_verbose(&antispam_cfg->dbgcfg, "mail copy: src spam: %d, dst spam: %d," " src unsure: %d\n", src_spam, dst_spam, src_unsu); if ((src_spam || src_unsu) && !dst_spam) asbox->movetype = MMT_TO_CLEAN; else if ((!src_spam || src_unsu) && dst_spam) asbox->movetype = MMT_TO_SPAM; } if (asbox->super.copy(t, mail, flags, keywords, copy_dest_mail) < 0) return -1; /* * If copying used saving internally, we already have treated the mail */ if (asbox->save_hack || asbox->movetype == MMT_UNINTERESTING) ret = 0; else ret = antispam_cfg->backend->handle_mail(antispam_cfg, t, ast, copy_dest_mail, move_to_class(asbox->movetype)); /* * Both save_hack and movetype are only valid within a copy operation, * i.e. they are now invalid. Because, in theory, another operation * could be done after mailbox_open(), we need to reset the movetype * variable here. save_hack doesn't need to be reset because it is * only ever set within the save function and tested within this copy * function after being reset at the beginning of the copy, movetype * however is tested within the save_finish() function and a subsequent * save to the mailbox should not invoke the backend. */ asbox->movetype = MMT_APPEND; if (copy_dest_mail != dest_mail) mail_free(©_dest_mail); return ret; } static int antispam_save_init(struct mailbox_transaction_context *t, enum mail_flags flags, struct mail_keywords *keywords, time_t received_date, int timezone_offset, const char *from_envelope, struct istream *input, bool want_mail, struct mail_save_context **ctx_r) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); want_mail = TRUE; return asbox->super.save_init(t, flags, keywords, received_date, timezone_offset, from_envelope, input, want_mail, ctx_r); } static int antispam_save_finish(struct mail_save_context *ctx, struct mail *dest_mail) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->transaction->box); struct antispam_transaction_context *ast = ANTISPAM_CONTEXT(ctx->transaction); struct mail *save_dest_mail; int ret; if (dest_mail != NULL) save_dest_mail = dest_mail; else save_dest_mail = mail_alloc(ctx->transaction, MAIL_FETCH_PHYSICAL_SIZE, NULL); if (asbox->super.save_finish(ctx, save_dest_mail) < 0) return -1; asbox->save_hack = TRUE; ret = 0; switch (asbox->movetype) { case MMT_UNINTERESTING: break; case MMT_APPEND: /* Disallow APPENDs to UNSURE folders. */ if (mailbox_is_unsure(antispam_cfg, save_dest_mail->box)) { ret = -1; mail_storage_set_error(save_dest_mail->box->storage, "Cannot APPEND to an UNSURE folder."); break; } else if (mailbox_is_spam(antispam_cfg, save_dest_mail->box)) { /* * The client is APPENDing a message to a SPAM folder * so we try to train the backend on it. For most of * the backends, that can only succeed if the message * contains appropriate information. * * This happens especially when offlineimap is used and * the user moved a message to the SPAM folder while * offline---offlineimap cannot reproduce the COPY but * rather APPENDs the moved message on the next sync. * * This could be a bad if the spam headers were not * generated on our server, but since the user can * always APPEND to another folder and then COPY to a * SPAM folder backends need to be prepared for cases * like this anyway. With dspam, for example, the worst * that can happen is that the APPEND fails with a * training error from dspam. * * Unfortunately, we cannot handle the cases where * (1) the user moved a message from one folder that * contains SPAM to another folder containing SPAM * (2) the user moved a message out of the SPAM folder * (3) the user recovered a message from trash * * Because of these limitations, this behaviour needs * to be enabled with an option. */ if (!antispam_cfg->can_append_to_spam) { ret = -1; mail_storage_set_error( save_dest_mail->box->storage, "Cannot APPEND to a SPAM folder."); break; } asbox->movetype = MMT_TO_SPAM; /* fall through to default case to invoke backend */ } else { /* neither UNSURE nor SPAM, regular folder */ break; } /* fall through */ default: ret = antispam_cfg->backend->handle_mail(antispam_cfg, ctx->transaction, ast, save_dest_mail, move_to_class(asbox->movetype)); } if (save_dest_mail != dest_mail) mail_free(&save_dest_mail); return ret; } static struct antispam_transaction_context * antispam_transaction_begin(struct mailbox *box) { struct antispam_transaction_context *ast; ast = antispam_cfg->backend->start(antispam_cfg, box); i_assert(ast != NULL); return ast; } static void antispam_transaction_rollback(struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; antispam_cfg->backend->rollback(antispam_cfg, ast); *_ast = NULL; } static int antispam_transaction_commit(struct mailbox_transaction_context *ctx, struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; int ret; ret = antispam_cfg->backend->commit(antispam_cfg, ctx, ast); *_ast = NULL; return ret; } static int antispam_mail_update_keywords(struct mail *mail, enum modify_type modify_type, struct mail_keywords *keywords) { struct mail_private *pmail = (struct mail_private *)mail; struct antispam_mail *amail = ANTISPAM_CONTEXT(pmail); int ret; unsigned int i, numkwds; const array_t *ARRAY_DEFINE_PTR(idxkwd,const char *); const char *const *keyword_names; const char *const *orig_keywords; bool previous_spam_keyword, now_spam_keyword; idxkwd = mail_index_get_keywords(keywords->index); keyword_names = array_get(idxkwd, &numkwds); switch (modify_type) { case MODIFY_ADD: debug(&antispam_cfg->dbgcfg, "adding keyword(s)\n"); break; case MODIFY_REMOVE: debug(&antispam_cfg->dbgcfg, "removing keyword(s)\n"); break; case MODIFY_REPLACE: debug(&antispam_cfg->dbgcfg, "replacing keyword(s)\n"); break; default: i_assert(0); } orig_keywords = pmail->v.get_keywords(mail); if (orig_keywords) { debug(&antispam_cfg->dbgcfg, "original keyword list:\n"); while (*orig_keywords) { debug(&antispam_cfg->dbgcfg, " * %s\n", *orig_keywords); if (keyword_is_spam(antispam_cfg, *orig_keywords)) previous_spam_keyword = TRUE; orig_keywords++; } } debug(&antispam_cfg->dbgcfg, "keyword list:\n"); for (i = 0; i < keywords->count; i++) { unsigned int idx = keywords->idx[i]; i_assert(idx < numkwds); debug(&antispam_cfg->dbgcfg, " * %s\n", keyword_names[idx]); switch (modify_type) { case MODIFY_ADD: case MODIFY_REPLACE: if (keyword_is_spam(antispam_cfg, keyword_names[idx])) now_spam_keyword = TRUE; break; case MODIFY_REMOVE: if (keyword_is_spam(antispam_cfg, keyword_names[idx])) now_spam_keyword = FALSE; break; default: i_assert(0); } } ret = amail->super.update_keywords(mail, modify_type, keywords); debug(&antispam_cfg->dbgcfg, "ret = %d\n", ret); debug(&antispam_cfg->dbgcfg, "previous-spam, now-spam: %d, %d\n", previous_spam_keyword, now_spam_keyword); if (previous_spam_keyword != now_spam_keyword) { /* * Call backend here. * * TODO: It is not clear how to roll back the * keyword change if the backend fails. */ } return ret; } static void antispam_mail_free(struct mail *mail) { struct mail_private *pmail = (struct mail_private *)mail; struct antispam_mail *amail = ANTISPAM_CONTEXT(pmail); amail->super.free(mail); i_free(amail); } static struct mailbox_transaction_context * antispam_mailbox_transaction_begin(struct mailbox *box, enum mailbox_transaction_flags flags) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(box); struct mailbox_transaction_context *t; struct antispam_transaction_context *ast; t = asbox->super.transaction_begin(box, flags); ast = antispam_transaction_begin(box); array_idx_set(&t->module_contexts, antispam_storage_module_id, &ast); return t; } static int antispam_mailbox_transaction_commit(struct mailbox_transaction_context *ctx, enum mailbox_sync_flags flags) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_transaction_context *ast = ANTISPAM_CONTEXT(ctx); if (antispam_transaction_commit(ctx, &ast) < 0) { asbox->super.transaction_rollback(ctx); return -1; } else return asbox->super.transaction_commit(ctx, flags); } static void antispam_mailbox_transaction_rollback(struct mailbox_transaction_context *ctx) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_transaction_context *ast = ANTISPAM_CONTEXT(ctx); antispam_transaction_rollback(&ast); asbox->super.transaction_rollback(ctx); } static struct mail * antispam_mailbox_mail_alloc(struct mailbox_transaction_context *ctx, enum mail_fetch_field wanted_fields, struct mailbox_header_lookup_ctx *wanted_headers) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_mail *amail = i_new(struct antispam_mail, 1); struct mail_private *pmail; /* XXX: is this cast the right thing to do? */ pmail = (struct mail_private *) asbox->super.mail_alloc( ctx, wanted_fields, wanted_headers); amail->super = pmail->v; array_idx_set(&pmail->module_contexts, antispam_storage_module_id, &amail); pmail->v.update_keywords = antispam_mail_update_keywords; pmail->v.free = antispam_mail_free; return (struct mail *)pmail; } static struct mailbox *antispam_mailbox_open(struct mail_storage *storage, const char *name, struct istream *input, enum mailbox_open_flags flags) { struct antispam_mail_storage *as_storage = ANTISPAM_CONTEXT(storage); struct mailbox *box; struct antispam_mailbox *asbox; box = as_storage->super.mailbox_open(storage, name, input, flags); if (box == NULL) return NULL; asbox = p_new(box->pool, struct antispam_mailbox, 1); asbox->super = box->v; asbox->save_hack = FALSE; asbox->movetype = MMT_APPEND; if (antispam_cfg->need_folder_hook) { /* override save_init to override want_mail, we need that */ box->v.save_init = antispam_save_init; box->v.save_finish = antispam_save_finish; box->v.transaction_begin = antispam_mailbox_transaction_begin; box->v.transaction_commit = antispam_mailbox_transaction_commit; box->v.transaction_rollback = antispam_mailbox_transaction_rollback; box->v.copy = antispam_copy; } if (antispam_cfg->need_keyword_hook) box->v.mail_alloc = antispam_mailbox_mail_alloc; array_idx_set(&box->module_contexts, antispam_storage_module_id, &asbox); return box; } void antispam_mail_storage_created(struct mail_storage *storage) { struct antispam_mail_storage *as_storage; if (antispam_next_hook_mail_storage_created != NULL) antispam_next_hook_mail_storage_created(storage); as_storage = p_new(storage->pool, struct antispam_mail_storage, 1); as_storage->super = storage->v; storage->v.mailbox_open = antispam_mailbox_open; if (!antispam_storage_module_id_set) { antispam_storage_module_id = mail_storage_module_id++; antispam_storage_module_id_set = TRUE; } array_idx_set(&storage->module_contexts, antispam_storage_module_id, &as_storage); } static const char *_getenv(const char *env, void *data __attr_unused__) { t_push(); env = t_str_ucase(t_strconcat("antispam_", env, NULL)); env = getenv(env); t_pop(); return env; } /* defined by imap, pop3, lda */ extern void (*hook_mail_storage_created)(struct mail_storage *storage); void antispam_storage_init(void) { antispam_next_hook_mail_storage_created = hook_mail_storage_created; hook_mail_storage_created = antispam_mail_storage_created; antispam_cfg = antispam_setup_config(_getenv, NULL); } void antispam_storage_deinit(void) { antispam_free_config(antispam_cfg); } dovecot-antispam-2.0+20170109/antispam-storage-1.1.c000066400000000000000000000355701303466642300214670ustar00rootroot00000000000000/* * Storage implementation for antispam plugin * Copyright 2007-2008 Johannes Berg * * Derived from Quota plugin: * Copyright (C) 2005 Timo Sirainen */ #include #include "lib.h" #include "array.h" #include "istream.h" #include "mail-search.h" #include "mail-index.h" #include "mailbox-list-private.h" #include "mail-storage-private.h" #include "antispam-plugin.h" static void (*antispam_next_hook_mail_storage_created)(struct mail_storage *storage); static struct antispam_config *antispam_cfg; uint32_t PLUGIN_FUNCTION(id) = 0; #define ANTISPAM_CONTEXT(obj) \ MODULE_CONTEXT(obj, antispam_storage_module) #define ANTISPAM_MAIL_CONTEXT(obj) \ MODULE_CONTEXT(obj, antispam_mail_module) static MODULE_CONTEXT_DEFINE_INIT(antispam_storage_module, &mail_storage_module_register); static MODULE_CONTEXT_DEFINE_INIT(antispam_mail_module, &mail_module_register); enum mailbox_move_type { MMT_APPEND, MMT_UNINTERESTING, MMT_TO_CLEAN, MMT_TO_SPAM, }; struct antispam_internal_context { union mailbox_transaction_module_context module_ctx; struct antispam_transaction_context *backendctx; struct mail *mail; }; static enum classification move_to_class(enum mailbox_move_type tp) { switch (tp) { case MMT_TO_CLEAN: return CLASS_NOTSPAM; case MMT_TO_SPAM: return CLASS_SPAM; default: i_assert(0); } } struct antispam_mailbox { union mailbox_module_context module_ctx; enum mailbox_move_type movetype; /* used to check if copy was implemented with save */ unsigned int save_hack:1; }; static uint32_t antispam_storage_module_id = 0; static bool antispam_storage_module_id_set = FALSE; static int antispam_copy(struct mailbox_transaction_context *t, struct mail *mail, enum mail_flags flags, struct mail_keywords *keywords, struct mail *dest_mail) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t); int ret; bool src_trash, dst_trash; if (!dest_mail) { /* always need mail */ if (!ast->mail) ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY, NULL); dest_mail = ast->mail; } i_assert(mail->box); asbox->save_hack = FALSE; asbox->movetype = MMT_UNINTERESTING; if (mailbox_is_unsure(antispam_cfg, t->box)) { mail_storage_set_error(t->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot copy to unsure folder"); return -1; } src_trash = mailbox_is_trash(antispam_cfg, mail->box); dst_trash = mailbox_is_trash(antispam_cfg, t->box); debug_verbose(&antispam_cfg->dbgcfg, "mail copy: from trash: %d, to trash: %d\n", src_trash, dst_trash); if (!src_trash && !dst_trash) { bool src_spam = mailbox_is_spam(antispam_cfg, mail->box); bool dst_spam = mailbox_is_spam(antispam_cfg, t->box); bool src_unsu = mailbox_is_unsure(antispam_cfg, mail->box); debug_verbose(&antispam_cfg->dbgcfg, "mail copy: src spam: %d, dst spam: %d," " src unsure: %d\n", src_spam, dst_spam, src_unsu); if ((src_spam || src_unsu) && !dst_spam) asbox->movetype = MMT_TO_CLEAN; else if ((!src_spam || src_unsu) && dst_spam) asbox->movetype = MMT_TO_SPAM; } if (asbox->module_ctx.super.copy(t, mail, flags, keywords, dest_mail) < 0) return -1; /* * If copying used saving internally, we already have treated the mail */ if (asbox->save_hack || asbox->movetype == MMT_UNINTERESTING) ret = 0; else ret = antispam_cfg->backend->handle_mail(antispam_cfg, t, ast->backendctx, dest_mail, move_to_class(asbox->movetype)); /* * Both save_hack and movetype are only valid within a copy operation, * i.e. they are now invalid. Because, in theory, another operation * could be done after mailbox_open(), we need to reset the movetype * variable here. save_hack doesn't need to be reset because it is * only ever set within the save function and tested within this copy * function after being reset at the beginning of the copy, movetype * however is tested within the save_finish() function and a subsequent * save to the mailbox should not invoke the backend. */ asbox->movetype = MMT_APPEND; return ret; } static int antispam_save_init(struct mailbox_transaction_context *t, enum mail_flags flags, struct mail_keywords *keywords, time_t received_date, int timezone_offset, const char *from_envelope, struct istream *input, struct mail *dest_mail, struct mail_save_context **ctx_r) { struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t); struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); int ret; if (!dest_mail) { if (!ast->mail) ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY, NULL); dest_mail = ast->mail; } ret = asbox->module_ctx.super.save_init(t, flags, keywords, received_date, timezone_offset, from_envelope, input, dest_mail, ctx_r); if (ret >= 0) (*ctx_r)->dest_mail = dest_mail; return ret; } static int antispam_save_finish(struct mail_save_context *ctx) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->transaction->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx->transaction); struct mail *dest_mail; int ret; if (asbox->module_ctx.super.save_finish(ctx) < 0) return -1; dest_mail = ctx->dest_mail ? : ast->mail; asbox->save_hack = TRUE; ret = 0; switch (asbox->movetype) { case MMT_UNINTERESTING: break; case MMT_APPEND: /* Disallow APPENDs to UNSURE folders. */ if (mailbox_is_unsure(antispam_cfg, dest_mail->box)) { ret = -1; mail_storage_set_error(dest_mail->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot APPEND to an UNSURE folder."); break; } else if (mailbox_is_spam(antispam_cfg, dest_mail->box)) { /* * The client is APPENDing a message to a SPAM folder * so we try to train the backend on it. For most of * the backends, that can only succeed if the message * contains appropriate information. * * This happens especially when offlineimap is used and * the user moved a message to the SPAM folder while * offline---offlineimap cannot reproduce the COPY but * rather APPENDs the moved message on the next sync. * * This could be a bad if the spam headers were not * generated on our server, but since the user can * always APPEND to another folder and then COPY to a * SPAM folder backends need to be prepared for cases * like this anyway. With dspam, for example, the worst * that can happen is that the APPEND fails with a * training error from dspam. * * Unfortunately, we cannot handle the cases where * (1) the user moved a message from one folder that * contains SPAM to another folder containing SPAM * (2) the user moved a message out of the SPAM folder * (3) the user recovered a message from trash * * Because of these limitations, this behaviour needs * to be enabled with an option. */ if (!antispam_cfg->can_append_to_spam) { ret = -1; mail_storage_set_error( dest_mail->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot APPEND to a SPAM folder."); break; } asbox->movetype = MMT_TO_SPAM; /* fall through to default case to invoke backend */ } else { /* neither UNSURE nor SPAM, regular folder */ break; } /* fall through */ default: ret = antispam_cfg->backend->handle_mail(antispam_cfg, ctx->transaction, ast->backendctx, dest_mail, move_to_class(asbox->movetype)); } return ret; } static struct antispam_transaction_context * antispam_transaction_begin(struct mailbox *box) { struct antispam_transaction_context *ast; ast = antispam_cfg->backend->start(antispam_cfg, box); i_assert(ast != NULL); return ast; } static void antispam_transaction_rollback(struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; antispam_cfg->backend->rollback(antispam_cfg, ast); *_ast = NULL; } static int antispam_transaction_commit(struct mailbox_transaction_context *ctx, struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; int ret; ret = antispam_cfg->backend->commit(antispam_cfg, ctx, ast); *_ast = NULL; return ret; } static void antispam_mail_update_keywords(struct mail *mail, enum modify_type modify_type, struct mail_keywords *keywords) { struct mail_private *pmail = (struct mail_private *)mail; union mail_module_context *amail = ANTISPAM_MAIL_CONTEXT(pmail); unsigned int i, numkwds; const ARRAY_TYPE(keywords) *idxkwd = mail_index_get_keywords(keywords->index); const char *const *keyword_names = array_get(idxkwd, &numkwds); const char *const *orig_keywords; bool previous_spam_keyword, now_spam_keyword; switch (modify_type) { case MODIFY_ADD: debug(&antispam_cfg->dbgcfg, "adding keyword(s)\n"); break; case MODIFY_REMOVE: debug(&antispam_cfg->dbgcfg, "removing keyword(s)\n"); break; case MODIFY_REPLACE: debug(&antispam_cfg->dbgcfg, "replacing keyword(s)\n"); break; default: i_assert(0); } orig_keywords = pmail->v.get_keywords(mail); if (orig_keywords) { debug(&antispam_cfg->dbgcfg, "original keyword list:\n"); while (*orig_keywords) { debug(&antispam_cfg->dbgcfg, " * %s\n", *orig_keywords); if (keyword_is_spam(antispam_cfg, *orig_keywords)) previous_spam_keyword = TRUE; orig_keywords++; } } debug(&antispam_cfg->dbgcfg, "keyword list:\n"); for (i = 0; i < keywords->count; i++) { unsigned int idx = keywords->idx[i]; i_assert(idx < numkwds); debug(&antispam_cfg->dbgcfg, " * %s\n", keyword_names[idx]); switch (modify_type) { case MODIFY_ADD: case MODIFY_REPLACE: if (keyword_is_spam(antispam_cfg, keyword_names[idx])) now_spam_keyword = TRUE; break; case MODIFY_REMOVE: if (keyword_is_spam(antispam_cfg, keyword_names[idx])) now_spam_keyword = FALSE; break; default: i_assert(0); } } amail->super.update_keywords(mail, modify_type, keywords); debug(&antispam_cfg->dbgcfg, "previous-spam, now-spam: %d, %d\n", previous_spam_keyword, now_spam_keyword); if (previous_spam_keyword != now_spam_keyword) { /* * Call backend here. * * TODO: It is not clear how to roll back the * keyword change if the backend fails. */ } } static struct mailbox_transaction_context * antispam_mailbox_transaction_begin(struct mailbox *box, enum mailbox_transaction_flags flags) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(box); struct mailbox_transaction_context *t; struct antispam_transaction_context *ast; struct antispam_internal_context *aic; t = asbox->module_ctx.super.transaction_begin(box, flags); aic = i_new(struct antispam_internal_context, 1); ast = antispam_transaction_begin(box); aic->backendctx = ast; MODULE_CONTEXT_SET(t, antispam_storage_module, aic); return t; } static int antispam_mailbox_transaction_commit(struct mailbox_transaction_context *ctx, uint32_t *uid_validity_r, uint32_t *first_saved_uid_r, uint32_t *last_saved_uid_r) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx); if (antispam_transaction_commit(ctx, &ast->backendctx) < 0) { if (ast->mail) mail_free(&ast->mail); asbox->module_ctx.super.transaction_rollback(ctx); return -1; } if (ast->mail) mail_free(&ast->mail); return asbox->module_ctx.super.transaction_commit(ctx, uid_validity_r, first_saved_uid_r, last_saved_uid_r); } static void antispam_mailbox_transaction_rollback(struct mailbox_transaction_context *ctx) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx); if (ast->mail) mail_free(&ast->mail); asbox->module_ctx.super.transaction_rollback(ctx); antispam_transaction_rollback(&ast->backendctx); } static struct mail * antispam_mailbox_mail_alloc(struct mailbox_transaction_context *ctx, enum mail_fetch_field wanted_fields, struct mailbox_header_lookup_ctx *wanted_headers) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); union mail_module_context *amail; struct mail *_mail; struct mail_private *mail; _mail = asbox->module_ctx.super. mail_alloc(ctx, wanted_fields, wanted_headers); mail = (struct mail_private *)_mail; amail = p_new(mail->pool, union mail_module_context, 1); amail->super = mail->v; mail->v.update_keywords = antispam_mail_update_keywords; MODULE_CONTEXT_SET_SELF(mail, antispam_mail_module, amail); return _mail; } static struct mailbox *antispam_mailbox_open(struct mail_storage *storage, const char *name, struct istream *input, enum mailbox_open_flags flags) { union mail_storage_module_context *as_storage = ANTISPAM_CONTEXT(storage); struct mailbox *box; struct antispam_mailbox *asbox; box = as_storage->super.mailbox_open(storage, name, input, flags); if (box == NULL) return NULL; asbox = p_new(box->pool, struct antispam_mailbox, 1); asbox->module_ctx.super = box->v; asbox->save_hack = FALSE; asbox->movetype = MMT_APPEND; if (antispam_cfg->need_folder_hook) { /* override save_init to override want_mail, we need that */ box->v.save_init = antispam_save_init; box->v.save_finish = antispam_save_finish; box->v.transaction_begin = antispam_mailbox_transaction_begin; box->v.transaction_commit = antispam_mailbox_transaction_commit; box->v.transaction_rollback = antispam_mailbox_transaction_rollback; box->v.copy = antispam_copy; } if (antispam_cfg->need_keyword_hook) box->v.mail_alloc = antispam_mailbox_mail_alloc; MODULE_CONTEXT_SET(box, antispam_storage_module, asbox); return box; } void antispam_mail_storage_created(struct mail_storage *storage) { union mail_storage_module_context *as_storage; if (antispam_next_hook_mail_storage_created != NULL) antispam_next_hook_mail_storage_created(storage); as_storage = p_new(storage->pool, union mail_storage_module_context, 1); as_storage->super = storage->v; storage->v.mailbox_open = antispam_mailbox_open; if (!antispam_storage_module_id_set) { antispam_storage_module_id = PLUGIN_FUNCTION(id); antispam_storage_module_id_set = TRUE; } MODULE_CONTEXT_SET_SELF(storage, antispam_storage_module, as_storage); } static const char *_getenv(const char *env, void *data ATTR_UNUSED) { T_BEGIN { env = t_str_ucase(t_strconcat("antispam_", env, NULL)); env = getenv(env); } T_END; return env; } /* defined by imap, pop3, lda */ extern void (*hook_mail_storage_created)(struct mail_storage *storage); void antispam_storage_init(void) { antispam_next_hook_mail_storage_created = hook_mail_storage_created; hook_mail_storage_created = antispam_mail_storage_created; antispam_cfg = antispam_setup_config(_getenv, NULL); } void antispam_storage_deinit(void) { antispam_free_config(antispam_cfg); } dovecot-antispam-2.0+20170109/antispam-storage-1.2.c000066400000000000000000000351311303466642300214610ustar00rootroot00000000000000/* * Storage implementation for antispam plugin * Copyright 2007-2008 Johannes Berg * Copyright 2009 Jonas Maurus * * Derived from Quota plugin: * Copyright (C) 2005 Timo Sirainen */ #include #include "lib.h" #include "array.h" #include "istream.h" #include "mail-search.h" #include "mail-index.h" #include "mailbox-list-private.h" #include "mail-storage-private.h" #include "antispam-plugin.h" static void (*antispam_next_hook_mail_storage_created)(struct mail_storage *storage); static struct antispam_config *antispam_cfg; uint32_t PLUGIN_FUNCTION(id) = 0; #define ANTISPAM_CONTEXT(obj) \ MODULE_CONTEXT(obj, antispam_storage_module) #define ANTISPAM_MAIL_CONTEXT(obj) \ MODULE_CONTEXT(obj, antispam_mail_module) static MODULE_CONTEXT_DEFINE_INIT(antispam_storage_module, &mail_storage_module_register); static MODULE_CONTEXT_DEFINE_INIT(antispam_mail_module, &mail_module_register); enum mailbox_move_type { MMT_APPEND, MMT_UNINTERESTING, MMT_TO_CLEAN, MMT_TO_SPAM, }; struct antispam_internal_context { union mailbox_transaction_module_context module_ctx; struct antispam_transaction_context *backendctx; struct mail *mail; }; static enum classification move_to_class(enum mailbox_move_type tp) { switch (tp) { case MMT_TO_CLEAN: return CLASS_NOTSPAM; case MMT_TO_SPAM: return CLASS_SPAM; default: i_assert(0); } } struct antispam_mailbox { union mailbox_module_context module_ctx; enum mailbox_move_type movetype; /* used to check if copy was implemented with save */ unsigned int save_hack:1; }; static uint32_t antispam_storage_module_id = 0; static bool antispam_storage_module_id_set = FALSE; static int antispam_copy(struct mail_save_context *ctx, struct mail *mail) { struct mailbox_transaction_context *t = ctx->transaction; struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t); int ret; bool src_trash, dst_trash; if (!ctx->dest_mail) { /* always need mail */ if (!ast->mail) ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY, NULL); ctx->dest_mail = ast->mail; } i_assert(mail->box); asbox->save_hack = FALSE; asbox->movetype = MMT_UNINTERESTING; if (mailbox_is_unsure(antispam_cfg, t->box)) { mail_storage_set_error(t->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot copy to unsure folder"); return -1; } src_trash = mailbox_is_trash(antispam_cfg, mail->box); dst_trash = mailbox_is_trash(antispam_cfg, t->box); debug_verbose(&antispam_cfg->dbgcfg, "mail copy: from trash: %d, to trash: %d\n", src_trash, dst_trash); if (!src_trash && !dst_trash) { bool src_spam = mailbox_is_spam(antispam_cfg, mail->box); bool dst_spam = mailbox_is_spam(antispam_cfg, t->box); bool src_unsu = mailbox_is_unsure(antispam_cfg, mail->box); debug_verbose(&antispam_cfg->dbgcfg, "mail copy: src spam: %d, dst spam: %d," " src unsure: %d\n", src_spam, dst_spam, src_unsu); if ((src_spam || src_unsu) && !dst_spam) asbox->movetype = MMT_TO_CLEAN; else if ((!src_spam || src_unsu) && dst_spam) asbox->movetype = MMT_TO_SPAM; } if (asbox->module_ctx.super.copy(ctx, mail) < 0) return -1; /* * If copying used saving internally, we already have treated the mail */ if (asbox->save_hack || asbox->movetype == MMT_UNINTERESTING) ret = 0; else ret = antispam_cfg->backend->handle_mail(antispam_cfg, t, ast->backendctx, ctx->dest_mail, move_to_class(asbox->movetype)); /* * Both save_hack and movetype are only valid within a copy operation, * i.e. they are now invalid. Because, in theory, another operation * could be done after mailbox_open(), we need to reset the movetype * variable here. save_hack doesn't need to be reset because it is * only ever set within the save function and tested within this copy * function after being reset at the beginning of the copy, movetype * however is tested within the save_finish() function and a subsequent * save to the mailbox should not invoke the backend. */ asbox->movetype = MMT_APPEND; return ret; } static int antispam_save_begin(struct mail_save_context *ctx, struct istream *input) { struct mailbox_transaction_context *t = ctx->transaction; struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t); struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); int ret; if (!ctx->dest_mail) { if (!ast->mail) ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY, NULL); ctx->dest_mail = ast->mail; } ret = asbox->module_ctx.super.save_begin(ctx, input); return ret; } static int antispam_save_finish(struct mail_save_context *ctx) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->transaction->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx->transaction); struct mail *dest_mail; int ret; if (asbox->module_ctx.super.save_finish(ctx) < 0) return -1; dest_mail = ctx->dest_mail ? : ast->mail; asbox->save_hack = TRUE; ret = 0; switch (asbox->movetype) { case MMT_UNINTERESTING: break; case MMT_APPEND: /* Disallow APPENDs to UNSURE folders. */ if (mailbox_is_unsure(antispam_cfg, dest_mail->box)) { ret = -1; mail_storage_set_error(dest_mail->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot APPEND to an UNSURE folder."); break; } else if (mailbox_is_spam(antispam_cfg, dest_mail->box)) { /* * The client is APPENDing a message to a SPAM folder * so we try to train the backend on it. For most of * the backends, that can only succeed if the message * contains appropriate information. * * This happens especially when offlineimap is used and * the user moved a message to the SPAM folder while * offline---offlineimap cannot reproduce the COPY but * rather APPENDs the moved message on the next sync. * * This could be a bad if the spam headers were not * generated on our server, but since the user can * always APPEND to another folder and then COPY to a * SPAM folder backends need to be prepared for cases * like this anyway. With dspam, for example, the worst * that can happen is that the APPEND fails with a * training error from dspam. * * Unfortunately, we cannot handle the cases where * (1) the user moved a message from one folder that * contains SPAM to another folder containing SPAM * (2) the user moved a message out of the SPAM folder * (3) the user recovered a message from trash * * Because of these limitations, this behaviour needs * to be enabled with an option. */ if (!antispam_cfg->can_append_to_spam) { ret = -1; mail_storage_set_error( dest_mail->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot APPEND to a SPAM folder."); break; } asbox->movetype = MMT_TO_SPAM; /* fall through to default case to invoke backend */ } else { /* neither UNSURE nor SPAM, regular folder */ break; } /* fall through */ default: ret = antispam_cfg->backend->handle_mail(antispam_cfg, ctx->transaction, ast->backendctx, dest_mail, move_to_class(asbox->movetype)); } return ret; } static struct antispam_transaction_context * antispam_transaction_begin(struct mailbox *box) { struct antispam_transaction_context *ast; ast = antispam_cfg->backend->start(antispam_cfg, box); i_assert(ast != NULL); return ast; } static void antispam_transaction_rollback(struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; antispam_cfg->backend->rollback(antispam_cfg, ast); *_ast = NULL; } static int antispam_transaction_commit(struct mailbox_transaction_context *ctx, struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; int ret; ret = antispam_cfg->backend->commit(antispam_cfg, ctx, ast); *_ast = NULL; return ret; } static void antispam_mail_update_keywords(struct mail *mail, enum modify_type modify_type, struct mail_keywords *keywords) { struct mail_private *pmail = (struct mail_private *)mail; union mail_module_context *amail = ANTISPAM_MAIL_CONTEXT(pmail); unsigned int i, numkwds; const ARRAY_TYPE(keywords) *idxkwd = mail_index_get_keywords(keywords->index); const char *const *keyword_names = array_get(idxkwd, &numkwds); const char *const *orig_keywords; bool previous_spam_keyword, now_spam_keyword; switch (modify_type) { case MODIFY_ADD: debug(&antispam_cfg->dbgcfg, "adding keyword(s)\n"); break; case MODIFY_REMOVE: debug(&antispam_cfg->dbgcfg, "removing keyword(s)\n"); break; case MODIFY_REPLACE: debug(&antispam_cfg->dbgcfg, "replacing keyword(s)\n"); break; default: i_assert(0); } orig_keywords = pmail->v.get_keywords(mail); if (orig_keywords) { debug(&antispam_cfg->dbgcfg, "original keyword list:\n"); while (*orig_keywords) { debug(&antispam_cfg->dbgcfg, " * %s\n", *orig_keywords); if (keyword_is_spam(antispam_cfg, *orig_keywords)) previous_spam_keyword = TRUE; orig_keywords++; } } debug(&antispam_cfg->dbgcfg, "keyword list:\n"); for (i = 0; i < keywords->count; i++) { unsigned int idx = keywords->idx[i]; i_assert(idx < numkwds); debug(&antispam_cfg->dbgcfg, " * %s\n", keyword_names[idx]); switch (modify_type) { case MODIFY_ADD: case MODIFY_REPLACE: if (keyword_is_spam(antispam_cfg, keyword_names[idx])) now_spam_keyword = TRUE; break; case MODIFY_REMOVE: if (keyword_is_spam(antispam_cfg, keyword_names[idx])) now_spam_keyword = FALSE; break; default: i_assert(0); } } amail->super.update_keywords(mail, modify_type, keywords); debug(&antispam_cfg->dbgcfg, "previous-spam, now-spam: %d, %d\n", previous_spam_keyword, now_spam_keyword); if (previous_spam_keyword != now_spam_keyword) { /* * Call backend here. * * TODO: It is not clear how to roll back the * keyword change if the backend fails. */ } } static struct mailbox_transaction_context * antispam_mailbox_transaction_begin(struct mailbox *box, enum mailbox_transaction_flags flags) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(box); struct mailbox_transaction_context *t; struct antispam_transaction_context *ast; struct antispam_internal_context *aic; t = asbox->module_ctx.super.transaction_begin(box, flags); aic = i_new(struct antispam_internal_context, 1); ast = antispam_transaction_begin(box); aic->backendctx = ast; MODULE_CONTEXT_SET(t, antispam_storage_module, aic); return t; } static int antispam_mailbox_transaction_commit(struct mailbox_transaction_context *ctx, uint32_t *uid_validity_r, uint32_t *first_saved_uid_r, uint32_t *last_saved_uid_r) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx); if (antispam_transaction_commit(ctx, &ast->backendctx) < 0) { if (ast->mail) mail_free(&ast->mail); asbox->module_ctx.super.transaction_rollback(ctx); return -1; } if (ast->mail) mail_free(&ast->mail); return asbox->module_ctx.super.transaction_commit(ctx, uid_validity_r, first_saved_uid_r, last_saved_uid_r); } static void antispam_mailbox_transaction_rollback(struct mailbox_transaction_context *ctx) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx); if (ast->mail) mail_free(&ast->mail); asbox->module_ctx.super.transaction_rollback(ctx); antispam_transaction_rollback(&ast->backendctx); } static struct mail * antispam_mailbox_mail_alloc(struct mailbox_transaction_context *ctx, enum mail_fetch_field wanted_fields, struct mailbox_header_lookup_ctx *wanted_headers) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); union mail_module_context *amail; struct mail *_mail; struct mail_private *mail; _mail = asbox->module_ctx.super. mail_alloc(ctx, wanted_fields, wanted_headers); mail = (struct mail_private *)_mail; amail = p_new(mail->pool, union mail_module_context, 1); amail->super = mail->v; mail->v.update_keywords = antispam_mail_update_keywords; MODULE_CONTEXT_SET_SELF(mail, antispam_mail_module, amail); return _mail; } static struct mailbox *antispam_mailbox_open(struct mail_storage *storage, const char *name, struct istream *input, enum mailbox_open_flags flags) { union mail_storage_module_context *as_storage = ANTISPAM_CONTEXT(storage); struct mailbox *box; struct antispam_mailbox *asbox; box = as_storage->super.mailbox_open(storage, name, input, flags); if (box == NULL) return NULL; asbox = p_new(box->pool, struct antispam_mailbox, 1); asbox->module_ctx.super = box->v; asbox->save_hack = FALSE; asbox->movetype = MMT_APPEND; if (antispam_cfg->need_folder_hook) { /* override save_init to override want_mail, we need that */ box->v.save_begin = antispam_save_begin; box->v.save_finish = antispam_save_finish; box->v.transaction_begin = antispam_mailbox_transaction_begin; box->v.transaction_commit = antispam_mailbox_transaction_commit; box->v.transaction_rollback = antispam_mailbox_transaction_rollback; box->v.copy = antispam_copy; } if (antispam_cfg->need_keyword_hook) box->v.mail_alloc = antispam_mailbox_mail_alloc; MODULE_CONTEXT_SET(box, antispam_storage_module, asbox); return box; } void antispam_mail_storage_created(struct mail_storage *storage) { union mail_storage_module_context *as_storage; if (antispam_next_hook_mail_storage_created != NULL) antispam_next_hook_mail_storage_created(storage); as_storage = p_new(storage->pool, union mail_storage_module_context, 1); as_storage->super = storage->v; storage->v.mailbox_open = antispam_mailbox_open; if (!antispam_storage_module_id_set) { antispam_storage_module_id = PLUGIN_FUNCTION(id); antispam_storage_module_id_set = TRUE; } MODULE_CONTEXT_SET_SELF(storage, antispam_storage_module, as_storage); } static const char *_getenv(const char *env, void *data ATTR_UNUSED) { T_BEGIN { env = t_str_ucase(t_strconcat("antispam_", env, NULL)); env = getenv(env); } T_END; return env; } /* defined by imap, pop3, lda */ extern void (*hook_mail_storage_created)(struct mail_storage *storage); void antispam_storage_init(void) { antispam_next_hook_mail_storage_created = hook_mail_storage_created; hook_mail_storage_created = antispam_mail_storage_created; antispam_cfg = antispam_setup_config(_getenv, NULL); } void antispam_storage_deinit(void) { antispam_free_config(antispam_cfg); } dovecot-antispam-2.0+20170109/antispam-storage-2.0.c000066400000000000000000000355301303466642300214630ustar00rootroot00000000000000/* * Storage implementation for antispam plugin * Copyright 2007-2008 Johannes Berg * Copyright 2009 Jonas Maurus * * Derived from Quota plugin: * Copyright (C) 2005 Timo Sirainen */ #include #include "lib.h" #include "array.h" #include "istream.h" #include "mail-search.h" #include "mail-index.h" #include "mailbox-list-private.h" #include "mail-storage-private.h" #include "antispam-plugin.h" #define ANTISPAM_CONTEXT(obj) \ MODULE_CONTEXT(obj, antispam_storage_module) #define ANTISPAM_USER_CONTEXT(obj) \ MODULE_CONTEXT(obj, antispam_user_module) #define ANTISPAM_MAIL_CONTEXT(obj) \ MODULE_CONTEXT(obj, antispam_mail_module) static MODULE_CONTEXT_DEFINE_INIT(antispam_storage_module, &mail_storage_module_register); static MODULE_CONTEXT_DEFINE_INIT(antispam_user_module, &mail_user_module_register); static MODULE_CONTEXT_DEFINE_INIT(antispam_mail_module, &mail_module_register); enum mailbox_move_type { MMT_APPEND, MMT_UNINTERESTING, MMT_TO_CLEAN, MMT_TO_SPAM, }; struct antispam_internal_context { union mailbox_transaction_module_context module_ctx; struct antispam_transaction_context *backendctx; struct mail *mail; }; static enum classification move_to_class(enum mailbox_move_type tp) { switch (tp) { case MMT_TO_CLEAN: return CLASS_NOTSPAM; case MMT_TO_SPAM: return CLASS_SPAM; default: i_assert(0); } } struct antispam_mailbox { union mailbox_module_context module_ctx; const struct antispam_config *cfg; enum mailbox_move_type movetype; /* used to check if copy was implemented with save */ unsigned int save_hack:1; }; struct antispam_mail_user { union mail_user_module_context module_ctx; struct antispam_config *cfg; }; struct antispam_mail { union mail_module_context module_ctx; const struct antispam_config *cfg; }; static int antispam_copy(struct mail_save_context *ctx, struct mail *mail) { struct mailbox_transaction_context *t = ctx->transaction; struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t); int ret; bool src_trash, dst_trash; if (!ctx->dest_mail) { /* always need mail */ if (!ast->mail) ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY, NULL); ctx->dest_mail = ast->mail; } i_assert(mail->box); asbox->save_hack = FALSE; asbox->movetype = MMT_UNINTERESTING; if (mailbox_is_unsure(asbox->cfg, t->box)) { mail_storage_set_error(t->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot copy to unsure folder"); return -1; } src_trash = mailbox_is_trash(asbox->cfg, mail->box); dst_trash = mailbox_is_trash(asbox->cfg, t->box); debug_verbose(&asbox->cfg->dbgcfg, "mail copy: from trash: %d, to trash: %d\n", src_trash, dst_trash); if (!src_trash && !dst_trash) { bool src_spam = mailbox_is_spam(asbox->cfg, mail->box); bool dst_spam = mailbox_is_spam(asbox->cfg, t->box); bool src_unsu = mailbox_is_unsure(asbox->cfg, mail->box); debug_verbose(&asbox->cfg->dbgcfg, "mail copy: src spam: %d, dst spam: %d," " src unsure: %d\n", src_spam, dst_spam, src_unsu); if ((src_spam || src_unsu) && !dst_spam) asbox->movetype = MMT_TO_CLEAN; else if ((!src_spam || src_unsu) && dst_spam) asbox->movetype = MMT_TO_SPAM; } if (asbox->module_ctx.super.copy(ctx, mail) < 0) return -1; /* * If copying used saving internally, we already have treated the mail */ if (asbox->save_hack || asbox->movetype == MMT_UNINTERESTING) ret = 0; else ret = asbox->cfg->backend->handle_mail( asbox->cfg, t, ast->backendctx, ctx->dest_mail, move_to_class(asbox->movetype)); /* * Both save_hack and movetype are only valid within a copy operation, * i.e. they are now invalid. Because, in theory, another operation * could be done after mailbox_open(), we need to reset the movetype * variable here. save_hack doesn't need to be reset because it is * only ever set within the save function and tested within this copy * function after being reset at the beginning of the copy, movetype * however is tested within the save_finish() function and a subsequent * save to the mailbox should not invoke the backend. */ asbox->movetype = MMT_APPEND; return ret; } static int antispam_save_begin(struct mail_save_context *ctx, struct istream *input) { struct mailbox_transaction_context *t = ctx->transaction; struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t); struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box); int ret; if (!ctx->dest_mail) { if (!ast->mail) ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY, NULL); ctx->dest_mail = ast->mail; } ret = asbox->module_ctx.super.save_begin(ctx, input); return ret; } static int antispam_save_finish(struct mail_save_context *ctx) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->transaction->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx->transaction); struct mail *dest_mail; int ret; if (asbox->module_ctx.super.save_finish(ctx) < 0) return -1; dest_mail = ctx->dest_mail ? : ast->mail; asbox->save_hack = TRUE; ret = 0; switch (asbox->movetype) { case MMT_UNINTERESTING: break; case MMT_APPEND: /* Disallow APPENDs to UNSURE folders. */ if (mailbox_is_unsure(asbox->cfg, dest_mail->box)) { ret = -1; mail_storage_set_error(dest_mail->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot APPEND to an UNSURE folder."); break; } else if (mailbox_is_spam(asbox->cfg, dest_mail->box)) { /* * The client is APPENDing a message to a SPAM folder * so we try to train the backend on it. For most of * the backends, that can only succeed if the message * contains appropriate information. * * This happens especially when offlineimap is used and * the user moved a message to the SPAM folder while * offline---offlineimap cannot reproduce the COPY but * rather APPENDs the moved message on the next sync. * * This could be a bad if the spam headers were not * generated on our server, but since the user can * always APPEND to another folder and then COPY to a * SPAM folder backends need to be prepared for cases * like this anyway. With dspam, for example, the worst * that can happen is that the APPEND fails with a * training error from dspam. * * Unfortunately, we cannot handle the cases where * (1) the user moved a message from one folder that * contains SPAM to another folder containing SPAM * (2) the user moved a message out of the SPAM folder * (3) the user recovered a message from trash * * Because of these limitations, this behaviour needs * to be enabled with an option. */ if (!asbox->cfg->can_append_to_spam) { ret = -1; mail_storage_set_error( dest_mail->box->storage, MAIL_ERROR_NOTPOSSIBLE, "Cannot APPEND to a SPAM folder."); break; } asbox->movetype = MMT_TO_SPAM; /* fall through to default case to invoke backend */ } else { /* neither UNSURE nor SPAM, regular folder */ break; } /* fall through */ default: ret = asbox->cfg->backend->handle_mail( asbox->cfg, ctx->transaction, ast->backendctx, dest_mail, move_to_class(asbox->movetype)); } return ret; } static struct antispam_transaction_context * antispam_transaction_begin(struct mailbox *box) { struct antispam_transaction_context *ast; struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(box); ast = asbox->cfg->backend->start(asbox->cfg, box); i_assert(ast != NULL); return ast; } static void antispam_transaction_rollback(const struct antispam_config *cfg, struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; cfg->backend->rollback(cfg, ast); *_ast = NULL; } static int antispam_transaction_commit(const struct antispam_config *cfg, struct mailbox_transaction_context *ctx, struct antispam_transaction_context **_ast) { struct antispam_transaction_context *ast = *_ast; int ret; ret = cfg->backend->commit(cfg, ctx, ast); *_ast = NULL; return ret; } static void antispam_mail_update_keywords(struct mail *mail, enum modify_type modify_type, struct mail_keywords *keywords) { struct mail_private *pmail = (struct mail_private *)mail; struct antispam_mail *amail = ANTISPAM_MAIL_CONTEXT(pmail); unsigned int i, numkwds; const ARRAY_TYPE(keywords) *idxkwd = mail_index_get_keywords(keywords->index); const char *const *keyword_names = array_get(idxkwd, &numkwds); const char *const *orig_keywords; bool previous_spam_keyword, now_spam_keyword; switch (modify_type) { case MODIFY_ADD: debug(&amail->cfg->dbgcfg, "adding keyword(s)\n"); break; case MODIFY_REMOVE: debug(&amail->cfg->dbgcfg, "removing keyword(s)\n"); break; case MODIFY_REPLACE: debug(&amail->cfg->dbgcfg, "replacing keyword(s)\n"); break; default: i_assert(0); } orig_keywords = pmail->v.get_keywords(mail); if (orig_keywords) { debug(&amail->cfg->dbgcfg, "original keyword list:\n"); while (*orig_keywords) { debug(&amail->cfg->dbgcfg, " * %s\n", *orig_keywords); if (keyword_is_spam(amail->cfg, *orig_keywords)) previous_spam_keyword = TRUE; orig_keywords++; } } debug(&amail->cfg->dbgcfg, "keyword list:\n"); for (i = 0; i < keywords->count; i++) { unsigned int idx = keywords->idx[i]; i_assert(idx < numkwds); debug(&amail->cfg->dbgcfg, " * %s\n", keyword_names[idx]); switch (modify_type) { case MODIFY_ADD: case MODIFY_REPLACE: if (keyword_is_spam(amail->cfg, keyword_names[idx])) now_spam_keyword = TRUE; break; case MODIFY_REMOVE: if (keyword_is_spam(amail->cfg, keyword_names[idx])) now_spam_keyword = FALSE; break; default: i_assert(0); } } amail->module_ctx.super.update_keywords(mail, modify_type, keywords); debug(&amail->cfg->dbgcfg, "previous-spam, now-spam: %d, %d\n", previous_spam_keyword, now_spam_keyword); if (previous_spam_keyword != now_spam_keyword) { /* * Call backend here. * * TODO: It is not clear how to roll back the * keyword change if the backend fails. */ } } static struct mailbox_transaction_context * antispam_mailbox_transaction_begin(struct mailbox *box, enum mailbox_transaction_flags flags) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(box); struct mailbox_transaction_context *t; struct antispam_transaction_context *ast; struct antispam_internal_context *aic; t = asbox->module_ctx.super.transaction_begin(box, flags); aic = i_new(struct antispam_internal_context, 1); ast = antispam_transaction_begin(box); aic->backendctx = ast; MODULE_CONTEXT_SET(t, antispam_storage_module, aic); return t; } static int antispam_mailbox_transaction_commit(struct mailbox_transaction_context *ctx, struct mail_transaction_commit_changes *changes_r) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx); if (antispam_transaction_commit(asbox->cfg, ctx, &ast->backendctx) < 0) { if (ast->mail) mail_free(&ast->mail); asbox->module_ctx.super.transaction_rollback(ctx); return -1; } if (ast->mail) mail_free(&ast->mail); return asbox->module_ctx.super.transaction_commit(ctx, changes_r); } static void antispam_mailbox_transaction_rollback(struct mailbox_transaction_context *ctx) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box); struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx); if (ast->mail) mail_free(&ast->mail); asbox->module_ctx.super.transaction_rollback(ctx); antispam_transaction_rollback(asbox->cfg, &ast->backendctx); } static void antispam_mail_allocated(struct mail *_mail) { struct mail_private *mail = (struct mail_private *)_mail; struct mail_vfuncs *v = mail->vlast; struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(_mail->box); struct antispam_mail *amail; if (asbox == NULL) return; amail = p_new(mail->pool, struct antispam_mail, 1); amail->module_ctx.super = *v; mail->vlast = &amail->module_ctx.super; amail->cfg = asbox->cfg; if (asbox->cfg->need_keyword_hook) v->update_keywords = antispam_mail_update_keywords; MODULE_CONTEXT_SET(mail, antispam_mail_module, amail); } static void antispam_mailbox_free(struct mailbox *box) { struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(box); asbox->module_ctx.super.free(box); } static void antispam_mailbox_allocated(struct mailbox *box) { struct mailbox_vfuncs *v = box->vlast; struct antispam_mailbox *asbox; struct antispam_mail_user *asuser = ANTISPAM_USER_CONTEXT(box->list->ns->user); if (asuser == NULL) return; asbox = p_new(box->pool, struct antispam_mailbox, 1); asbox->module_ctx.super = *v; box->vlast = &asbox->module_ctx.super; asbox->save_hack = FALSE; asbox->movetype = MMT_APPEND; asbox->cfg = asuser->cfg; v->free = antispam_mailbox_free; if (asbox->cfg->need_folder_hook) { /* override save_init to override want_mail, we need that */ v->save_begin = antispam_save_begin; v->save_finish = antispam_save_finish; v->transaction_begin = antispam_mailbox_transaction_begin; v->transaction_commit = antispam_mailbox_transaction_commit; v->transaction_rollback = antispam_mailbox_transaction_rollback; v->copy = antispam_copy; } MODULE_CONTEXT_SET(box, antispam_storage_module, asbox); } static const char *_getenv(const char *name, void *data) { struct mail_user *user = data; const char *env; T_BEGIN { env = t_strconcat("antispam_", t_str_lcase(name), NULL); env = mail_user_plugin_getenv(user, env); } T_END; return env; } static void antispam_user_deinit(struct mail_user *user) { struct antispam_mail_user *asuser = ANTISPAM_USER_CONTEXT(user); antispam_free_config(asuser->cfg); asuser->module_ctx.super.deinit(user); } void antispam_mail_user_created(struct mail_user *user) { struct mail_user_vfuncs *v = user->vlast; struct antispam_mail_user *asuser; struct antispam_config *cfg; cfg = antispam_setup_config(_getenv, user); if (!cfg) return; asuser = p_new(user->pool, struct antispam_mail_user, 1); asuser->cfg = cfg; asuser->module_ctx.super = *v; user->vlast = &asuser->module_ctx.super; v->deinit = antispam_user_deinit; MODULE_CONTEXT_SET(user, antispam_user_module, asuser); } static struct mail_storage_hooks antispam_mail_storage_hooks = { .mail_user_created = antispam_mail_user_created, .mailbox_list_created = NULL, .mail_namespace_storage_added = NULL, .mailbox_allocated = antispam_mailbox_allocated, .mail_allocated = antispam_mail_allocated, }; void antispam_storage_init(struct module *module) { mail_storage_hooks_add(module, &antispam_mail_storage_hooks); } void antispam_storage_deinit(void) { mail_storage_hooks_remove(&antispam_mail_storage_hooks); } dovecot-antispam-2.0+20170109/antispam-storage.c000066400000000000000000000000701303466642300211550ustar00rootroot00000000000000#include "dovecot-version.h" #include ANTISPAM_STORAGE dovecot-antispam-2.0+20170109/antispam.7000066400000000000000000000271611303466642300174510ustar00rootroot00000000000000.TH ANTISPAM 7 "24 March 2012" "" "" .SH NAME antispam \- The dovecot antispam plugin. .SH DESCRIPTION The dovecot antispam plugin watches a defined spam folder (defaults to "SPAM"). It works together with a spam system that classifies each message as it is delivered. When the message is classified as spam, it shall be delivered to the spam folder, otherwise via the regular filtering file the user may have (maildrop, sieve, ...). Now the user has everything classified as spam in the special spam folder, everything else where it should be sorted to. This is not enough because our spam scanner needs training. We'll occasionally have false positives and false negatives. Now this is the point where the dovecot antispam plugin comes into play. Instead of moving mail into special folders or forwarding them to special mail addresses for retraining, the plugin offers two actions for the user: .IP " 1." 4 moving mail out of the SPAM folder and .IP " 2." 4 moving mail into the SPAM folder. .PP The dovecot plugin watches these actions (and additionally prohibits APPENDs to the SPAM folder, more for technical reasons than others) and tells the spam classifier that it made an error and needs to re-classify the message (as spam/not spam depending on which way it was moved.) The advantage of this approach is that the mail ends up in the right target folder directly and needs not be touched twice. When other classifiers like crm114 that have an `unsure' state are used, the plugin can also help, it supports an `unsure' folder feature. The unsure folder cannot be written to, but moving out from there into a folder that is considered a spam folder will learn as spam, any other folder (except trashes) will cause learning as not-spam. .SH INSTALLATION First copy the `defconfig' file to `.config' and edit it as necessary. You need to have the dovecot headers installed and possibly other things depending on the backend you choose. Then, assuming you have configured the INSTALLDIR correctly, simply run `make install'. If you do not wish to use the install target, simply copy the plugin (that is, the file lib90_antispam_plugin.so) to your dovecot imap plugin directory; by default this is /usr/lib/dovecot/modules/imap/ or any dir you have configured (look for the mail_plugin_dir configuration directive.) Open your dovecot configuration file (usually /etc/dovecot/dovecot.conf) and add the antispam plugin to the imap protocol section: .nf protocol imap { mail_plugins = antispam # mail_plugin_dir = /usr/lib/dovecot/modules/imap } .fi .SH BACKENDS The plugin supports multiple backends, there are currently a few working backends included in the distribution: .SS dspam executable backend (dspam specific) This backend instantly retrains by calling dspam. There are some problems with this approach including (1) it can take a long time during which the IMAP session is blocked (2) when many users retrain many messages at once server load may spike .SS pipe backend (spam filter agnostic) This backend simply pipes the mail to train to a process it executes. This can for example be used to send it as email to mail aliases for retraining. This backend can be very easy to set up if you already have a working setup that uses training addresses as recommended by many spam filter setups. Since this backend simply pipes the message to a program (by default sendmail) it can also be used for all kinds of other spam filters, for example spamassassin (by calling sa-learn instead of sendmail.) .SS crm114 executable backend (crm114 specific) This backend instantly retrains by calling mailreaver.crm which needs to be configured (defaulting to /bin/false!); the argument --good or --spam is given depending on how mail is moved. You need to use the unsure folder option (see below) together with this plugin and deliver unsure mail into an unsure folder, spam mail into a spam folder and other mail regularly. Has the same drawbacks as the dspam approach. .SS spool2dir backend (general) This backend spools the message into a file. No further processing is performed. You need to write an extra daemon that picks up the spooled files and trains the spam filter as appropriate. You can, for example, use incron to pick up new emails. .SH CONFIGURATION Aside from the build-configuration done in the `.config' file, you have the following run-time options (shown along with the default): .nf plugin { ################## # GENERIC OPTIONS # Debugging options # Uncomment to get the desired debugging behaviour. # Note that in some cases stderr debugging will not be as # verbose as syslog debugging due to internal limitations. # # antispam_debug_target = syslog # antispam_debug_target = stderr # antispam_verbose_debug = 1 # # This can be used to get a prefix, e.g. by specifying %u in it # antispam_debug_prefix = "antispam: " # backend selection, MUST be configured first, # there's no default so you need to set one of # these options: # antispam_backend = crm114 # antispam_backend = dspam # antispam_backend = pipe # antispam_backend = spool2dir # mail signature (used with any backend requiring a signature) antispam_signature = X-DSPAM-Signature # action to take on mails without signature # (used with any backend requiring a signature) # (we recommend only setting this to 'move' after verifying that the # whole setup is working) # antispam_signature_missing = move # move silently without training antispam_signature_missing = error # The list of folders for trash, spam and unsure can be given # with three options, e.g. "trash" matches the given folders # exactly as written, "trash_pattern" accept the * wildcard at # the end of the foldername, "trash_pattern_ignorecase" # accepts the * wildcard at the end of the foldername _and_ # matches the name case insensitivly. # the *-wildcard with the following meaning: # * at the end: any folder that _start_ with the string # e.g.: # antispam_trash_pattern = deleted *;Gel&APY-schte * # match any folders that start with "deleted " or "Gelöschte " # match is _case_senstive_! # # antispam_trash_pattern_ignorecase = deleted *;Gel&APY-schte * # match any folders that start with "deleted " or "gelöschte " # match is _case_insenstive_, except the non-USASCII letters, # "ö" in this example. # To match the upper-case Ö, too, you need to add yet another # pattern "gel&ANY-schte *", note the different UTF7 encoding: # &ANY- instead of &APY-. # semicolon-separated list of Trash folders (default unset i.e. none) # antispam_trash = # antispam_trash = trash;Trash;Deleted Items; Deleted Messages # antispam_trash_pattern = trash;Trash;Deleted * # antispam_trash_pattern_ignorecase = trash;Deleted * # semicolon-separated list of spam folders antispam_spam = SPAM # antispam_spam_pattern = SPAM # antispam_spam_pattern_ignorecase = SPAM # semicolon-separated list of unsure folders (default unset i.e. none) # antispam_unsure = # antispam_unsure_pattern = # antispam_unsure_pattern_ignorecase = # Whether to allow APPENDing to SPAM folders or not. Must be set to # "yes" (case insensitive) to be activated. Before activating, please # read the discussion below. # antispam_allow_append_to_spam = no ########################### # BACKEND SPECIFIC OPTIONS # #=================== # dspam plugin # dspam binary antispam_dspam_binary = /usr/bin/dspam # semicolon-separated list of extra arguments to dspam # (default unset i.e. none) # antispam_dspam_args = # antispam_dspam_args = --deliver=;--user;%u # % expansion done by dovecot # antispam_dspam_args = --mode=teft # Ignore mails where the DSPAM result header contains any of the # strings listed in the blacklist # (default unset i.e. none) # antispam_dspam_result_header = X-DSPAM-Result # semicolon-separated list of blacklisted results, case insensitive # antispam_dspam_result_blacklist = Virus # semicolon-separated list of environment variables to set # (default unset i.e. none) # antispam_dspam_env = # antispam_dspam_env = HOME=%h;USER=%u #===================== # pipe plugin # # This plug can be used to train via an arbitrary program that # receives the message on standard input. Since sendmail can be # such a program, it can be used to send the message to another # email address for training there. # # For example: # antispam_pipe_program = /path/to/mailtrain # (defaults to /usr/sbin/sendmail) # antispam_pipe_program_args = --for;%u # antispam_pipe_program_spam_arg = --spam # antispam_pipe_program_notspam_arg = --ham # antispam_pipe_tmpdir = /tmp # will call it, for example, like this: # /path/to/mailtrain --for jberg --spam # # The old configuration options from when this plugin was called # "mailtrain" are still valid, these are, in the same order as # above: antispam_mail_sendmail, antispam_mail_sendmail_args, # antispam_mail_spam, antispam_mail_notspam and antispam_mail_tmpdir. # # Alternatively, if you need to give multiple options, you can use # the spam_args/notspam_args parameters (which are used in preference # of the singular form): # antispam_pipe_program_spam_args = --spam;--my-other-param1 # antispam_pipe_program_notspam_args = --ham;--my-other-param2 # which will then call # /path/to/mailtrain --for jberg --spam --my-other-param1 # temporary directory antispam_pipe_tmpdir = /tmp # spam/not-spam argument (default unset which will is not what you want) # antispam_pipe_program_spam_arg = # antispam_pipe_program_notspam_arg = # binary to pipe mail to antispam_pipe_program = /usr/sbin/sendmail #antispam_pipe_program_args = -f;%u@example.com # % expansion done by dovecot #=================== # crm114 plugin # mailreaver binary antispam_crm_binary = /bin/false # antispam_crm_binary = /usr/share/crm114/mailreaver.crm # semicolon-separated list of extra arguments to crm114 # (default unset i.e. none) # antispam_crm_args = # antispam_crm_args = --config=/path/to/config # semicolon-separated list of environment variables to set # (default unset i.e. none) # antispam_crm_env = # antispam_crm_env = HOME=%h;USER=%u # NOTE: you need to set the signature for this backend antispam_signature = X-CRM114-CacheID #=================== # spool2dir plugin # spam/not-spam spool2dir drop (default unset which will give errors) # The first %%lu is replaced by the current time. # The second %%lu is replaced by a counter to generate unique names. # These two tokens MUST be present in the template! However # you can insert any C-style modifier as shown. # antispam_spool2dir_spam = /tmp/spamspool/%%020lu-%u-%%05lus # antispam_spool2dir_notspam = /tmp/spamspool/%%020lu-%u-%%05luh } .fi .SH ALLOWING APPENDS? You should be careful with allowing APPENDs to SPAM folders. The reason for possibly allowing it is to allow not-SPAM --> SPAM transitions to work with offlineimap. However, because with APPEND the plugin cannot know the source of the message, multiple bad scenarios can happen: .IP " 1." 4 SPAM --> SPAM transitions cannot be recognised and are trained .IP " 2." 4 the same holds for Trash --> SPAM transitions .PP Additionally, because we cannot recognise SPAM --> not-SPAM transitions, training good messages will never work with APPEND. .SH AUTHORS Johannes Berg, Frank Cusack, Benedikt Boehm, Andreas Schneider dovecot-antispam-2.0+20170109/crm114-exec.c000066400000000000000000000131451303466642300176370ustar00rootroot00000000000000/* * crm114 backend for dovecot antispam plugin * * Copyright (C) 2004-2007 Johannes Berg * 2006 Frank Cusack * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License Version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include "lib.h" #include "mail-storage-private.h" #include "antispam-plugin.h" static int call_reaver(const struct antispam_config *cfg, const char *signature, enum classification wanted) { pid_t pid; const char *class_arg; int pipes[2]; switch (wanted) { case CLASS_NOTSPAM: class_arg = "--good"; break; case CLASS_SPAM: class_arg = "--spam"; break; } /* * For reaver stdin, it wants to read a full message but * really only needs the signature. */ if (pipe(pipes)) return -1; pid = fork(); if (pid < 0) return -1; if (pid) { int status; close(pipes[0]); /* * Reaver wants the mail but only needs the cache ID */ write(pipes[1], cfg->crm.sigcfg.signature_hdr, strlen(cfg->crm.sigcfg.signature_hdr)); write(pipes[1], ": ", 2); write(pipes[1], signature, strlen(signature)); write(pipes[1], "\r\n\r\n", 4); close(pipes[1]); /* * Wait for reaver */ waitpid(pid, &status, 0); if (!WIFEXITED(status)) return 1; return WEXITSTATUS(status); } else { int fd = open("/dev/null", O_RDONLY); char **argv; /* 2 fixed, extra, terminating NULL */ int sz = sizeof(char *) * (2 + cfg->crm.extra_args_num + 1); int i; argv = i_malloc(sz); memset(argv, 0, sz); close(0); close(1); close(2); /* see above */ close(pipes[1]); if (dup2(pipes[0], 0) != 0) exit(1); close(pipes[0]); if (dup2(fd, 1) != 1) exit(1); if (dup2(fd, 2) != 2) exit(1); close(fd); argv[0] = (char *)cfg->crm.reaver_binary; argv[1] = (char *)class_arg; for (i = 0; i < cfg->crm.extra_args_num; i++) argv[i + 2] = (char *)cfg->crm.extra_args[i]; debugv(&cfg->dbgcfg, argv); T_BEGIN { for (i = 0; i < cfg->crm.extra_env_num; i++) { char *name, *value; name = t_strdup_noconst(cfg->crm.extra_env[i]); value = strchr(name, '='); if (value) { *value = '\0'; value++; } setenv(name, value, 1); } } T_END; execv(cfg->crm.reaver_binary, argv); /* fall through if reaver can't be found */ debug(&cfg->dbgcfg, "executing %s failed: %d (uid=%d, gid=%d)", cfg->crm.reaver_binary, errno, getuid(), getgid()); exit(127); /* not reached */ return -1; } } struct antispam_transaction_context { struct siglist *siglist; }; static struct antispam_transaction_context * backend_start(const struct antispam_config *cfg ATTR_UNUSED, struct mailbox *box ATTR_UNUSED) { struct antispam_transaction_context *ast; ast = i_new(struct antispam_transaction_context, 1); ast->siglist = NULL; return ast; } static void backend_rollback(const struct antispam_config *cfg ATTR_UNUSED, struct antispam_transaction_context *ast) { signature_list_free(&ast->siglist); i_free(ast); } static int backend_commit(const struct antispam_config *cfg, struct mailbox_transaction_context *ctx, struct antispam_transaction_context *ast) { struct siglist *item = ast->siglist; int ret = 0; while (item) { if (call_reaver(cfg, item->sig, item->wanted)) { ret = -1; mail_storage_set_error(ctx->box->storage, ME(NOTPOSSIBLE) "Failed to call reaver"); break; } item = item->next; } signature_list_free(&ast->siglist); i_free(ast); return ret; } static int backend_handle_mail(const struct antispam_config *cfg, struct mailbox_transaction_context *t, struct antispam_transaction_context *ast, struct mail *mail, enum classification want) { return signature_extract_to_list(&cfg->crm.sigcfg, t, mail, &ast->siglist, want); } static void backend_init(struct antispam_config *cfg, const char *(getenv)(const char *env, void *data), void *getenv_data) { const char *tmp; int i; tmp = getenv("CRM_BINARY", getenv_data); if (tmp) { cfg->crm.reaver_binary = tmp; debug(&cfg->dbgcfg, "reaver binary set to %s\n", tmp); } else cfg->crm.reaver_binary = "/bin/false"; tmp = getenv("CRM_ARGS", getenv_data); if (tmp) { cfg->crm.extra_args = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->crm.extra_args_num = str_array_length( (const char *const *)cfg->crm.extra_args); for (i = 0; i < cfg->crm.extra_args_num; i++) debug(&cfg->dbgcfg, "reaver extra arg %s\n", cfg->crm.extra_args[i]); } tmp = getenv("CRM_ENV", getenv_data); if (tmp) { cfg->crm.extra_env = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->crm.extra_env_num = str_array_length( (const char *const *)cfg->crm.extra_env); for (i = 0; i < cfg->crm.extra_env_num; i++) debug(&cfg->dbgcfg, "reaver env %s\n", cfg->crm.extra_env[i]); } signature_init(&cfg->crm.sigcfg, &cfg->dbgcfg, getenv, getenv_data); } struct backend crm114_backend = { .init = backend_init, .handle_mail = backend_handle_mail, .start = backend_start, .rollback = backend_rollback, .commit = backend_commit, }; dovecot-antispam-2.0+20170109/debug.c000066400000000000000000000045331303466642300167750ustar00rootroot00000000000000#include #include #include #include "antispam-plugin.h" #include "antispam-version.h" static void _debug(const struct antispam_debug_config *cfg, const char *format, va_list ap) { const char *fmt; if (cfg->target == ADT_NONE) return; T_BEGIN { fmt = t_strconcat(cfg->prefix, format, NULL); switch (cfg->target) { case ADT_NONE: break; case ADT_SYSLOG: vsyslog(LOG_DEBUG, fmt, ap); break; case ADT_STDERR: vfprintf(stderr, fmt, ap); fflush(stderr); break; } } T_END; } void debug(const struct antispam_debug_config *cfg, const char *fmt, ...) { va_list args; va_start(args, fmt); _debug(cfg, fmt, args); va_end(args); } void debugv(const struct antispam_debug_config *cfg, char **args) { size_t len, pos = 0, buflen = 1024; char *buf; const char *str; T_BEGIN { buf = t_buffer_get(buflen); while (1) { str = *args; if (!str) break; len = strlen(str); if (pos + len + 1 >= buflen) { buflen = nearest_power(pos + len + 2); buf = t_buffer_reget(buf, buflen); } memcpy(buf + pos, str, len); pos += len; buf[pos++] = ' '; args++; } buf[pos++] = '\0'; t_buffer_alloc(pos); debug(cfg, "%s", buf); } T_END; } void debugv_not_stderr(const struct antispam_debug_config *cfg, char **args) { if (cfg->target == ADT_STDERR) return; debugv(cfg, args); } void debug_verbose(const struct antispam_debug_config *cfg, const char *fmt, ...) { va_list args; if (!cfg->verbose) return; va_start(args, fmt); _debug(cfg, fmt, args); va_end(args); } int debug_init(struct antispam_debug_config *cfg, const char *(getenv)(const char *env, void *data), void *getenv_data) { const char *tmp; tmp = getenv("DEBUG_TARGET", getenv_data); if (tmp) { if (strcmp(tmp, "syslog") == 0) cfg->target = ADT_SYSLOG; else if (strcmp(tmp, "stderr") == 0) cfg->target = ADT_STDERR; else return -1; } cfg->prefix = getenv("DEBUG_PREFIX", getenv_data); if (!cfg->prefix) cfg->prefix = "antispam: "; debug(cfg, "plugin initialising (%s)\n", ANTISPAM_VERSION); tmp = getenv("VERBOSE_DEBUG", getenv_data); if (tmp) { char *endp; unsigned long val = strtoul(tmp, &endp, 10); if (*endp || val >= 2) { debug(cfg, "Invalid verbose_debug setting\n"); return -1; } cfg->verbose = val; debug_verbose(cfg, "verbose debug enabled\n"); } return 0; } dovecot-antispam-2.0+20170109/defconfig000066400000000000000000000021211303466642300174010ustar00rootroot00000000000000# Example plugin build time configuration # # This file lists the configuration options that are used when building the # antispam plugin. All lines starting with # are ignored. Configuration option # lines must be commented out completely, if they are not to be included, # i.e. just setting VARIABLE=n is not disabling that variable. # # This file is included in Makefile, so variables like CFLAGS and LIBS can also # be modified from here. In most cases, these lines should use += in order not # to override previous values of the variables. # Dovecot build/header directory # Building the plugin requires configured dovecot sources or having # configured it with --enable-header-install in which case you can # point DOVECOT= to the installed headers too. # If unset, it defaults to /usr/include/dovecot which typically is the # right place so you don't have to worry about it. #DOVECOT=../dovecot-1.0.5 #DOVECOT=../dovecot-1.1 #DOVECOT=/usr/include/dovecot # install directory for 'make install' # NB no need for a final '/' INSTALLDIR=/usr/lib/dovecot/modules/imap # extra CFLAGS # CFLAGS += -g3 dovecot-antispam-2.0+20170109/dovecot-version.c000066400000000000000000000042771303466642300210420ustar00rootroot00000000000000#include #include #include #include #include "config.h" int main(int argc, char **argv) { const char *v = PACKAGE_STRING; char *e; int maj = 0, min = 0, patch = 0; if (strncmp(v, "dovecot ", 8) && strncmp(v, "Dovecot ", 8)) return 1; /* skip "dovecot " */ v += 8; maj = strtol(v, &e, 10); if (v == e) return 2; v = e + 1; min = strtol(v, &e, 10); if (v == e) return 3; /* not end of string yet? */ if (*e) { v = e + 1; if (isdigit(*v)) { patch = strtol(v, &e, 10); if (v == e) return 4; } else patch = 0; } printf("/* Auto-generated file, do not edit */\n\n"); printf("#define DOVECOT_VERSION_CODE(maj, min, patch) " "((maj)<<16 | ((min)<<8) | (patch))\n\n"); printf("#define DOVECOT_VCODE " "0x%.2x%.2x%.2x\n", maj, min, 0); printf("#define DOVECOT_VCODE_PATCH " "0x%.2x%.2x%.2x\n", maj, min, patch); printf("#define DOVECOT_IS_EQ(maj, min) " "DOVECOT_VCODE == DOVECOT_VERSION_CODE(maj, min, 0)\n"); printf("#define DOVECOT_IS_GT(maj, min) " "DOVECOT_VCODE > DOVECOT_VERSION_CODE(maj, min, 0)\n"); printf("#define DOVECOT_IS_GE(maj, min) " "DOVECOT_VCODE >= DOVECOT_VERSION_CODE(maj, min, 0)\n"); printf("#define DOVECOT_IS_LT(maj, min) " "DOVECOT_VCODE < DOVECOT_VERSION_CODE(maj, min, 0)\n"); printf("#define DOVECOT_IS_LE(maj, min) " "DOVECOT_VCODE <= DOVECOT_VERSION_CODE(maj, min, 0)\n"); printf("#define DOVECOT_P_IS_EQ(maj, min, patch) " "DOVECOT_VCODE_PATCH == DOVECOT_VERSION_CODE(maj, min, patch)\n"); printf("#define DOVECOT_P_IS_GT(maj, min, patch) " "DOVECOT_VCODE_PATCH > DOVECOT_VERSION_CODE(maj, min, patch)\n"); printf("#define DOVECOT_P_IS_GE(maj, min, patch) " "DOVECOT_VCODE_PATCH >= DOVECOT_VERSION_CODE(maj, min, patch)\n"); printf("#define DOVECOT_P_IS_LT(maj, min, patch) " "DOVECOT_VCODE_PATCH < DOVECOT_VERSION_CODE(maj, min, patch)\n"); printf("#define DOVECOT_P_IS_LE(maj, min, patch) " "DOVECOT_VCODE_PATCH <= DOVECOT_VERSION_CODE(maj, min, patch)\n"); /* Use the antispam-storage-2.0.c for dovecot 2.1 and 2.2 as well */ if (maj == 2 && min < 3) min = 0; printf("#define ANTISPAM_STORAGE " "\"antispam-storage-%d.%d.c\"\n", maj, min); return 0; } dovecot-antispam-2.0+20170109/dspam-exec.c000066400000000000000000000165351303466642300177420ustar00rootroot00000000000000/* * dspam backend for dovecot antispam plugin * * Copyright (C) 2004-2007 Johannes Berg * 2006 Frank Cusack * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License Version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include "lib.h" #include "mail-storage-private.h" #include "antispam-plugin.h" static int call_dspam(const struct antispam_config *cfg, const char *signature, enum classification wanted) { pid_t pid; const char *class_arg; const char *sign_arg; int pipes[2]; sign_arg = t_strconcat("--signature=", signature, NULL); switch (wanted) { case CLASS_NOTSPAM: class_arg = t_strconcat("--class=", "innocent", NULL); break; case CLASS_SPAM: class_arg = t_strconcat("--class=", "spam", NULL); break; } /* * For dspam stderr; dspam seems to not always exit with a * non-zero exit code on errors so we treat it as an error * if it logged anything to stderr. */ if (pipe(pipes) < 0) return -1; pid = fork(); if (pid < 0) return -1; if (pid) { int status; char buf[1025]; int readsize; bool error = FALSE; close(pipes[1]); do { readsize = read(pipes[0], buf, sizeof(buf) - 1); if (readsize < 0) { readsize = -1; if (errno == EINTR) readsize = -2; } /* * readsize > 0 means that we read a message from * dspam, -1 means we failed to read for some odd * reason */ if (readsize > 0 || readsize == -1) error = TRUE; if (readsize > 0) { buf[readsize] = '\0'; debug(&cfg->dbgcfg, "dspam error: %s\n", buf); } } while (readsize == -2 || readsize > 0); /* * Wait for dspam, should return instantly since we've * already waited above (waiting for stderr to close) */ waitpid(pid, &status, 0); if (!WIFEXITED(status)) error = TRUE; close(pipes[0]); if (error) return 1; return WEXITSTATUS(status); } else { int fd = open("/dev/null", O_RDONLY); char **argv; /* 4 fixed args, extra args, terminating NULL */ int sz = sizeof(char *) * (4 + cfg->dspam.extra_args_num + 1); int i; argv = i_malloc(sz); memset(argv, 0, sz); close(0); close(1); close(2); /* see above */ close(pipes[0]); if (dup2(pipes[1], 2) != 2) exit(1); if (dup2(pipes[1], 1) != 1) exit(1); close(pipes[1]); if (dup2(fd, 0) != 0) exit(1); close(fd); argv[0] = (char *)cfg->dspam.binary; argv[1] = "--source=error"; argv[2] = (char *)class_arg; argv[3] = (char *)sign_arg; for (i = 0; i < cfg->dspam.extra_args_num; i++) argv[i + 4] = (char *)cfg->dspam.extra_args[i]; /* * not good with stderr debuggin since we then write to * stderr which our parent takes as a bug */ debugv_not_stderr(&cfg->dbgcfg, argv); T_BEGIN { for (i = 0; i < cfg->dspam.extra_env_num; i++) { char *name, *value; name = t_strdup_noconst(cfg->dspam.extra_env[i]); value = strchr(name, '='); if (value) { *value = '\0'; value++; } setenv(name, value, 1); } } T_END; execv(cfg->dspam.binary, argv); debug(&cfg->dbgcfg, "executing %s failed: %d (uid=%d, gid=%d)", cfg->dspam.binary, errno, getuid(), getgid()); /* fall through if dspam can't be found */ exit(127); /* not reached */ return -1; } } struct antispam_transaction_context { struct siglist *siglist; }; static struct antispam_transaction_context * backend_start(const struct antispam_config *cfg ATTR_UNUSED, struct mailbox *box ATTR_UNUSED) { struct antispam_transaction_context *ast; ast = i_new(struct antispam_transaction_context, 1); ast->siglist = NULL; return ast; } static void backend_rollback(const struct antispam_config *cfg ATTR_UNUSED, struct antispam_transaction_context *ast) { signature_list_free(&ast->siglist); i_free(ast); } static int backend_commit(const struct antispam_config *cfg, struct mailbox_transaction_context *ctx, struct antispam_transaction_context *ast) { struct siglist *item = ast->siglist; int ret = 0; while (item) { if (call_dspam(cfg, item->sig, item->wanted)) { ret = -1; mail_storage_set_error(ctx->box->storage, ME(NOTPOSSIBLE) "Failed to call dspam"); break; } item = item->next; } signature_list_free(&ast->siglist); i_free(ast); return ret; } static int backend_handle_mail(const struct antispam_config *cfg, struct mailbox_transaction_context *t, struct antispam_transaction_context *ast, struct mail *mail, enum classification want) { const char *const *result = NULL; int i; /* * Check for whitelisted classifications that should * be ignored when moving a mail. eg. virus. */ if (cfg->dspam.result_header) result = get_mail_headers(mail, cfg->dspam.result_header); if (result && result[0]) { for (i = 0; i < cfg->dspam.result_bl_num; i++) { if (strcasecmp(result[0], cfg->dspam.result_bl[i]) == 0) return 0; } } return signature_extract_to_list(&cfg->dspam.sigcfg, t, mail, &ast->siglist, want); } static void backend_init(struct antispam_config *cfg, const char *(getenv)(const char *env, void *data), void *getenv_data) { const char *tmp; int i; tmp = getenv("DSPAM_BINARY", getenv_data); if (tmp) cfg->dspam.binary = tmp; else cfg->dspam.binary = "/usr/bin/dspam"; debug(&cfg->dbgcfg, "dspam binary set to %s\n", cfg->dspam.binary); tmp = getenv("DSPAM_RESULT_HEADER", getenv_data); if (tmp) { cfg->dspam.result_header = tmp; debug(&cfg->dbgcfg, "dspam result set to %s\n", cfg->dspam.result_header); tmp = getenv("DSPAM_RESULT_BLACKLIST", getenv_data); if (tmp) { cfg->dspam.result_bl = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->dspam.result_bl_num = str_array_length( (const char *const *)cfg->dspam.result_bl); for (i = 0; i < cfg->dspam.result_bl_num; i++) debug(&cfg->dbgcfg, "dspam result blacklist %s\n", cfg->dspam.result_bl[i]); } } tmp = getenv("DSPAM_ARGS", getenv_data); if (tmp) { cfg->dspam.extra_args = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->dspam.extra_args_num = str_array_length( (const char *const *)cfg->dspam.extra_args); for (i = 0; i < cfg->dspam.extra_args_num; i++) debug(&cfg->dbgcfg, "dspam extra arg %s\n", cfg->dspam.extra_args[i]); } tmp = getenv("DSPAM_ENV", getenv_data); if (tmp) { cfg->dspam.extra_env = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->dspam.extra_env_num = str_array_length( (const char *const *)cfg->dspam.extra_env); for (i = 0; i < cfg->dspam.extra_env_num; i++) debug(&cfg->dbgcfg, "dspam env %s\n", cfg->dspam.extra_env[i]); } signature_init(&cfg->dspam.sigcfg, &cfg->dbgcfg, getenv, getenv_data); } struct backend dspam_backend = { .init = backend_init, .handle_mail = backend_handle_mail, .start = backend_start, .rollback = backend_rollback, .commit = backend_commit, }; dovecot-antispam-2.0+20170109/pipe.c000066400000000000000000000227241303466642300166460ustar00rootroot00000000000000/* * mailing backend for dovecot antispam plugin * * Copyright (C) 2007 Johannes Berg * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License Version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include "lib.h" #include "dict.h" #include "mail-storage-private.h" #include "ostream.h" #include "istream.h" #include "antispam-plugin.h" static int run_pipe(const struct antispam_config *cfg, int mailfd, enum classification wanted) { char **dest; int dest_num; pid_t pid; int status; switch (wanted) { case CLASS_SPAM: dest = cfg->pipe.spam_args; dest_num = cfg->pipe.spam_args_num; break; case CLASS_NOTSPAM: dest = cfg->pipe.ham_args; dest_num = cfg->pipe.ham_args_num; break; } if (!dest) return -1; pid = fork(); if (pid == -1) return -1; debug(&cfg->dbgcfg, "running mailtrain backend program %s", cfg->pipe.pipe_binary); if (pid) { if (waitpid(pid, &status, 0) == -1) return -1; if (!WIFEXITED(status)) return -1; return WEXITSTATUS(status); } else { char **argv; int sz = sizeof(char *) * (2 + cfg->pipe.extra_args_num + dest_num + 1); int i, j, fd; argv = i_malloc(sz); memset(argv, 0, sz); argv[0] = (char *) cfg->pipe.pipe_binary; for (i = 0; i < cfg->pipe.extra_args_num; i++) { argv[i + 1] = (char *) cfg->pipe.extra_args[i]; debug(&cfg->dbgcfg, "running mailtrain backend program parameter %d %s", i + 1, argv[i + 1]); } for (j = 0; j < dest_num; j++) { argv[i + 1 + j] = (char *) dest[j]; debug(&cfg->dbgcfg, "running mailtrain backend program parameter %d %s", i + 1 + j, argv[i + 1 + j]); } dup2(mailfd, 0); fd = open("/dev/null", O_WRONLY); dup2(fd, 1); dup2(fd, 2); close(fd); execv(cfg->pipe.pipe_binary, argv); _exit(1); /* not reached */ return -1; } } struct antispam_transaction_context { char *tmpdir; int count; int tmplen; }; static struct antispam_transaction_context * backend_start(const struct antispam_config *cfg ATTR_UNUSED, struct mailbox *box ATTR_UNUSED) { struct antispam_transaction_context *ast; char *tmp; ast = i_new(struct antispam_transaction_context, 1); ast->count = 0; tmp = i_strconcat(cfg->pipe.tmpdir, "/antispam-mail-XXXXXX", NULL); ast->tmpdir = mkdtemp(tmp); if (!ast->tmpdir) i_free(tmp); else ast->tmplen = strlen(ast->tmpdir); return ast; } static int process_tmpdir(const struct antispam_config *cfg, struct mailbox_transaction_context *ctx, struct antispam_transaction_context *ast) { int cnt = ast->count; int fd; char *buf; enum classification wanted; int rc = 0; T_BEGIN { buf = t_malloc(20 + ast->tmplen); while (rc == 0 && cnt > 0) { cnt--; i_snprintf(buf, 20 + ast->tmplen - 1, "%s/%d", ast->tmpdir, cnt); fd = open(buf, O_RDONLY); read(fd, &wanted, sizeof(wanted)); if ((rc = run_pipe(cfg, fd, wanted))) { mail_storage_set_error(ctx->box->storage, ME(TEMP) "failed to send mail"); debug(&cfg->dbgcfg, "run program failed with exit code %d\n", rc); rc = -1; } close(fd); } } T_END; return rc; } static void clear_tmpdir(struct antispam_transaction_context *ast) { char *buf; T_BEGIN { buf = t_malloc(20 + ast->tmplen); while (ast->count > 0) { ast->count--; i_snprintf(buf, 20 + ast->tmplen - 1, "%s/%d", ast->tmpdir, ast->count); unlink(buf); } rmdir(ast->tmpdir); } T_END; } static void backend_rollback(const struct antispam_config *cfg ATTR_UNUSED, struct antispam_transaction_context *ast) { if (ast->tmpdir) { /* clear it! */ clear_tmpdir(ast); i_free(ast->tmpdir); } i_free(ast); } static int backend_commit(const struct antispam_config *cfg, struct mailbox_transaction_context *ctx, struct antispam_transaction_context *ast) { int ret; if (!ast->tmpdir) { i_free(ast); return 0; } ret = process_tmpdir(cfg, ctx, ast); clear_tmpdir(ast); i_free(ast->tmpdir); i_free(ast); return ret; } static int backend_handle_mail(const struct antispam_config *cfg, struct mailbox_transaction_context *t, struct antispam_transaction_context *ast, struct mail *mail, enum classification wanted) { struct istream *mailstream; struct ostream *outstream; int ret; char *buf; const unsigned char *beginning; size_t size; int fd; if (!ast->tmpdir) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to initialise temporary dir"); return -1; } if (!cfg->pipe.ham_args || !cfg->pipe.spam_args) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "antispam plugin not configured"); return -1; } if (mail_get_stream(mail, NULL, NULL, &mailstream) < 0) { mail_storage_set_error(t->box->storage, ME(EXPUNGED) "Failed to get mail contents"); return -1; } T_BEGIN { buf = t_malloc(20 + ast->tmplen); i_snprintf(buf, 20 + ast->tmplen - 1, "%s/%d", ast->tmpdir, ast->count); fd = creat(buf, 0600); if (fd < 0) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to create temporary file"); ret = -1; goto out; } ast->count++; outstream = o_stream_create_from_fd(fd, t->box->pool); if (!outstream) { ret = -1; mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to stream temporary file"); goto out_close; } if (o_stream_send(outstream, &wanted, sizeof(wanted)) != sizeof(wanted)) { ret = -1; mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to write marker to temp file"); goto failed_to_copy; } if (i_stream_read_data(mailstream, &beginning, &size, 5) < 0 || size < 5) { ret = -1; mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to read mail beginning"); goto failed_to_copy; } /* "From "? skip line */ if (memcmp("From ", beginning, 5) == 0) i_stream_read_next_line(mailstream); if (o_stream_send_istream(outstream, mailstream) < 0) { ret = -1; mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to copy to temporary file"); goto failed_to_copy; } ret = 0; failed_to_copy: o_stream_destroy(&outstream); out_close: close(fd); out: ; } T_END; return ret; } static void backend_init(struct antispam_config *cfg, const char *(getenv)(const char *env, void *data), void *getenv_data) { const char *tmp; int i; tmp = getenv("PIPE_PROGRAM_SPAM_ARGS", getenv_data); if (tmp) { cfg->pipe.spam_args = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->pipe.spam_args_num = str_array_length( (const char *const *)cfg->pipe.spam_args); for (i = 0; i < cfg->pipe.spam_args_num; i++) debug(&cfg->dbgcfg, "pipe backend spam arg[%d] = %s\n", i, cfg->pipe.spam_args[i]); } else { tmp = getenv("PIPE_PROGRAM_SPAM_ARG", getenv_data); if (!tmp) tmp = getenv("MAIL_SPAM", getenv_data); if (tmp) { /* bit of a hack */ cfg->pipe.spam_args = p_strsplit(cfg->mem_pool, tmp, "\x01"); cfg->pipe.spam_args_num = 1; debug(&cfg->dbgcfg, "pipe backend spam argument = %s\n", tmp); tmp = NULL; } } tmp = getenv("PIPE_PROGRAM_NOTSPAM_ARGS", getenv_data); if (tmp) { cfg->pipe.ham_args = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->pipe.ham_args_num = str_array_length( (const char *const *)cfg->pipe.ham_args); for (i = 0; i < cfg->pipe.ham_args_num; i++) debug(&cfg->dbgcfg, "pipe backend ham arg[%d] = %s\n", i, cfg->pipe.ham_args[i]); } else { tmp = getenv("PIPE_PROGRAM_NOTSPAM_ARG", getenv_data); if (!tmp) tmp = getenv("MAIL_NOTSPAM", getenv_data); if (tmp) { /* bit of a hack */ cfg->pipe.ham_args = p_strsplit(cfg->mem_pool, tmp, "\x01"); cfg->pipe.ham_args_num = 1; debug(&cfg->dbgcfg, "pipe backend not-spam argument = %s\n", tmp); tmp = NULL; } } tmp = getenv("PIPE_PROGRAM", getenv_data); if (!tmp) tmp = getenv("MAIL_SENDMAIL", getenv_data); if (tmp) { cfg->pipe.pipe_binary = tmp; debug(&cfg->dbgcfg, "pipe backend program = %s\n", tmp); } else cfg->pipe.pipe_binary = "/usr/sbin/sendmail"; tmp = getenv("PIPE_PROGRAM_ARGS", getenv_data); if (!tmp) tmp = getenv("MAIL_SENDMAIL_ARGS", getenv_data); if (tmp) { cfg->pipe.extra_args = p_strsplit(cfg->mem_pool, tmp, ";"); cfg->pipe.extra_args_num = str_array_length( (const char *const *)cfg->pipe.extra_args); for (i = 0; i < cfg->pipe.extra_args_num; i++) debug(&cfg->dbgcfg, "pipe backend program arg[%d] = %s\n", i, cfg->pipe.extra_args[i]); } tmp = getenv("PIPE_TMPDIR", getenv_data); if (!tmp) tmp = getenv("MAIL_TMPDIR", getenv_data); if (tmp) cfg->pipe.tmpdir = tmp; else cfg->pipe.tmpdir = "/tmp"; debug(&cfg->dbgcfg, "pipe backend tmpdir %s\n", cfg->pipe.tmpdir); } struct backend pipe_backend = { .init = backend_init, .handle_mail = backend_handle_mail, .start = backend_start, .rollback = backend_rollback, .commit = backend_commit, }; dovecot-antispam-2.0+20170109/signature.c000066400000000000000000000045421303466642300177100ustar00rootroot00000000000000 #include #include "antispam-plugin.h" #include "mail-storage-private.h" void signature_init(struct signature_config *cfg, const struct antispam_debug_config *dbgcfg, const char *(getenv)(const char *env, void *data), void *getenv_data) { const char *tmp = getenv("SIGNATURE", getenv_data); if (tmp) cfg->signature_hdr = tmp; else cfg->signature_hdr = "X-DSPAM-Signature"; debug(dbgcfg, "signature header line is \"%s\"\n", cfg->signature_hdr); tmp = getenv("SIGNATURE_MISSING", getenv_data); if (!tmp) tmp = "error"; if (strcmp(tmp, "move") == 0) { cfg->signature_nosig_ignore = 1; debug(dbgcfg, "will silently move mails with missing signature\n"); } else if (strcmp(tmp, "error") != 0) { debug(dbgcfg, "invalid signature_missing setting '%s', ignoring\n", tmp); } } int signature_extract_to_list(const struct signature_config *cfg, struct mailbox_transaction_context *t, struct mail *mail, struct siglist **list, enum classification wanted) { const char *const *signatures; struct siglist *item; signatures = get_mail_headers(mail, cfg->signature_hdr); if (!signatures || !signatures[0]) { if (!cfg->signature_nosig_ignore) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "antispam signature not found"); return -1; } else { return 0; } } while (signatures[1]) signatures++; item = i_new(struct siglist, 1); item->next = *list; item->wanted = wanted; item->sig = i_strdup(signatures[0]); *list = item; return 0; } int signature_extract(const struct signature_config *cfg, struct mailbox_transaction_context *t, struct mail *mail, const char **signature) { const char *const *signatures; signatures = get_mail_headers(mail, cfg->signature_hdr); if (!signatures || !signatures[0]) { if (!cfg->signature_nosig_ignore) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "antispam signature not found"); return -1; } else { *signature = NULL; return 0; } } while (signatures[1]) signatures++; *signature = signatures[0]; return 0; } void signature_list_free(struct siglist **list) { struct siglist *item, *next; i_assert(list); item = *list; while (item) { next = item->next; i_free(item->sig); i_free(item); item = next; if (item) next = item->next; } } dovecot-antispam-2.0+20170109/spool2dir.c000066400000000000000000000175171303466642300176320ustar00rootroot00000000000000/* * mailing backend for dovecot antispam plugin * * Copyright (C) 2008 Steffen Kaiser * this backend "spool2dir" bases on "mailtrain" backend of * Copyright (C) 2007 Johannes Berg * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License Version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ /* * spool2dir antispam backend / plugin * * Any modification of SPAM status is recorded into a directory. * * Configuration * * Via settings similiar to the other antispam backends * antispam_spool2dir_spam :- filename tempate for SPAM messages * antispam_spool2dir_notsam :- filename tempate for HAM messages * * The templates _must_ provide two arguments: * 1. %%lu - the current unix time (lowercase L, lowercase U) * 2. %%lu - a counter to create different temporary files * Note: The %-sign must be given two times to protect agains * the expansion by Dovecot itself. You can put any legal * format modification character of C's printf() function between * '%%' and 'lu'. * * e.g.: * antispam_spool2dir_spam = /tmp/spamspool/%%020lu-%%05lu-%u-S * antispam_spool2dir_ham = /tmp/spamspool/%%020lu-%%05lu-%u-H * * This example will spool the messages into the directory * /tmp/spamspool. The individual files start with 20 digits, * followe by a dash, 5 digits, the current username and S or H, * indicating Spam or Ham messages. * The first %%lu placeholder is replace by the current unix time, * the second %%lu with the counter. That way, if the same user * trains the same message twice, the filename indicates the order * in which it was done. So if the message was trained as SPAM first, * as HAM later, HAM superceeds SPAM. * * Operation * * When the antispam plugin identifies detects a SPAM status change, * e.g. moving/copying a message from any antispam_spam folder into * a folder _not_ listed in antispam_spam or antispam_trash, this * backend spools the complete message into antispam_mail_basedir. * If there is an error copying _all_ messages around, old spools * are kept, but the current one is deleted. For instance, if the * user is copying 15 messages, but only 10 succeed, the 10 would * be usually deleted. In this backend there is no rollback of * successfully spooled message, only the failed message is * deleted. * * Possible usage models * * A) * I use spool2dir for training the Bayes database as follows: * * Every 10 seconds a service invokes the training program, unless * it already runs. * * The training progran reads the content of the spool directory, sorts * the filenames alphanumerically, waits two seconds to allow any current * spool2dir processes to finish currently open files. * Then one message at a time is read and identified, if it contains * local modifications, e.g. user-visible SPAM reports, which are removed. * Furthermore, reports of untrustworthy people are discarded. * This process continues until either all messages are processed or * the next message would have another SPAM report type (HAM or SPAM). The * file names of the messages processed til now are passed to the Bayes * trainer to be processed within one run. Then those messages are removed. * * B) * * An Inotify server watches the spamspool directory and passes the messages * to spamd. No need for the filenames to indicate the order anymore, unless * the inotify server is not fast enough. */ #include #include #include #include #include "lib.h" #include "mail-storage-private.h" #include "ostream.h" #include "istream.h" #include "antispam-plugin.h" struct antispam_transaction_context { int count; }; static void backend_rollback(const struct antispam_config *cfg ATTR_UNUSED, struct antispam_transaction_context *ast) { i_free(ast); } static int backend_commit(const struct antispam_config *cfg ATTR_UNUSED, struct mailbox_transaction_context *ctx ATTR_UNUSED, struct antispam_transaction_context *ast) { i_free(ast); return 0; } static int backend_handle_mail(const struct antispam_config *cfg, struct mailbox_transaction_context *t, struct antispam_transaction_context *ast, struct mail *mail, enum classification wanted) { struct istream *mailstream; struct ostream *outstream; int ret = -1; const char *dest, *buf; const unsigned char *beginning; size_t size; int fd = -1; i_assert(ast); switch (wanted) { case CLASS_SPAM: dest = cfg->s2d.spamspool; break; case CLASS_NOTSPAM: dest = cfg->s2d.hamspool; break; default: /* cannot handle this */ return -1; } if(!dest) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "antispam plugin / spool2dir backend not configured"); return -1; } if (mail_get_stream(mail, NULL, NULL, &mailstream) < 0) { mail_storage_set_error(t->box->storage, ME(EXPUNGED) "Failed to get mail contents"); return -1; } T_BEGIN { /* atomically create a _new_ file */ while (ast->count <= 9999) { buf = t_strdup_printf(dest, (long)time(0), (long)++ast->count); fd = open(buf, O_CREAT | O_EXCL | O_WRONLY, 0600); if (fd >= 0 || errno != EEXIST) break; /* current filename in buf already exists, zap it */ } if (fd < 0) { debug(&cfg->dbgcfg, "spool2dir backend: Failed to create spool file %s: %s\n", dest, strerror(errno)); mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to create spool file"); goto out; } /* buf still points to allocated memory, because fd >= 0 */ outstream = o_stream_create_from_fd(fd, t->box->pool); if (!outstream) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to stream spool file"); goto out_close; } if (i_stream_read_data(mailstream, &beginning, &size, 5) < 0 || size < 5) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to read mail beginning"); goto failed_to_copy; } /* "From "? skip line */ if (memcmp("From ", beginning, 5) == 0) i_stream_read_next_line(mailstream); if (o_stream_send_istream(outstream, mailstream) < 0) { mail_storage_set_error(t->box->storage, ME(NOTPOSSIBLE) "Failed to copy to spool file"); goto failed_to_copy; } ret = 0; failed_to_copy: o_stream_destroy(&outstream); out_close: close(fd); if (ret) unlink(buf); out: ; } T_END; return ret; } static void backend_init(struct antispam_config *cfg, const char *(getenv)(const char *env, void *data), void *getenv_data) { cfg->s2d.spamspool = getenv("SPOOL2DIR_SPAM", getenv_data); if (cfg->s2d.spamspool) debug(&cfg->dbgcfg, "spool2dir spamspool %s\n", cfg->s2d.spamspool); cfg->s2d.hamspool = getenv("SPOOL2DIR_NOTSPAM", getenv_data); if (cfg->s2d.hamspool) debug(&cfg->dbgcfg, "spool2dir hamspool %s\n", cfg->s2d.hamspool); } static struct antispam_transaction_context * backend_start(const struct antispam_config *cfg ATTR_UNUSED, struct mailbox *box ATTR_UNUSED) { struct antispam_transaction_context *ast; ast = i_new(struct antispam_transaction_context, 1); ast->count = 0; return ast; } struct backend spool2dir_backend = { .init = backend_init, .handle_mail = backend_handle_mail, .start = backend_start, .rollback = backend_rollback, .commit = backend_commit, }; dovecot-antispam-2.0+20170109/version.sh000077500000000000000000000007761303466642300175740ustar00rootroot00000000000000#!/bin/sh VERSION=2.0 if head=$(git rev-parse --verify HEAD 2>/dev/null); then git update-index --refresh --unmerged > /dev/null descr=$(git describe) # on git builds check that the version number above # is correct... [ "${descr%%-*}" = "v$VERSION" ] || exit 2 echo -n '#define ANTISPAM_VERSION "' > $1 echo -n "${descr#v}" >> $1 if git diff-index --name-only HEAD | read dummy ; then echo -n "-dirty" >> $1 fi echo '"' >> $1 else echo '#define ANTISPAM_VERSION "'$VERSION'-notgit"' > $1 fi