From: Lars Wirzenius Date: Sat, 3 Dec 2005 18:41:56 +0000 (+0200) Subject: Initial import. X-Git-Url: https://git.sommitrealweird.co.uk/eoc.git/commitdiff_plain/0f7cd8b5551e2ec4333cb10561375872dd7e03d3?ds=inline Initial import. --- 0f7cd8b5551e2ec4333cb10561375872dd7e03d3 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d60c31a --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..9e5239e --- /dev/null +++ b/ChangeLog @@ -0,0 +1,720 @@ +2005-12-03 Lars Wirzenius + + * I'm switching EoC development to bzr and using commit messages + instead of a Changelog. This is the last ChangeLog entry. + +2005-12-03 Lars Wirzenius + + * enemies-of-carlotta.1: Changed - to \- since that is more + correct. Added the missing word "pipe" in the description + of --moderate. Added note about + vs - in the Postfix regexp + example. Documented --show-lists. + + * eoc.py: Don't append a footer if the message is base64 encoded. + +2005-04-16 Lars Wirzenius + + * templates/*: Changed content dispositions from attachment to + inline so that they are shown by default in mailers. + +2005-04-16 Lars Wirzenius + + * templates/bounce-owner-notification, templates/bounce-warning, + templates/setlist-confirm: Say "attached" instead of "appended" + or "below". + +2005-04-16 Lars Wirzenius + + * eoc.py: nice_7bit: Don't treat white space characters as + requiring MIME encoding. + +2005-04-10 Lars Wirzenius + + * Making release version 1.1.5. + +2005-04-10 Lars Wirzenius + + * eoc.py, templates/help*: Use "EoC" instead of "Enemies of + Carlotta" so as not to scare off users who don't get the movie + reference. Did not change filenames, since those are only + visible to the listmaster and the listmaster presumably can read + the home page. + +2005-04-10 Lars Wirzenius + + * eoc.py: Cleaning woman now removes groups without subscribers. + They can happen when we add a subscriber to a list when they are + there already and were the only member in their group. + +2005-04-10 Lars Wirzenius + + * eoc.py, eocTests.py, enemies-of-carlotta.1: Added option + "pristine-headers" to disable header MIME encoding. + +2005-04-10 Lars Wirzenius + + * enemies-of-carlotta.1, eoc.py, eocTests.py, + templates/msg-moderate: Applied patches from Pascal Hakim + to implement post moderators separately from list owners. + If there are no moderators, the list owners are also moderators. + +2005-04-10 Lars Wirzenius + + * eoc.py, eocTest.py, enemies-of-carlotta.1: Applied patch from + Jaakko Niemi to implement a feature to optionally ignore bounces. + +2005-04-10 Lars Wirzenius + + * eoc.py, eocTests.py: Fixes for treating addresses in a + case in-sensitive manner (but storing in the form that the + user gave). + +2005-04-10 Lars Wirzenius + + * eoc.py: Don't log debug() to stderr. Log info() and error() first + to file, and to stderr only after that. This should avoid problems + when MTA's buffer for our stderr output fills up. + +2005-04-10 Lars Wirzenius + + * templates/sub-owner-notification.fi, + templates/unsub-owner-notification.fi: Added the missing word + "tehdä". Pointed out by Leena Romppainen. + +2005-04-10 Lars Wirzenius + + * Makefile: Installation improvements. Added a mandir variable + and made man1dir, man1dires, and man1dirfr dependent on it. This + allows the manual dirs to easily be relocated. Also added a + command variable to allow renaming of the command in bindir. + +2004-12-06 Lars Wirzenius + + * Making release version 1.1.4. + +2004-12-06 Lars Wirzenius + + * eoc.py: Introduced locking of individual messages to MessageBox. + Used this to prevent more than one request for post approval + processing from happening at the same time. This should fix the + problem found by Pascal Hakim, where several moderators are so + fast to reply to a moderation request that the list gets two or + more copies of the same message. + +2004-12-06 Lars Wirzenius + + * TODO: Added. + +2004-12-05 Lars Wirzenius + + * enemies-of-carlotta.1.es: Updated Spanish manual page from + Ricardo Javier Cardenes. + +2004-12-04 Lars Wirzenius + + * README.CVS: Wrote. + +2004-11-27 Lars Wirzenius + + * Making release version 1.1.3. + +2004-11-27 Lars Wirzenius + + * eoc.py: If email.Header module is missing, then work as otherwise, + but don't MIME encode headers. This should restore the ability to + run on Python 2.1 and 2.2. + + * README: Noted. + +2004-11-27 Lars Wirzenius + + * eoc.py: --show-lists now alphabetizes the output. + +2004-11-27 Lars Wirzenius + + * templates/bounce-probe*, templates/sub-already*, + templates/unsub-already*: Unused, removed. + +2004-11-27 Lars Wirzenius + + * eoc.py: Added the beginnings of a plugin feature. The only hook + for now is "send_mail_to_subscribers_hook" which allows the plugin + to manipulate the mail before it is sent to the subscribers. + + * eoc.py: Added --no-act option to make testing easier. + + * enemies-of-carlotta.1: Documented the plugins. + +2004-11-27 Lars Wirzenius + + * eoc.py: Made it so that an empty template means the mail is not + sent at all. This can be used to prevent, say, "please wait for + moderation" messages from being sent on a per-list basis. + + * enemies-of-carlotta.1: Documented this. + +2004-11-27 Lars Wirzenius + + * eoc.py: Added --version option. + +2004-11-27 Lars Wirzenius + + * eoc.py: Allow setting of more list options with --create or + --edit: added new options --language, --mail-on-forced-unsubscribe, + --mail-on-subscription-changes. + + * enemies-of-carlotta.1: Documented them. + +2004-11-27 Lars Wirzenius + + * eoc.py: Added command line options --get and --set. + + * enemies-of-carlotta.1: Documented them. + +2004-11-27 Lars Wirzenius + + * eocTests.py: Added a test to check that the headers of sent + mails are only 7-bit ASCII characters (tab, newline, carriage + return, plus 32-126). + + * eoc.py: Encode outgoing mails to use only 7-bit characters in + the headers. This is required for the mails to work correctly + with MIME compliant mail readers. Note that this change makes + use of the email.Header module in Python 2.3, so with this change + the minimum supported Python version is 2.3. + + * README: Noted Python version requirement change. + +2004-09-18 Lars Wirzenius + + * Making release version 1.1.2. + +2004-09-18 Lars Wirzenius + + * enemies-of-carlotta.1.es: Added manual page translation to + Spanish by Ivan Juanes. Nobody expectes the Spanish manual + page. + + * Makefile: Install the Spanish manual page. + +2004-09-18 Lars Wirzenius + + * eoc.py, templates/*: Implemented suggestion from Magnus Holmgren + to make attached messages real MIME attachments rather than + just inserting the raw message code into a text/plain message. + Also converted all templates to UTF-8, since that makes it + easier to edit them for me. Hopefully nothing broke. + +2004-09-18 Lars Wirzenius + + * eoc.py: When an EoC exception occurs, print out a sensible + error message about it rather than letting Python print out + a stack trace. + +2004-09-18 Lars Wirzenius + + * eoc.py, eocTests.py, enemies-of-carlotta.1: Added support for + adding headers to and removing headers from mails sent to the + list. See the files headers-to-add and headers-to-remove in the + list directories. + +2004-09-12 Lars Wirzenius + + * eoc.py, eocTest.py: Added support for $listdir/headers-to-add + (but it still needs to be documented). + +2004-09-03 Lars Wirzenius + + * eoc.py, eocTest.py: Some refactoring and new unit tests. + +2004-08-28 Lars Wirzenius + + * Making release version 1.1.1. This release is dedicated to Jaakko + Niemi, the winner of the bug finding competition for 1.1.0. + +2004-08-28 Lars Wirzenius + + * enemies-of-carlotta.1: Added note about the "templates" directory + in the list specific directory using a patch sent by Jaakko Niemi, + who thereby won the bug finding competition. + +2004-08-26 Lars Wirzenius + + * eoc.py: in AddressParser, return the canonical name of the list, + instead of converting it to lower case. Also, when opening a list, + open using the canonical name rather than one converted to lower + case or derived from the incoming mail address. Thanks to Jaakko + Niemi for pointing these problems out. + + * eocTests.py: Improved testing cases when the name of a list is + not all lower case. + +2004-08-26 Lars Wirzenius + + * eoc.py, eocTests.py: Bugfix for bug found by Jaakko Niemi. + If the list has been created with a name containing upper case, + --is-list won't work. Fixed by making AddressParser convert + all list names it gets to lower case. + + * Makefile: Bugfix for bug found by Jaakko Niemi. Used bashism in + the install target, which made install not work under dash and + other shells. Fixed by manually expanding a {foo,bar} construct. + + * Makefile: Bugfix for bug found by Jaakko Niemi. qmqp.py was not + installed by "make install". Oops. + +2004-08-24 Lars Wirzenius + + * eoc.py: Changed md5sum_as_hex to use .hexdigest() instead of + doing the hex conversion manually. I should read more manuals. + Thanks to Magnus Holmgren for pointing this out. + +2004-08-23 Lars Wirzenius + + * Making release version 1.1.0. + +2004-07-25 Lars Wirzenius + + * eoc.py: Added a realname for the From line in message + templates. This is meant to avoid some spam filters. + +2004-07-25 Lars Wirzenius + + * eoc.py, eocTests.py: Refactoring. Moved address parsing into + its own class, for simplicity, and started work on making EoC + specific exceptions be more user friendly as far as error + messages are concerned. + +2004-07-25 Lars Wirzenius + + * eocTests.py: Added some test cases for recipient address + parsing. + +2004-07-09 Lars Wirzenius + + * eoc.py: Refactoring changes to make code nicer. No functional + changes. + +2004-07-09 Lars Wirzenius + + * eoc.py: Removed a bunch of documentation from the beginning of + the file. It was never finished, and was partly outdated, and it's + better put in README anyway, when I have a moment to flesh it out. + + * eocTests.py: Simplified (shortened) the implementation of a test + case in the anticipation of adding more cases to it. + +2004-03-31 Lars Wirzenius + + * templates/*.sv: Added Swedish translation from Magnus Holmgren. + +2004-02-21 Lars Wirzenius + + * eoc.py: Use the rejection address for subscription and posting + moderation requests. This is to make it easier for mutt users + to mail to the rejection address: they can just answer "no" + to the question about using Reply-To. Thanks to Antti-Juhani + Kaijanaho for pointing this out. + +2004-02-21 Lars Wirzenius + + * enemies-of-carlotta.1: Updated. + + * eoc.py: Added --sender and --recipient options on suggestion + from Tommi Virtanen. + +2004-02-21 Lars Wirzenius + + * eoc.py: Convert addresses to lower case so that random + capitalizations (especially in domain names) don't break things + for us. + +2004-02-21 Lars Wirzenius + + * eoc.py: Added some safeguards against subscribing addresses + without @ characters. + +2004-02-21 Lars Wirzenius + + * eoc.py: Catch GetoptError exception and print an error message. + This is nicer for the user than the stack trace. + +2004-02-21 Lars Wirzenius + + * qmqp.py: Added QMQP sending module by Jaakko Niemi. Thanks! + Did change encoding of single recipient so that Postfix will + accept that. + + * eoc.py: Changes to allow use of QMQP. Also, when logging a + sent mail, do it with a bit more white space so that it will be + easier to read the log file. + +2004-01-13 Lars Wirzenius + + * enemies-of-carlotta.1: Removed the word "also" from the + description of where subscription confirmation requests are + sent, since it was incorrect. Also added a suggestion that + --cleaning-woman should be run once per hour. + + * Makefile: Added patch from Jacek Konieczny to add DESTDIR + support. + +2003-09-07 Lars Wirzenius + + * Making release version 1.0.3. + +2003-09-07 Lars Wirzenius + + * enemies-of-carlotta.1: Removed erroneous quotation marks in + the Qmail section. Added section documenting all mail commands. + +2003-09-06 Lars Wirzenius + + * eoc.py: Bugfix. log_file() was broken in that it would replace + an already open output stream with one writing to /dev/null. + Fixed that. Also, not writing to /dev/null anymore in cases where + DOTDIR doesn't exist, but rather using a special purpose output + file stream simulator DevNull. + +2003-07-16 Lars Wirzenius + + * eoc.py, eocTests.py, enemies-of-carlotta.1: Added command line + option --post for bypassing moderation status on a list. + +2003-07-14 Lars Wirzenius + + * Making release version 1.0.2. + +2003-06-20 Lars Wirzenius + + * enemies-of-carlotta.1: Fixed the example for creating a + mailing list. + + * eoc.py: Added option --show-lists, from Stefan (who gave no + last name). + +2003-05-11 Lars Wirzenius + + * Making release version 1.0.1. + +2003-05-11 Lars Wirzenius + + * eoc.py: Bounce handling was totally broken, because the + final step was missing. What used to happen: EoC sends mail to + subscribers, it bounces, states goes from "ok" to "bounced"; + after one week, cleaning woman sends probe; two weeks after + bounce, cleaning woman unsubscribes. What was missing: if probe + bounces, state is set to "probebounced", and cleaning woman only + unsubscribes if state is "probebounced", otherwise it resets + state to "ok". This was quite an embarrassing bug. + +2003-05-11 Lars Wirzenius + + * eoc.py: --help option implemented. + * eoc.py: Don't create dotdir if only --help is given. + * eoc.py: If dotdir exists, but secret doesn't, create secret + instead of crashing. + +2003-04-13 Lars Wirzenius + + * Making release version 1.0. + + * There have been no changes. I had planned to improve + documentation, but the release party is tomorrow and, well, + I want this out. Anyway, I'll have a better idea what to put + into the manual if people first send me hate mail about the + difficult parts. + +2003-04-13 Lars Wirzenius + + * Making release version 0.23. + +2003-04-13 Lars Wirzenius + + * eoc.py: -reject now actually removes the rejected message. Oops. + +2003-04-13 Lars Wirzenius + + * eoc.py: Changed mail command -setlist to welcome new subscribers + and say goodbye to old ones. + + * eoc.py: Added mail command -setlistsilently, which is the same + as -setlist, but preserves the old behavior of not welcoming or + saying goodbye. + + * templates/setlist-confirm{,.es,.fi,.fr}: Removed the sentence + saying that new subscribers won't be welcomed. Hopefully I + recognized it correctly in French and Spanish. + +2003-03-22 Lars Wirzenius + + * Making release 0.22. + +2003-03-22 Lars Wirzenius + + * eoc.py: Messages sent by EoC that used to have an empty SMTP + sender (which made them look like bounce messages) now have + foo-ignore@example (for the foo@example.com list). This avoids + having to deal with different ways to specify an empty sender + and is also more correct since the messages sent by EoC aren't + really bounces. + +2003-03-16 Lars Wirzenius + + * Making release version 0.21. + +2003-03-15 Lars Wirzenius + + * templates/*.es and *.fr: Translations to Spanish for new + templates by Ricardo Javier Cardenes, and to French by Pierre + Machard. + +2003-03-15 Lars Wirzenius + + * eoc.py: Implemented -setlist command to allow list owner to + change the whole subscriber list as one operation. This should + be useful for people maintaining the list of subscriber outside + EoC's control, e.g., when the list is generated from a database. + + * templates/setlist-badlist, templates/setlist-confirm, + templates/setlist-done, templates/setlist-sorry: New templates + for this feature. Need to be translated. + + * templates/setlist-*.fi: Translated to Finnish. + +2003-03-15 Lars Wirzenius + + * eoc.py: If the sender of a subscription or unsubscription + request is a list owner, the list owners are requested to do + the confirmation, instead of the address being subscribed. + The welcome or goodbye message is still sent to the subscriber. + +2003-03-15 Lars Wirzenius + + * eoc.py: When someone is sending mail to a moderated list, + inform them that their message has been sent to the list owners + for approval. + + * templates/msg-wait: New mail template. Needs translations. + + * templates/msg-wait.fi: Translated to Finnish. + +2003-03-15 Lars Wirzenius + + * enemies-of-carlotta.1.fr: Proofread translation by Gérard + Delafond, sent by Pierre Machard. + +2003-03-15 Lars Wirzenius + + * templates/footer.es, templates/footer.es: Translations re-worded + so that they only use 7 bit ASCII characters, to avoid charset + problems. Thanks to Pierre Machard and Ricardo Javier Cardenes. + +2003-03-14 Lars Wirzenius + + * eoc.py: When subscribing to a list with subscription moderation, + the would-be subscriber is notified that a request has been sent + to the moderator and that they need to be patient. + + * eocTests.py: Related changes. + + * templates/sub-wait: New mail template for this. Needs + translation. + + * templates/sub-wait.fi: Translated sub-wait to Finnish. + + * templates/*.fr: Added Content-type headers to all templates. + +2003-03-14 Lars Wirzenius + + * templates/footer.fi: Reworded to use only us-ascii letters. + +2003-03-08 Lars Wirzenius + + * BENCHMARKS, eoc-benchmark, eoc-benchmark-procmailrc: Added. + +2003-02-25 Lars Wirzenius + + * enemies-of-carlotta.1.fr, templates/*.fr: Added translations + sent by Pierre Machard (pierre at machard.org). + + * templates/*.es: Added translations sent by Ricardo Javier + Cardenes (ricardo at conysis.com). + + * Making release 0.20. + +2003-02-22 Lars Wirzenius + + * eoc.py: Added configuration option "language". + + * templates/*.fi: Translated templates to Finnish so that I can + test the "language" configuration option. + + * enemies-of-carlotta.1: Added "CONFIGURATION" section. + + * Making relase 0.19. + +2003-02-18 Lars Wirzenius + + * eoc.py: Use os.path.isfile instead of os.path.exists to see + whether a MessageBox contains a file. + +2003-02-16 Lars Wirzenius + + * eoc.py: Command line options --edit, --subscribe, --unsubscribe, + and --list now allow the list name to be abbreviated by + leaving out the domain (and @). I don't want to allow shorter + abbreviations to make it less likely that you specify the wrong + list by mistake. + + * eoc.py: When a bouncing address is restored to "ok" status, + its bounce message is removed from the bounce-box. + + * eoc.py: Added configuration options mail-on-forced-unsubscribe + and mail-on-subscription-changes. + + * Making release 0.18. + +2003-02-09 Lars Wirzenius + + * eoc.py: Applied patch from Ricardo Javier Cardenes + to implement posting option "auto", which + will let messages from subscribers automatically into the list + and send others to the moderator. + + * Making release 0.17. + +2003-01-12 Lars Wirzenius + + * Releasing version 0.16.1. + + * eoc.py: Bounce message quoting had a stupid bug: it didn't + add newlines. + +2003-01-11 Lars Wirzenius + + * Releasing version 0.16. + + * eoc.py: First bounce message is now saved (up to 4096 bytes) and + quoted in the bounce-warning message. + + * eoc.py: When state changes to bounce, it is noted in the log file. + + * eoc.py, eocTests.py: Addresses now can't be added twice to + the list. + + * eoc.py: Added missing exception MissingTemplate. + +2002-12-11 Lars Wirzenius + + * Releasing version 0.15. + +2002-12-08 Lars Wirzenius + + * Added --moderate option for asking a message to be + moderated. To be used for spam filtering, at least. + +2002-12-08 Lars Wirzenius + + * eocTests.py: Set the quiet flag, so that "make check" doesn't + output debuggning messages. + +2002-12-08 Lars Wirzenius + + * eoc.py: Added --smtp-server option for sending via SMTP, not + /usr/sbin/sendmail. + + * enemies-of-carlotta.1: Documented all options. + + * eocTests.py: Don't require a dot-eoc directory for running + the tests. + +2002-12-08 Lars Wirzenius + + * Releasing version 0.14. + +2002-12-08 Lars Wirzenius + + * eoc.py: Also look in the list's template directory for templates. + This is useful for doing list specific customizations, e.g., for + the footer. + +2002-12-07 Lars Wirzenius + + * enemies-of-carlotta: Wrote startup wrapper for faster startup. + + * Makefile: Install startup wrapper as the binary, and eoc.py + into the share directory, plus compile eoc.py on installtion.§ + +2002-11-04 Lars Wirzenius + + * eoc.py: Cleaning woman now logs addresses it removes and sends + a final goodbye message when it does. + + * templates/bounce-probe: Fixed spelling mistake. + + * templates/bounce-goodbye: Wrote. + +2002-10-26 Lars Wirzenius + + * Added support for a footer to be appended to each mail. + +2002-10-26 Lars Wirzenius + + * A bounce splits a group with many addresses. If the original group + contained addresses in several domains, it is split into groups + according to domains, otherwise into groups with single addresses. + + * --cleaning-woman joins groups that haven't bounced for a week + into bigger groups. + +2002-10-26 Lars Wirzenius + + * Releasing version 0.13. + +2002-10-26 Lars Wirzenius + + * eoc.py: Bug fix. When there were over ten subscribers, the + generation of group ids was wrong, since a list of group ids + was sorted lexically, instead of numerically. + + * eocTests.py: Added test case to test this. + +2002-10-26 Lars Wirzenius + + * eoc.py: Added --quiet and --sendmail options. + + * eocTests.py: Related changes. + +2002-10-21 Lars Wirzenius + + * Releasing version 0.12. + +2002-10-21 Lars Wirzenius + + * --incoming always reads stdin. + +2002-10-11 Lars Wirzenius + + * Makefile: added $(prefix), installation of templates. + + * fix-config: directory is given on the command line. + + * eoc.py: Added some more debugging output. + +2002-09-22 Lars Wirzenius + + * Starting upstream ChangeLog. Sooner or later someone else is + going to be maintaining the Debian packaging stuff, and then + it's sensible to keep things separate. + + * Removed debian/*. I can live without them myself, and I don't + want to maintain them when there are perfectly too Debian + developers available. :) + + * Makefile: Added an install target. Very simplistic. + + * COPYING, README: Added these files. This is beginning to seem + like something that can be shown in public. + + * Releasing version 0.11. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5420849 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +prefix = /usr/local +bindir = $(prefix)/bin +mandir = $(prefix)/share/man +man1dir = $(mandir)/man1 +man1dires = $(mandir)/es/man1 +man1dirfr = $(mandir)/fr/man1 +sharedir = $(prefix)/share/enemies-of-carlotta +command = enemies-of-carlotta + +all: check + +check: + python testrun.py + +install: + install -d $(DESTDIR)$(bindir) + install -d $(DESTDIR)$(sharedir) + install -d $(DESTDIR)$(man1dir) + install -d $(DESTDIR)$(man1dires) + install -d $(DESTDIR)$(man1dirfr) + sed 's,^SHAREDIR=.*,SHAREDIR="$(sharedir)",' enemies-of-carlotta \ + > $(DESTDIR)$(bindir)/$(command) + chmod 755 $(DESTDIR)$(bindir)/$(command) + sh fix-config $(sharedir) < eoc.py > $(DESTDIR)$(sharedir)/eoc.py + install -m 0755 qmqp.py $(DESTDIR)$(sharedir)/qmqp.py + python -c 'import py_compile; py_compile.compile("$(DESTDIR)$(sharedir)/eoc.py")' + python -c 'import py_compile; py_compile.compile("$(DESTDIR)$(sharedir)/qmqp.py")' + install -m 0644 enemies-of-carlotta.1 $(DESTDIR)$(man1dir)/$(command).1 + install -m 0644 enemies-of-carlotta.1.es $(DESTDIR)$(man1dires)/$(command).1 + install -m 0644 enemies-of-carlotta.1.fr $(DESTDIR)$(man1dirfr)/$(command).1 + install -m 0644 templates/????* $(DESTDIR)$(sharedir) diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..6b92302 --- /dev/null +++ b/NEWS @@ -0,0 +1,131 @@ +NEWS file for Enemies of Carlotta, a mailing list manager + +Significant user-visible changes from version 1.1.4 to version 1.1.5: + + * Debug logs no longer go to stderr, just to the log file. This should + prevent sudden deaths from MTA buffers becoming full. + + * Bugs about treating addresses in case-insensitive manners fixed. + + * Lists can now be configured to ignore bounces. (By Jaakko Niemi.) + + * Moderators for posting can be separate from list owners. (By Pascal + Hakim.) + + * Header MIME encoding can be disabled per list. + +Significant user-visible changes from version 1.1.3 to version 1.1.4: + + * Fixed race condition when multiple moderators approve or reject the + same message at the same time. This could result in the message being + sent multiple times. + +Significant user-visible changes from version 1.1.2 to version 1.1.3: + + * Mails sent by EoC now get their headers MIME encoded (if the version + of Python in use has the email.Header module). + + * A bunch of new command line options: --get, --set, --language, + --mail-on-forced-unsubscribe, --mail-on-subscription-changes, + --version + + * Making an empty template file prevents the corresponding mail from + being sent at all. This can be used, for example, to prevent + "please wait for moderation" mails. + + * The beginnings of a plugin architecture, where plugins can modify + EoC's operation or the mails it sends. There is only one supported + hook at the moment, though, but more will be added as needed. + + * --show-lists now shows lists in alphabetical order. + +Significant user-visible changes from version 1.1.1 to version 1.1.2: + + * This is still a DEVELOPMENT release. There have been many changes + and the documentation and translations are probably out of date. + There may be bugs lurking as well. + + * Headers of messages sent to the list can be manipulated in simple + ways: some headers can be removed and others added. + + * Error messages are now user-friendlier than Python's stack traces. + + * When EoC attaches a message to something it sends to a user, it + does so by making it a proper MIME attachment. + + * Message templates are now in UTF-8. + + * Manual page in Spanish. + +Significant user-visible changes from version 1.1.0 to version 1.1.1: + + * This is still a DEVELOPMENT release. There have been many changes + and the documentation and translations are probably out of date. + There may be bugs lurking as well. + + * Manual page mentions the "templates" sub-directory of the list specific + directory. + + * QMQP implementation is now installed by "make install". + + * Bashism in the Makefile have been removed. + + * Bugs regarding lists with names that are not all lower case have + been fixed. + + * This release is dedicated to Jaakko Niemi who found most bugs in 1.1.0. + +Significant user-visible changes from version 1.0.3 to version 1.1.0: + + * This is a DEVELOPMENT release. There have been many changes and the + documentation and translations are probably out of date. There may be + bugs lurking as well. + + * QMQP support from Jaakko Niemi. + + * Command line syntax errors result in an error message rather than + a stack trace. + + * Upper and lower case are no longer treated as being different characters + in e-mail addresses. + + * New options --sender and --recipient. + + * Subscription and posting moderation requests now use the rejection + address to make life easier for mutt users. + + * Swedish translation. + + * Mails sent by EoC now have a "real name" in the From header, to lessen + the chance they are caught by spam filters. + +Significant changes from version 1.0.2 to version 1.0.3: + + * Added option --post, to bypass moderation. + + * The manual page now documents all mail command addresses. + + * Bug fix: Qmail section in the manual page had extra quotation marks. + They have been removed. + + * Bug fix: Log file didn't get more than the first message of each + run. Now fixed so that it gets all messages. (Stderr got everything + even before, though.) + +Significant changes from version 1.0.1 to version 1.0.2: + + * Manual page fix: the example for creating a new list now actually + works. + + * Added option --show-lists, to list all lists. + +Significant changes from version 1.0 to version 1.0.1: + + * Bug fixes only. + + * --help option implemented. + + * --create: If ~/.enemies-of-carlotta exists, but a file named + "secret" inside it doesn't, create the file instead of crashing. + + * Bounce handling fixed. diff --git a/README b/README new file mode 100644 index 0000000..6d2e4af --- /dev/null +++ b/README @@ -0,0 +1,18 @@ +README for Enemies of Carlotta, a mailing list manager +by Lars Wirzenius, liw@iki.fi + + +Enemies of Carlotta is a simple mailing list manager that mimicks the +ezmlm (http://www.ezmlm.org/) commands. Please see the manual page +or the home page (http://liw.iki.fi/liw/eoc/) for documentation, and +join the mailing list (send mail to eoc-subscribe@liw.iki.fi). + +You need Python 2.3 or newer, and lockfile (from procmail). If you are +willing to live without having headers encoded with MIME, Python 2.1 or +2.2 should also work. + +To install, edit Makefile, the variables bindir and man1dir, and +then run "make install" as root. + +Enemies of Carlotta is licensed using the GNU General Public License, +version 2. diff --git a/enemies-of-carlotta b/enemies-of-carlotta new file mode 100644 index 0000000..4852c46 --- /dev/null +++ b/enemies-of-carlotta @@ -0,0 +1,16 @@ +#!/usr/bin/python +# +# This file works as the Enemies of Carlotta startup wrapper. The real +# program is in eoc.py, stored in SHAREDIR (see below). It is large enough +# that the time Python takes to parse and compile it to bytecode is +# significant, therefore we use a very short wrapper (this file) and +# install eoc.py in a way that allows it to be compiled, thus reducing +# startup time. + +SHAREDIR="." + +import sys +sys.path.insert(0, SHAREDIR) + +import eoc +eoc.main(sys.argv[1:]) diff --git a/enemies-of-carlotta.1 b/enemies-of-carlotta.1 new file mode 100644 index 0000000..322ca65 --- /dev/null +++ b/enemies-of-carlotta.1 @@ -0,0 +1,606 @@ +.TH ENEMIES\-OF\-CARLOTTA 1 +.SH NAME +enemies\-of\-carlotta \- a simple mailing list manager +.SH SYNOPSIS +.B enemies\-of\-carlotta +.IR "" [ options "] [" addresses ] +.SH "DESCRIPTION" +.B enemies\-of\-carlotta +is a simple mailing list manager. +If you don't know what a mailing list manager is, you should learn +what they are before trying to use one. +A manual page is unfortunately too short to explain it. +.PP +Enemies of Carlotta keeps all data about the lists in the +.I ~/.enemies\-of\-carlotta +directory. +It will be created automatically when you create your first list. +You need to arrange manually for the mails to be processed by the +list manager. +The details differ from mail system to another. +For QMail and Postfix, see below. +.PP +Each list has one or more owners, who also moderate subscriptions or +moderate some or all postings. +On completely unmoderated lists the list owners are responsible for +answering questions about the list. +On completely moderated lists, they have to approve each message before +it is sent to the list. +On lists with +.IR posting=auto , +messages from subscribers are sent automatically to the list, and the +moderators have to approve the rest. +.SH OPTIONS +.TP +.BR \-\-name= foo@example.com +Specify the list the command is to operate on. +Most of the remaining options require you to set the list name with this +option. +With the \-\-edit, \-\-subscribe, \-\-unsubscribe, and \-\-list options, +the name can be abbreviated to by leaving out the @ sign and domain. +.TP +.BI \-\-create +Create a new list. +You must specify at least one owner with +.BR \-\-owner . +.TP +.BI \-\-owner= address +Specify a list owner when creating or editing a list. +.TP +.BI \-\-moderator= address +Specificy a list moderator when creating or editing a list. +.TP +.BI \-\-language= language\-code +Set the language code used for looking up template files. +The code should be empty (the default, meaning English), or a two\-letter +code such as +.B fi +or +.BR es . +.TP +.B \-\-cleaning\-woman +Deal with bouncing addresses and do other cleanup. +You must run +.B "enemies\-of\-carlotta \-\-cleaning\-woman" +periodically, such as once per hour. +It will clean up all your lists. +.TP +.BI \-\-destroy +Destroy the list. +.TP +.BI \-\-edit +Modify the list configuration. +.TP +.BI \-\-subscription= type +When creating a list, set its subscription mode to +.I free +or +.IR moderated . +Use with +.BR \-\-edit , +or +.BR \-\-create . +.TP +.BI \-\-posting= type +When creating a list, set its posting mode to +.IR free , +.IR auto , +or +.IR moderated . +Use with +.BR \-\-edit , +or +.BR \-\-create . +.TP +.BI \-\-archived= yes\-or\-no +Should list messages be archived to the +.B archive\-box +directory in the list directory under the +.B "~/.enemies\-of\-carlotta" +directory. +Use +.I yes +or +.IR no . +.TP +.BI \-\-mail\-on\-subscription\-changes= yes\-or\-no +Should the list owners be notified when someone subscribes to or +unsubscribes from the list? +Use +.I yes +or +.IR no . +Default is no. +.TP +.BI \-\-mail\-on\-forced\-unsubscription= yes\-or\-no +Should list owners be notified when someone is forcibly dropped from +the list due to too much bouncing? +Use +.I yes +or +.IR no . +Default is no. +.TP +.BI \-\-ignore\-bounce= yes\-or\-no +Should bounces be ignored? +Use +.I yes +or +.IR no . +Default is no. +.TP +.BI \-\-list +List the subscribers of a list. +.TP +.BI \-\-subscribe +Add subscribers to a list. +The non\-option arguments are the addresses to be subscribed. +Note that addresses added this way won't be sent confirmation requests. +.TP +.BI \-\-unsubscribe +Remove subscribers from a list. +The non\-option arguments are the addresses to be unsubscribed. +Note that addresses removed this way won't be sent confirmation requests. +.TP +.B \-\-incoming +Deal with an incoming message in the standard input. +The SMTP envelope sender address must be given in the +.I SENDER +environment variable, and the SMTP envelope recipient address in the +.I RECIPIENT +environment variable. +(QMail and Postfix do this automatically.) +.TP +.BI \-\-skip\-prefix= string +Before analyzing the recipient address to see which list it refers, remove +.I string +from its beginning. +This helps deal with QMail and Postfix virtual domains, see above. +.TP +.BI \-\-domain= domain.name +Before analyzing the recipient address to see which list it refers, replace +the domain name part with +.IR domain.name . +This helps deal with Postfix virtual domains. +.TP +.BI \-\-is\-list +Does the address specified with +.B \-\-name +refer to a valid list? +This sets the exit code to zero (success) if it does, or one (failure) +if it does not. +.TP +.BI \-\-sendmail= pathname +Use +.I pathname +instead of +.B /usr/sbin/sendmail +for sending mail via a command line interface. +Note that the command must obey the sendmail command line interface. +.TP +.BI \-\-smtp\-server= hostname +Send mail using the SMTP server at +.I hostname +(port 25). +The server must be configured to allow the list host to relay mail +through it. +Note that a command line interface is used by default; +SMTP sending is used only if you use this option. +.TP +.BI \-\-qmqp\-server= hostname +Send mail using the QMQP server at +.I hostname +(port 628). +The server must be configured to allow the list host to relay mail +through it. +Note that a command line interface is used by default; +QMQP sending is used only if you use this option. +.TP +.BI \-\-moderate +Force an incoming message to be moderated, even if it is going to a list +where posting is free. +This can be used for spam filtering: +you pipe incoming messages through whatever spam filter you choose to use +and if the mssage looks like spam, you ask it to be moderated by a human. +.TP +.BI \-\-post +Force an incoming message to be posted, even if it is going to a list +where posting is moderated. +This can be used when there is an external check for whether a mail +is acceptable to the list, e.g., by checking digital signatures. +.TP +.BI \-\-quiet +By default, debugging log messages are sent to the standard error output +stream. +With this option, they aren't. +.TP +.BI \-\-sender= foo@example.com +.TP +.BI \-\-recipient= foo@example.com +These two options are used with +.B \-\-incoming +and +.B \-\-is\-list +to override the environment variables +.B SENDER +and +.BR RECIPIENT , +respectively. +.TP +.BI \-\-get +Get the values of one or more configuration variables. +The name of the variables are given on the command line after the options. +Each value is printed on a separate line. +.TP +.BI \-\-set +Set the values of one or more configuration variables. +The names and values are given on the command line after the options +and separated by an equals sign ("="). +For example, the following would set the language of a list to Finnish: +.B "enemies\-of\-carlotta \-\-name=foo@bar \-\-set language=fi" +.TP +.BI \-\-version +Print out the version of the program. +.TP +.BI \-\-show\-lists +List the lists enemies\-of\-carlotta knows about. +.SH CONFIGURATION +Each list is represented by a directory, named after the list, in +.IR ~/.enemies\-of\-carlotta . +That directory contains several files and directories, described below. +In general, it is not necessary to touch these at all. +However, some esoteric configuration can only be done by hand editing +of the list configuration file. +.TP +.B config +The list configuration file. +Contents are described below. +.TP +.B subscribers +Subscriber database. +Each line contains a subscriber group, with the first five space +delimited fields being group identifier, status, timestamp for when +the group was created, timestamp for the bounce that made it switch +from status 'ok' to 'bounced', and the bounce identifier. +.TP +.B archive\-box +Archived messages. +.TP +.B bounce\-box +Bounce messages groups not in state 'ok'. +.TP +.B headers\-to\-add +These headers are added to the mails sent to the list. +They are copied to the beginning of the existing headers exactly as they +are in the file, after list headers ("List\-ID" and such) have been added +and those mentioned in +.B headers\-to\-remove +have been removed. +.TP +.B headers\-to\-remove +These headers are removed from mails sent to the list. +.TP +.B moderation\-box +Messages waiting for moderator approval. +.TP +.B subscription\-box +Subscription and unsubscription requests waiting to be confirmed by the user. +.TP +.B templates +Directory containing list spesific templates (optional). If this +directory exists, templates are searched from it before going for +system wide templates. An empty file here means the corresponding +corresponding message is not sent at all. This can, for example, to +be used to turn off the "please wait for moderator" mails on a per\-list +basis. +.TP +.B plugins +Directory containing plugins, Python source files that are loaded +automatically by EoC upon startup. +The plugins may change how EoC operates. +.PP +The +.B config +file has a +.IR keyword = value +format: +.PP +.RS +.nf +[list] +owners = liw@liw.iki.fi +archived = no +posting = free +subscription = free +mail\-on\-subscription\-changes = yes +mail\-on\-forced\-unsubscribe = yes +language = fi +.fi +.RE +.PP +The keywords +.BR archived , +.BR posting , +and +.B subscription +correspond to the options with the same names. +Other keywords are: +.TP +.B owners +List of addresses for the owners. Set with the +.I \-\-owner +option. +.TP +.B moderators +List of addresses for the moderators. Set with the +.I \-\-moderator +option. +.TP +.B mail\-on\-subscription\-changes +Should the owners be mailed when users subscribe or unsubscribe? +.TP +.B mail\-on\-forced\-unsubscribe +Should the owners be mailed when people are removed from the list due to +excessive bouncing? +.TP +.B ignore_bounce +Bounce messages are ignored on this list. Useful for example if +list should have static subscriber list. +.TP +.B language +Suffix for templates, to allow support for multiple languages. +(If +.I language +is set to "fi", then the template named "foo" is first searched as +"foo.fi".) +.TP +.B pristine\-headers +Do not MIME encode the headers. Set to "yes" to not encode, anything +else (including empty or unset) means encoding will happen. +.SH EXAMPLES +To create a list called +.IR moviefans@example.com , +owned by +.IR ding@example.com , +use the following command (all on one line): +.sp 1 +.nf +.RS +enemies\-of\-carlotta \-\-name=moviefans@example.com +\-\-owner=ding@example.com \-\-create +.RE +.PP +Note that you need to arrange mail to arrive at the list (and its +command addresses) by configuring your mail system. +For Qmail and Postfix, see below. +.PP +To see the subscribers on that list: +.sp 1 +.RS +enemies\-of\-carlotta \-\-name=moviefans@example.com \-\-list +.RE +.PP +People wanting to subscribe to the list should mail +.sp 1 +.RS +moviefans\-subscribe@example.com +.RE +.SH QMAIL +With QMail, to arrange for incoming mail to be processed by Enemies of +Carlotta, you need to create a couple of +.I .qmail\-extension +files per list. +For example, if your username is joe and you wish to run the +joe\-fans mailing list, you need to create two files, +.I .qmail\-fans +and +.IR .qmail\-fans\-default , +containing +.sp 1 +.RS +|enemies\-of\-carlotta \-\-incoming +.RE +.PP +If you're running a virtual domain, example.com, and the mails are +being delivered to via +.I /var/qmail/control/virtualdomains to joe\-exampledotcom, the +files would be called +.I .qmail\-exampledotcom\-fans +and +.I .qmail\-exampledotcom\-fans\-default +and would contain +.sp 1 +.RS +|enemies\-of\-carlotta \-\-incoming \-\-skip\-prefix=joe\-exampledotcom\- +.RE +.sp 1 +(all on one line, of course, in case the manual page formatter breaks it +on several lines). +.SH POSTFIX +With Postfix, you need to set up a +.I .forward +file containing +.sp 1 +.RS +"|procmail \-p" +.RE +.sp 1 +and then a +.I .procmailrc +file containing +.sp 1 +.RS +:0 +.br +* ? enemies\-of\-carlotta \-\-name=$RECIPIENT \-\-is\-list +.br +| enemies\-of\-carlotta \-\-incoming +.RE +.PP +To use Enemies of Carlotta with a Postfix virtual domain, you need to +set up a +.I "virtual regular expression map," +typically called +.I /etc/postfix/virtual_regexp +(add +.I "virtual_maps = regexp:/etc/postfix/virtual_regexp" +to your +.I /etc/postfix/main.cf +file to enable it). +The regexp file needs to do ugly things to preserve the recipient +address. +Add the following to the regexp file: +.sp 1 +.RS +/^your\.virtual\.domain$/ dummy +.br +/^(yourlist|yourlist\-.*)@(your\.virtual\.domain)$/ joe+virtual\-$1 +.RE +.sp 1 +That's two lines. Use +.B joe-virtual +instead, if +.I recipient_delimiter +for your Postfix is configured to a minus instead of a plus.. +Then, in your +.I .procmailrc +file, add the +.I "\-\-skip\-prefix=joe\-virtual\-" +and +.I \-\-domain=your.virtual.domain +options to both calls to +.BR enemies\-of\-carlotta . +.PP +(Yes, I think these things are much too complicated, too.) +.SH "MAIL COMMANDS" +Users and list owners use Enemies of Carlotta via e\-mail using +command addresses such as +.BR foo\-subscribe@example.com . +Here is a list of all command addresses list users and owners can give. +In all these examples, the name of the mailing list is +.BR foo@example.com . +.SS "Mail commands anyone can use" +These commands are meant for everyone's use. +They don't require any special priviledges. +.TP +.BR foo@example.com +Send mail to all list subscribers. +The message may have to be manually approved by the list moderators first, +and they have the power to reject a message. +.TP +.BR foo\-owner@example.com +Send mail to the list owner or owners instead. +.TP +.BR foo\-help@example.com +Sending mail to this address makes the list manager reply with +the help message for the list. +.TP +.BR foo\-subscribe@example.com +Send mail to this address to subscribe to a list. +The list manager will respond with a confirmation request. +You won't be subscribed unless you reply to the confirmation request. +This way, malicious people can't put your address on a mailing list, +or many mailing lists. +.TP +.BR foo\-subscribe\-joe=example.com@example.com +This is a second form of the subscription address. +If you want to subscribe to the list with another address than the +one you're sending mail from, use this one. +In this case, the address to be subscribed is joe@example.com. +Note that the confirmation request is sent to Joe, since it is +his address that is to be added to the list. +.TP +.BR foo\-unsubscribe@example.com +To unsubscribe from a list, send mail to this address from the address +that is subscribed to the list. +Again, you will receive a confirmation request, to prevent malicious +people from unsubscribing you against your will. +.TP +.BR foo\-unsubscribe\-joe=example.com@example.com +To unsubscribe Joe, use this address. +Again, it is Joe who gets to confirm. +.SS "Mail commands for the list owners" +These are commands that list owners can use to administer their list. +.TP +.BR foo\-subscribe\-joe=example.com@example.com +If a list owner sends mail like this, it is they who get the confirmation +request, not Joe. +It is generally better for people to subscribe themselves, but sometimes +list owners want to do it, when they have permission from the person +and feel helpful. +.TP +.BR foo\-unsubscribe\-joe=example.com@example.com +List owners can also unsubscribe other people. +.TP +.BR foo\-list@example.com +To see who are on the list, this is the address to use. +It only works if the sender address is one of the list owners. +The sender address is the one used on the SMTP level, +not the one in the From: header. +.TP +.BR foo\-setlist@example.com +This lets a list owner set the whole subscriber list at once. +This is similar to using lots and lots and lots of \-subscribe and +\-unsubscribe commands, only less painful. +Everyone who is added to the list gets a welcome message, and +everyone who is removed from the list gets a goodbye message. +.TP +.BR foo\-setlistsilently@example.com +This is similar to \-setlist, but no welcome and goodbye messages are sent. +.SH PLUGINS +Enemies of Carlotta supports plugins. +If you don't know what Python programming is, you may want to skip this +section. +.PP +A plugin is a Python module (file named with a +.B .py +suffix), placed in the +.B ~/.enemies\-of\-carlotta/plugins +directory. +The plugins are loaded automatically upon startup, if their declared +interface version matches the one implemented by Enemies of Carlotta. +The interface version is declared by the module global variable +.BR PLUGIN_INTERFACE_VERSION . +.PP +Plugins can define hook functions that are called by appropriate places in +the EoC code. +At the moment, the only hook function is +.BR send_mail_to_subscribers_hook , +which can manipulate a mail message before it is sent to the subscribers. +The function must look like this: +.PP +.ti +5 +def send_mail_to_subscribers_hook(list, text): +.PP +The +.I list +argument is a reference to the +.I MailingList +object that corresponds to the list in question, and +.I text +is the complete text of the mail message as it exists. +The function must return the new contents of the mail message. +.SH FILES +.TP +.I ~/.enemies\-of\-carlotta +All files related to your mailing lists. +.TP +.I ~/.enemies\-of\-carlotta/secret +Secret password used to generate signed addresses for bounce checking +and subscription verification. +.TP +.I ~/.enemies\-of\-carlotta/foo@example.com +Directory containing data pertaining to the foo@example.com list. +Except for the +.I config +file in this directory, you shouldn't edit anything by hand. +.TP +.I ~/.enemies\-of\-carlotta/foo@example.com/config +Configuration file for the mailing list. +You may need to edit this file by hand if you wish to change moderation +status or list owners. +.SH "SEE ALSO" +You may want to visit the +.I "Enemies of Carlotta" +home page at +.IR http://www.iki.fi/liw/eoc/ . diff --git a/enemies-of-carlotta.1.es b/enemies-of-carlotta.1.es new file mode 100644 index 0000000..40a02c1 --- /dev/null +++ b/enemies-of-carlotta.1.es @@ -0,0 +1,670 @@ +.TH ENEMIES-OF-CARLOTTA 1 +.SH NAME +enemies-of-carlotta \- sencillo gestor de listas de correo +.SH SYNOPSIS +.B enemies-of-carlotta +.IR "" [ opciones "] [" direcciones ] +.SH "DESCRIPCIÓN" +.B enemies-of-carlotta +es un gestor sencillo para listas de correo. +Si no sabe qué es un gestor de listas de correo, es mejor que +aprenda lo que son, antes de intentar usar uno concreto. +Por desgracia, no hay espacio para eso en una página de +manual. +.PP +Enemies of Carlotta mantiene todos los datos sobre las listas de correo +en un directorio llamado +.I ~/.enemies-of-carlotta . +Se creará automáticamente en cuanto Usted cree la primera lista. +Tendrá que hacer arreglos a mano para que el gestor de listas pueda +procesar los mensajes. +Los detalles varían de un servidor de correo a otro. +Para qmail y Postfix, véase infra. +.PP +Cada lista tiene uno o más propietarios, que también moderan suscripciones +o incluso algunos o todos los envíos a la lista. +En listas sin moderación alguna, el propietario de la lista es el +responsable de contestar las dudas acerca de la lista. +En listas con moderación completa, tienen que aprobar cada mensaje, antes +de que éste pueda enviarse a la lista. +En listas con la opción +.IR posting=auto , +los mensajes de los suscriptores se envían automáticamente a la lista, +y los moderadores tienen que aprobar el resto de mensajes. +.SH OPCIONES +.\" --------------------------------------------------------------- +.TP +.BR --name= lista@example.com +Especifica sobre qué lista ha de actuar la orden especificada. +Casi todas las restantes opciones precisan que especifique antes el nombre +de la lista con la opción antedicha. +Con las opciones +--edit, --subscribe, --unsubscribe, y --list , +el nombre puede abreviarse quitando el signo @ y el dominio que le sigue. +.\" --------------------------------------------------------------- +.TP +.BI --create +Crear una lista nueva. +Ha de especificar al menos un propietario con la opción +.BR --owner . +.\" --------------------------------------------------------------- +.TP +.BI --owner= dirección +Al crear una lista, especifica un propietario de la lista. +.\" --------------------------------------------------------------- +.TP +.BI --language= código-idioma +Establece el código de idioma que se usa para buscar plantillas. +El código debería estar vacío (opción por defecto, es decir inglés), o +un código de dos letras como +.B fi +o +.BR es . +.\" --------------------------------------------------------------- +.TP +.B --cleaning-woman +Se encarga de las direcciones de rebote y hace otras limpiezas varias. +Ha de ejecutar periódicamente +.B "enemies-of-carlotta --cleaning-woman" , +algo así como una vez por hora. +Efectuará una limpieza de todas sus listas. +.\" --------------------------------------------------------------- +.TP +.BI --destroy +Eliminar la lista. +.\" --------------------------------------------------------------- +.TP +.BI --edit +Modificar la configuración de la lista. +.\" --------------------------------------------------------------- +.TP +.BI --subscription= tipo +Al crear una lista, establece su modo de suscripción a +.I free +(libre) o bien +.IR moderated +(moderado). +Úselo con +.BR --edit , +o con +.BR --create . +.\" --------------------------------------------------------------- +.TP +.BI --posting= tipo +Al crear una lista, establece su modo de envío de mensajes a +.IR free +(libre), +.IR auto +(auto), +o bien +.IR moderated +(moderado). +Úselo con +.BR --edit , +o con +.BR --create . +.\" --------------------------------------------------------------- +.TP +.BI --archived= yes-o-no +Especifica si los mensajes de la lista deben archivarse en el directorio +.B archive-box +en el directorio de la lista que a su vez existe dentro del directorio +.B "~/.enemies-of-carlotta" . +Utilice +.I yes +o bien +.IR no . +.\" --------------------------------------------------------------- +.TP +.BI --mail-on-subscription-changes= yes-o-no +¿Debería notificarse a los dueños de la lista cuando alguien se +suscribe o desuscribe de ella? +Use +.I yes +o +.IR no . +Por defecto es no. +.\" --------------------------------------------------------------- +.TP +.BI --mail-on-forced-unsubscription= yes-o-no +¿Debería notificarse a los dueños de la lista cuando se elimina a +alguien de la lista forzosamente por exceso de rebotes? +Use +.I yes +o +.IR no . +Por defecto es no. +.\" --------------------------------------------------------------- +.TP +.BI --list +Muestra los suscriptores de una lista de correo. +.\" --------------------------------------------------------------- +.TP +.BI --subscribe +Añade suscriptores a una lista de correo. +Los argumentos que no son opciones, son las direcciones que hay que +suscribir a la lista. +Observe que las direcciones que se añadan mediante este procedimiento +no recibirán una confirmación de suscripción, sino que se las +suscribirá directamente. +.\" --------------------------------------------------------------- +.TP +.BI --unsubscribe +Elimina suscriptores de una lista de correo. +Los argumentos que no son opciones, son las direcciones que hay que +desuscribir de la lista. +Observe que las direcciones que se eliminen mediante este procedimiento +no recibirán una confirmación de desuscripción, sino que se las eliminará +directamente. +.\" --------------------------------------------------------------- +.TP +.B --incoming +Encargarse de un mensaje que se recibe por la entrada estándar. +La dirección del remitente del envoltorio SMTP +(envelope sender address) debe especificarse mediante la variable +de entorno +.I SENDER , +y la dirección del destinatario del envoltorio SMTP +(SMTP envelope recipient address) debe especificarse en la variable +de entorno +.I RECIPIENT . +(qmail y Postfix lo hacen automáticamente). +.\" --------------------------------------------------------------- +.TP +.BI --skip-prefix= cadena +Antes de analizar la dirección del destinatario para ver a qué lista se +refiere, eliminar +.I cadena +de su comienzo. +Esta característica ayuda en el caso de los dominios virtuales de +qmail y Postfix; véase más arriba. +.\" --------------------------------------------------------------- +.TP +.BI --domain= nombre.dominio +Antes de analizar la dirección del destinatario para ver a qué lista se +refiere, sustituir la parte del dominio por +.IR nombre. dominio . +Esta característica es útil en el caso de los dominios virtuales de +Postfix. +.\" --------------------------------------------------------------- +.TP +.BI --is-list +¿Se refiere la lista especificada en la opción +.B --name +a una lista válida? +Devuelve un estado de salida de cero (éxito) si es válida, o un estado +de uno (fallo) si no es válida. +.\" --------------------------------------------------------------- +.TP +.BI --sendmail= ruta-hasta-el-programa +Utilice +.I ruta-hasta-el-programa +en lugar de +.B /usr/sbin/sendmail +para enviar correo por medio de una interfaz de línea de órdenes. +Nótese que la orden alternativa debe seguir las convenciones de la +interfaz de línea de órdenes sendmail. +.\" --------------------------------------------------------------- +.TP +.BI --smtp-server= nombre.de.servidor +Enviar el correo usando el servidor SMTP +.I nombre.de.servidor +(puerto 25). +El server ha de estar configurado para permitir que la lista +pueda efectuar la retransmisión de correo a través de él. +Nótese que la opción por defecto es usar la interfaz de línea +de órdenes. Esta opción de enviar por SMTP sólo se utilizará +si la especifica explícitamente. +.\" --------------------------------------------------------------- +.TP +.BI --qmqp-server= nombredemaquina +Enviar correo usando el servidor QMQP que hay en +.I nombredemaquina +(puerto 628). +El servidor debe estar configurado para permitir que la máquina de +la lista reenvíe correo a través suyo. +Tenga en cuenta que por defecto se usa una interfaz de línea de +órdenes para el envío; sólo se utilizará QMQP si especifica esta opción. +.\" --------------------------------------------------------------- +.TP +.BI --moderate +Forzar la moderación de mensajes para un mensaje dado, incluso si va a ir +a parar a una lista de envío libre de mensajes. +Puede usar esta opción para el filtrado de correo electrónico no +solicitado (spam): +sus mensajes entrantes pasan por el filtro de spam que Usted especifique +y si el mensaje califica como spam, se solicita la moderación del mensaje +por parte de una persona. +.\" --------------------------------------------------------------- +.TP +.BI --post +Forzar el envío de un mensaje entrante a una lista dada, incluso si +va a ir a parar a una lista que tenga el envío moderado. +Puede usar esta opción cuando hay una comprobación externa de si +un correo es aceptable en una lista; por ejempo, si dispone de +un comprobador de firmas digitales. +.\" --------------------------------------------------------------- +.TP +.BI --quiet +De forma predeterminada, los mensajes de registro de depuración se envían +al flujo de salida de error estándar. +Con esta opción, se anula dicho comportamiento. +.\" --------------------------------------------------------------- +.TP +.BI --sender= foo@example.com +.TP +.BI --recipient= foo@example.com +Estas dos opciones se usan junto a +.B --incoming +y +.B --is-list +para imponerse a las variables de entorno +.B SENDER +y +.BR RECIPIENT , +respectivamente. +.\" --------------------------------------------------------------- +.TP +.BI --get +Obtiene los valores de una o más variables de configuración. +El nombre de las variables se da en la línea de órdenes tras las opciones. +Cada valor se imprime en una línea aparte. +.\" --------------------------------------------------------------- +.TP +.BI --set +Establece los valores de una o más variables de configuración. +Los nombres y valores se dan en la línea de órdenes tras las opciones +y separadas por signos 'igual' ("="). +Por ejemplo, lo siguiente establecería el finlandés como idioma de una +lista: +.B "enemies-of-carlotta --name=foo@bar --set language=fi" +.\" --------------------------------------------------------------- +.TP +.BI --version +Muestra la versión del programa. +.\" --------------------------------------------------------------- +.SH CONFIGURACIÓN +Cada lista está representada por un directorio, que recibe el nombre +de la lista, y que está dentro de +.IR ~/.enemies-of-carlotta . +Dicho directorio contiene varios ficheros y directorios, que se describen +más abajo. En general, no es necesario tocarlos para nada. +Sin embargo, determinadas configuraciones, un tanto esotéricas, sólo pueden +establecerse editando a mano el fichero de configuración de la lista. +.TP +.B config +El fichero de configuración de la lista. +Su contenido se describe más abajo. +.TP +.B subscribers +Base de datos de suscriptores. +Cada línea contiene un grupo de suscriptores, siendo los cinco +primeros campos delimitados por espacios el identificador del grupo, +el estado la marca temporal de cuándo se creó el grupo, la +marca temporal de cuándo cambió su estado de 'ok' a 'bounced' +(rebotado), y el identificador de la devolución (bounce). +.TP +.B archive-box +Mensajes de la lista archivados. +.TP +.B bounce-box +Grupos de mensajes rebotados (bounce) y que no están en estado 'ok'. +.TP +.B headers-to-add +Cabeceras a añadir a los mensajes enviados a esta lista. +Se copian al principio de cualquier cabecera existente exactamente tal +como estén en el fichero, tras haber añadido las cabeceras de la lista +("List-ID", etc) y eliminado las mencionadas en +.B headers-to-remove . +.TP +.B headers-to-remove +Estas cabeceras se eliminan de los mensajes enviados a la lista. +.TP +.B moderation-box +Mensajes en espera de aprobación por parte del moderador. +.TP +.B subscription-box +Solicitudes de suscripción y desuscripción en espera de confirmación +por parte del usuario. +.TP +.B templates +Directorio que contiene plantillas (opcionales) específicas a la lista. +Si existe este directorio, se buscan las plantillas allí antes de ir en +busca de plantillas globales. Un fichero vacío indica que el mensaje +correspondiente no será enviado. Esto puede usarse, por ejemplo, para +desactivar los mensajes «espere por la moderación» en determinadas +listas. +.TP +.B plugins +Directorio que contiene plugins. Son archivos fuente en Python que +carga EoC automáticamente al arrancar. +Los plugins pueden variar la manera en que opera EoC. +.PP +El fichero +.B config +tiene un formato +.IR palabra_clave = valor +: +.PP +.RS +.nf +[list] +owners = liw@liw.iki.fi +archived = no +posting = free +subscription = free +mail-on-subscription-changes = yes +mail-on-forced-unsubscribe = yes +language = es +.fi +.RE +.PP +Las palabras clave +.BR archived , +.BR posting , +y +.B subscription +corresponden a las opciones de su mismo nombre. +Otras palabras clave son: +.TP +.B owners +Lista de las direcciones de los propietarios. +Especifíquelas con la opción +.I --owner . +.TP +.B mail-on-subscription-changes +Especifica si hay que mandar un correo a los propietarios +de la lista cada vez que un usuario se suscribe o se desuscribe. +.TP +.B mail-on-forced-unsubscribe +Especifica si hay que mandar un correo a los propietarios de la lista +cada vez que un usuario es dado de baja por excesivo rebote de mensajes. +.TP +.B language +Sufijo para las plantillas, para permitir el suporte de múltiples +lenguas. +(Si +.I language +tiene el valor "es", entonces a la plantilla llamada "aficionados" se la busca +en primer lugar como "aficionados.es".) +.\" --------------------------------------------------------------- +.SH EJEMPLOS +Para crear una lista llamada +.IR cinefilos@example.com , +cuyo propietario sea +.IR dingo@example.com , +utilice la siguiente orden (todo en una línea): +.sp 1 +.nf +.RS +enemies-of-carlotta --name=cinefilos@example.com +--owner=dingo@example.com --create +.RE +.PP +Observe que debe configurar su servidor de correo en concreto +para que el correo llegue a la lista. +Para qmail y postfix, véase infra. +.PP +To see the subscribers on that list: +.sp 1 +.RS +enemies-of-carlotta --name=cinefilos@example.com --list +.RE +.PP +Quien quiera suscribirse a la lista ha de escribir un correo a: +.sp 1 +.RS +cinefilos-subscribe@example.com +.RE +.SH QMAIL +Con qmail, para conseguir que el correo entrante sea procesado por +Enemies of Carlotta, tiene que crear dos ficheros +.I .qmail-extension +por cada lista. +Por ejemplo, si su nombre de usuario es pepe y quiere ejecutar la +lista pepe-aficionados, ha de crear dos ficheros, +.I .qmail-aficionados +y +.IR .qmail-aficionados-default , +que contengan la línea +.sp 1 +.RS +|enemies-of-carlotta --incoming +.RE +.PP +Si tiene configurado un dominio virtual, example.com, y los correos +se entregan vía +.I /var/qmail/control/virtualdomains a pepe-exampledotcom , +los ficheros se llamarían +.I .qmail-exampledotcom-aficionados +y +.I .qmail-exampledotcom-aficionados-default +y contendrían +.sp 1 +.RS +|enemies-of-carlotta --incoming --skip-prefix=pepe-exampledotcom- +.RE +.sp 1 +(todo en una línea, claro, por si acaso su lector de páginas de manual +formatea la orden anterior en varias líneas). +.SH POSTFIX +Con postfix, ha de configurar un fichero +.I .forward +que contenga +.sp 1 +.RS +"|procmail -p" +.RE +.sp 1 +y además un fichero +.I .procmailrc +que contenga +.sp 1 +.RS +:0 +.br +* ? enemies-of-carlotta --name=$RECIPIENT --is-list +.br +| enemies-of-carlotta --incoming +.RE +.PP +Para usar Enemies of Carlotta con un dominio virtual de Postfix, +ha de configurar un +.I "mapa virtual de expresiones regulares", +que típicamente está en +.I /etc/postfix/virtual_regexp +(añada +.I "virtual_maps = regexp:/etc/postfix/virtual_regexp" +a su fichero +.I /etc/postfix/main.cf +para activar esta carcterística). +El fichero de expresiones regulares ha de hacer cosas extrañas para +conservar la dirección del destinatario. +Añada lo siguiente al fichero de expresiones regulares: +.sp 1 +.RS +/^su\.dominio\.virtual$/ dummy +.br +/^(sulista|sulista-.*)@(su\.dominio\.virtual)$/ pepe-virtual-$1 +.RE +.sp 1 +(Lo anterior estaba en dos líneas). +Luego, en su fichero +.I .procmailrc +añada la opción +.I "--skip-prefix=joe-virtual-" +y también +.I --domain=your.virtual.domain +para las dos llamadas a +.BR enemies-of-carlotta . +.PP +(Sí, nosotros también pensamos que estas configuraciones son demasiado complicadas). +.\" --------------------------------------------------------------- +.\" --------------------------------------------------------------- +.\" --------------------------------------------------------------- +.SH "ÓRDENES PARA EL CORREO" +Los usuarios y los propietarios de las listas utilizan +Enemies of Carlotta a través del correo electrónico, usando para ello +direcciones a modo de órdenes, como por ejemplo +.BR aficionados-subscribe@example.com . +He aquí una lista de todas las órdenes que pueden usar tanto usuarios +como propietarios de listas de correo. +En todos estos ejemplos, el nombre de la lista de correo es +.BR aficionados@example.com . +.\" --------------------------------------------------------------- +.SS "Órdenes a través de correo que pueden usar todos" +Estas órdenes están pensadas para el uso general. +No precisan de ningún privilegio especial.. +.\" --------------------------------------------------------------- +.TP +.BR aficionados@example.com +Enviar correo a todos los suscritos a la lista. +El mensaje pueden haberlo aprobado previamente de forma manual los administradores +de la lista, que están facultados para rechazar los mensajes. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-owner@example.com +Enviar un correo al propietario o propietarios de la lista. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-help@example.com +Enviar un correo a esta dirección hace que el gestor de listas de +correo nos devuelva un correo con la ayuda existente sobre la +lista en cuestión. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-subscribe@example.com +Envíe un mensaje a esta dirección para suscribirse a la lista. +El gestor de listas de correo le responderá con una confirmación +de suscripción. +No se le suscribirá a la lista a menos que responda a la petición +de confirmación. +De esta forma, un usuario malicioso no podrá poner su dirección +en una o en muchas listas de correo. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-subscribe-pepe=example.com@example.com +Esta es una manera alternativa de la dirección de suscripción. +Si desea suscribirse a la lista de correo con una dirección distinta +a aquella desde la que envía el mensaje, utilice esta modalidad. +En este caso, la dirección para suscribirse es pepe@example.com. +Nótese que la petición de confirmación se envía a Pepe, puesto +que es su dirección la que va a añadirse a la lista. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-unsubscribe@example.com +Para desuscribirse de una lista, envíe un correo a esta dirección +desde la dirección que desea desuscribir de la lista. +De nuevo recibirá una petición de confirmación, para evitar que +un usuario malicioso le desuscriba de una lista de correo contra su +voluntad. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-unsubscribe-pepe=example.com@example.com +Para desuscribir a Pepe, use esta dirección. +De nuevo, es Pepe quien recibirá la petición de confirmación. +.\" --------------------------------------------------------------- +.SS "Órdenes a través de correo que pueden usar los propietarios de las listas" +Se trata de órdenes que pueden usar los propietarios de listas para administrar su lista. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-subscribe-pepe=example.com@example.com +Si un propietario de una lista envía un correo a la dirección anterior, +él recibirá la petición de confirmación, y no Pepe. +Generalmente es mejor que los usuarios se suscriban ellos mismos, pero +a veces los propietarios de listas pueden desear esta característica, +cuando tienen permiso de la persona afectada y quieren resultar más +útiles. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-unsubscribe-joe=example.com@example.com +Los propietarios de listas también pueden desuscribir a otros usuarios. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-list@example.com +Para ver quién está en la lista, envíe un correo a esta dirección. +Sólo funciona si la dirección del remitente del correo coincide +con un propietario de la lista. La dirección "sender address" se usa +a nivel del protocolo SMTP, y no es la del encabezamiento "From:" +.\" --------------------------------------------------------------- +.TP +.BR aficionados-setlist@example.com +Esta orden permite al propietario de una lista especificar de una +sola vez toda la lista de suscriptores. Es equivalente a utilizar +muchas órdenes -subscribe y -unsubscribe, sólo que menos tedioso. +Todo el que resulte añadido a la lista recibe un mensaje de bienvenida, +y todo el que quede eliminado de la lista recibe un mensaje de despedida. +.\" --------------------------------------------------------------- +.TP +.BR aficionados-setlistsilently@example.com +Semejante a -setlist, pero no se envían mensajes ni de bienvenida, ni +de despedida. +.\" --------------------------------------------------------------- +.\" --------------------------------------------------------------- +.\" --------------------------------------------------------------- +.SH PLUGINS +Enemies of Carlotta admite plugins. +Si no sabe programar en Python, probablemente se puede saltar esta +sección. +.PP +Un plugin es un módulo de Python (fichero con un sufijo +.B .py +en el nombre), situado en el directorio +.B ~/.enemies-of-carlotta/plugins . +Los plugins se cargan automáticamente durante el arranque, si la versión +declarada de su interfaz se ajusta con la implementada por Enemies of +Carlotta. La versión de la interfaz se declara en la variable global del +módulo +.BR PLUGIN_INTERFACE_VERSION . +.PP +Los plugin pueden definir funciones que serán invocadas desde los lugares +apropiados del código EoC. +Por el momento, la única función de enganche (hook) disponible es +.BR send_mail_to_subscribers_hook , +que puede manipular un mensaje antes de que sea enviado a los suscriptores. +La función debe parecerse a esto: +.PP +.ti +5 +def send_mail_to_subscribers_hook(list, text): +.PP +El argumento +.I list +es una referencia al objeto +.I MailingList +que corresponde a la lista en cuestión, y +.I text +es el texto completo del mensaje de correo en su forma actual. +La función debe devolver el nuevo contenido del mensaje de correo. +.\" --------------------------------------------------------------- +.\" --------------------------------------------------------------- +.\" --------------------------------------------------------------- +.SH FICHEROS +.TP +.I ~/.enemies-of-carlotta +Aquí están todos los ficheros relacionados con sus listas de correo. +.TP +.I ~/.enemies-of-carlotta/secret +Contraseña secreta que se usa para generar direcciones firmadas +para comprobación de rebotes de correo y verificación de suscripción. +.TP +.I ~/.enemies-of-carlotta/aficionados@example.com +Directorio que contiene los datos relativos a la lista +aficionados@example.com. Excepto el fichero +.I config +de este directorio, no debe editar a mano nada de lo contenido en él. +.TP +.I ~/.enemies-of-carlotta/aficionados@example.com/config +Fichero de configuración de la lista de correo. +Quizá tenga que editar este fichero a mano si desea cambiar el estado +de moderación de la lista o sus propietarios. +.SH "VÉASE TAMBIÉN" +Visite la página de +.I "Enemies of Carlotta" +alojada en +.IR http://www.iki.fi/liw/eoc/ . +.TP +La traducción de esta página ha corrido a cargo de Iván Juanes +.BR kerberos@gulic.org +y de Ricardo Cárdenes +.BR heimy@gulic.org +como parte de los proyectos del grupo Gulic. diff --git a/enemies-of-carlotta.1.fr b/enemies-of-carlotta.1.fr new file mode 100644 index 0000000..4797929 --- /dev/null +++ b/enemies-of-carlotta.1.fr @@ -0,0 +1,421 @@ +.\" Relecture Gérard Delafond +.TH ENEMIES-OF-CARLOTTA 1 +.SH NOM +enemies-of-carlotta \- un gestionnaire de listes de diffusion simple +.SH SYNOPSIS +.B enemies-of-carlotta +.IR "" [ options "] [" adresses ] +.SH "DESCRIPTION" +.B enemies-of-carlotta +est un gestionnaire de listes de diffusion simple. +Si vous ne savez pas ce qu'est un gestionnaire de listes de diffusion, vous +devez commencez par apprendre de quoi il s'agit avant de vouloir vous en +servir. +.PP +Enemies of Carlotta conserve toutes les données des listes dans le répertoire +.I ~/.enemies-of-carlotta. +Il sera créé automatiquement lorsque vous créerez votre première liste. +Vous devez vous arranger manuellement pour que les courriels soient traités +par le gestionnaire de liste de diffusion. +Les détails peuvent varier d'un système à l'autre. +Pour QMail et Postfix, regardez ci-dessous. +.PP +Chaque liste possède un ou plusieurs propriétaires, qui modèrent également +les inscriptions, ou bien certains messages, voire tous. +Sur les listes non modérées, les propriétaires de la liste sont chargés de +répondre aux questions au sujet de la liste. +Sur les liste totalement modérées, ils doivent approuver chaque message +avant qu'il ne soit envoyé sur la liste. +Sur les liste où +.IR posting=auto , +les messages des abonnés sont automatiquement envoyés à la liste, et les +modérateurs n'ont pas besoin d'approuver que les messages qui ne +proviennent pas des abonnés. +.SH OPTIONS +.\" --------------------------------------------------------------- +.TP +.BR --name= foo@example.com +Précise la liste sur laquelle la commande doit être effectuée. +La plupart des options restantes nécessitent que vous précisiez +le nom de la liste avec cette option. +Avec les options --edit, --subscribe, --unsubscribe, et --list, +le nom peut être abrégé en enlevant le signe @ et le domaine. +.\" --------------------------------------------------------------- +.TP +.BI --create +Créer une nouvelle liste de diffusion +Vous devez précisez au moins un propriétaire à l'aide de +.BR --owner . +.\" --------------------------------------------------------------- +.TP +.BI --owner= adresse +Indique la liste des propriétaires lorsque la liste est créée. +.\" --------------------------------------------------------------- +.TP +.B --cleaning-woman +Règle les problèmes des messages qui n'ont pas pu être délivrés et +nettoie également quelques autres éléments. +Vous devez lancer +.B "enemies-of-carlotta --cleaning-woman" +périodiquement. +Cela nettoiera l'ensemble de vos listes de diffusion. +.\" --------------------------------------------------------------- +.TP +.BI --destroy +Détruit la liste. +.\" --------------------------------------------------------------- +.TP +.BI --edit +Modifie la configuration de la liste. +.\" --------------------------------------------------------------- +.TP +.BI --subscription= type +Lors de la création d'une liste, définit le mode d'abonnement à +.I free +(libre) ou +.IR moderated +(modéré). +Utilisez-le avec +.BR --edit , +ou +.BR --create . +.\" --------------------------------------------------------------- +.TP +.BI --posting= type +Lors de la création d'une liste, définit le mode d'envoi à +.IR free , +(libre) +.IR auto , +(automatique) +ou +.IR moderated +(modéré). +Utilisez-le avec +.BR --edit , +ou +.BR --create . +.\" --------------------------------------------------------------- +.TP +.BI --archived= yes-ou-no +Les messages à destination de la liste doivent-ils être archivés sous +la forme d'une +.B archive-box +ou d'un répertoire dans le répertoire dans liste +.B "~/.enemies-of-carlotta" +. +Utilisez +.I yes +(oui) ou +.IR no +(non). +.\" --------------------------------------------------------------- +.TP +.BI --list +Liste l'ensemble des abonnés à une liste de diffusion. +.\" --------------------------------------------------------------- +.TP +.BI --subscribe +Ajoute des abonnés à la liste. +Les arguments qui ne sont pas des options sont les adresses qui doivent +être abonnées. Notez que les adresses ajoutées de cette manière ne +recevront pas de demande de confirmation d'abonnement. +.\" --------------------------------------------------------------- +.TP +.BI --unsubscribe +Enlève un abonné de la liste. +Les arguments qui ne sont pas des options sont les adresses qui doivent +être désabonnées. Notez que les adresses enlevées de cette manière ne +recevront pas de demande de confirmation de désabonnement. +.\" --------------------------------------------------------------- +.TP +.B --incoming +Traite un message entrant sur l'entrée standard. +L'enveloppe SMTP qui précise l'adresse de l'expéditeur doit être +précisée par la variable d'environnement +.I SENDER +et l'enveloppe SMTP qui précise l'adresse du destinataire doit être +précisée par la variable d'environnement +.I RECIPIENT +(QMail et Postfix traitent cela automatiquement.) +.\" --------------------------------------------------------------- +.TP +.BI --skip-prefix= chaîne +Avant d'analyser l'adresse de destination pour regarder à quelle liste +le message est destiné, ce paramètre permet d'enlever +.I chaîne +depuis son début. +C'est utilisé pour traiter les domaines virtuels dans QMail et Postfix, +voyez ci-dessous. +.\" --------------------------------------------------------------- +.TP +.BI --domain= nom.domaine +Avant d'analyser l'adresse de destination pour voir à quelle liste le message +est destiné, remplace le nom de domaine par +.IR nom.domaine . +C'est utilisé pour traiter les domaines virtuels dans QMail et Postfix. +.\" --------------------------------------------------------------- +.TP +.BI --is-list +L'adresse précisée par +.B --name +fait-elle référence à un nom de liste valide\ ? +Cela retourne le code d'erreur zéro (succès) si c'est le cas, ou un (échec) +dans le cas contraire. +.\" --------------------------------------------------------------- +.TP +.BI --sendmail= chemin +Utilise +.I chemin +au lieu de +.B /usr/sbin/sendmail +Pour envoyer des courriels via une interface en ligne de commande. +Notez que la commande doit respecter l'interface de la ligne de commande +sendmail. +.\" --------------------------------------------------------------- +.TP +.BI --smtp-server= hôte +Envoi les courriels en utilisant le serveur +.I hôte +(port 25). +Le serveur doit être configuré pour permettre à la machine sur laquelle +fonctionne la liste de l'utiliser pour relais. +Notez que par défaut, c'est l'interface en ligne de commande qui est utilisée. +L'envoi au moyen de SMTP n'est utilisé que si vous utilisez cette option. +.\" --------------------------------------------------------------- +.TP +.BI --moderate +Force un message entrant à être modéré, même s'il est envoyé à une liste +où l'envoi est libre. +Cela peut être utilisé pour filtrer le spam\ : +Vos messages entrants peuvent être filtrés par n'importe quel système de +filtrage des courriels, dès lors qu'un message semble être du spam, +vous pouvez demander qu'il soit modéré par une personne humaine. +.\" --------------------------------------------------------------- +.TP +.BI --quiet +Par défaut, les messages de débogage des journaux sont envoyés sur +l'erreur standard. +Avec cette option, il ne le sont plus. +.\" --------------------------------------------------------------- +.SH CONFIGURATION +Chaque liste est représentée par un répertoire, nommé d'après le nom de +la liste, sous le répertoire +.IR ~/.enemies-of-carlotta . +Ce répertoire contient plusieurs fichiers et répertoires, qui sont décrits +ci-dessous. +En général, il n'est pas nécessaire de toucher à ces répertoires. +Cependant, certaines configurations ésotériques peuvent uniquement être +faites en éditant le fichier de configuration de la liste. +.TP +.B config +Le fichier de configuration de la liste. +Le contenu est décrit ci-dessous. +.TP +.B subscribers +La base de données des abonnés. +Chaque ligne contient un groupe d'abonné, dont les cinq premiers +espaces délimitent les champs qui sont les identifiants des groupes, +le statut, le timestamp de la date de création du groupe, le timestamp +pour les retours de courriel en cas d'échec, il peut varier de «\ ok\ » +à «\ bonced\ », et l'identifiant du message de retour en cas d'échec. +.TP +.B archive-box +Les messages archivés. +.TP +.B bounce-box +Groupes de messages qui n'ont pu être délivrés et qui sont dans le +statut «\ ok\ ». +.TP +.B moderation-box +Messages en attente d'approbation du modérateur. +.TP +.B subscription-box +Requêtes d'abonnement de désabonnement en attente de confirmation de la +part de l'utilisateur. +.PP +le fichier +.B config +possède des +.IR mot-clé = value +format\ : +.PP +.RS +.nf +[list] +owners = liw@liw.iki.fi +archived = no +posting = free +subscription = free +mail-on-subscription-changes = yes +mail-on-forced-unsubscribe = yes +language = fi +.fi +.RE +.PP +Les mots clés +.BR archived , +.BR posting , +et +.B subscription +correspondent aux options qui portent les mêmes noms. +Les autres mots-clés sont\ : +.TP +.B owners +Liste les adresses des propriétaires. Définissez-la à l'aide de +l'option +.I --owner. +.TP +.B mail-on-subscription-changes +Les propriétaires doivent-ils recevoir un avertissement lorsqu'un utilisateur +s'abonne ou se désabonne\ ? +.TP +.B mail-on-forced-unsubscribe +Les propriétaires doivent-ils recevoir un avertissement lorsqu'une personne +est enlevée de la liste car trop de courriels ne lui sont pas parvenus\ ? +.TP +.B language +Suffixe pour les templates, pour permettre le support de plusieurs +langues +(Si +.I language +est défini à «\ fr\ », alors la template nommée «\ foo\ » est d'abord +recherchée comme «\ foo.fr\ ».) +.\" --------------------------------------------------------------- +.SH EXEMPLES +Pour créer une liste nommée +.IR moviefans@example.com , +dont le propriétaire est +.IR ding@example.com , +utilisez la commande suivante (tout sur la même ligne)\ : +.sp 1 +.RS +enemies-of-carlotta --name=ding@example.com --create +.RE +.PP +Pour voir la liste de tous les abonnés à cette liste\ : +.sp 1 +.RS +enemies-of-carlotta --name=moviefans@example.com --list +.RE +.PP +Les personnes qui souhaitent être abonnées à la liste doivent +envoyer un courriel à +.sp 1 +.RS +moviefans-subscribe@example.com +.RE +.SH QMAIL +Dans Qmail, pour faire en sorte que le courrier entrant soit traité +par Enemies of Carlotta, vous avez besoin de créer quelques fichiers +.I .qmail-extension +par liste. +Par exemple, si votre nom d'utilisateur est joe et que vous souhaitez +utiliser la liste de diffusion joe-fans, vous devrez créer les +fichiers +.I .qmail-fans +et +.IR .qmail-fans-default , +qui contiennent +.sp 1 +.RS +|"enemies-of-carlotta --incoming" +.RE +.PP +Si vous utilisez un domaine virtuel, example.com, et que les +courriels sont délivrés via +.I /var/qmail/control/virtualdomains à joe-exampledotcom, +les fichiers seront nommés +.I .qmail-exampledotcom-fans +et +.I .qmail-exampledotcom-fans-default +et ils contiendront +.sp 1 +.RS +|"enemies-of-carlotta --incoming +.br +--skip-prefix=joe-exampledotcom-" +.RE +.sp 1 +(l'ensemble sur la même ligne, il va de soi). +.SH POSTFIX +Avec Postfix, vous devrez configurer un fichier +.I .forward +contenant +.sp 1 +.RS +"|procmail -p" +.RE +.sp 1 +et un fichier +.I .procmailrc +contenant +.sp 1 +.RS +:0 +.br +* ? enemies-of-carlotta --name=$RECIPIENT --is-list +.br +| enemies-of-carlotta --incoming +.RE +.PP +Pour utiliser Enemies of Carlotta avec un domaine virtuel Postfix, +vous devrez mettre en place une +.I «\ carte virtuelle d'expressions rationnelles\ », +généralement +.I /etc/postfix/virtual_regexp +(ajoutez +.I "virtual_maps = regexp:/etc/postfix/virtual_regexp" +dans votre fichier +.I /etc/postfix/main.cf +pour l'activer). +Le fichier d'expressions rationnelles a besoin de faire des choses +horribles pour conserver l'adresse de destination. +Ajoutez ce qui suit dans le fichier d'expressions rationnelles\ : +.sp 1 +.RS +/^your\.virtual\.domain$/ dummy +.br +/^(yourlist|yourlist-.*)@(your\.virtual\.domain)$/ joe-virtual-$1 +.RE +.sp 1 +(Ça fait deux lignes.) +Ensuite dans votre fichier, +.I .procmailrc, +ajoutez les options +.I "--skip-prefix=joe-virtual-" +et +.I --domain=your.virtual.domain +pour les deux appels à +.BR enemies-of-carlotta . +.PP +(Oui, je aussi trouve que ces choses là sont trop compliquées) +.SH FICHIERS +.TP +.I ~/.enemies-of-carlotta +L'ensemble des fichiers en rapports avec vos listes de diffusion. +.TP +.I ~/.enemies-of-carlotta/secret +Les mots de passe secrets utilisés pour générer des adresses signées pour +le contrôle des couriels d'échec et la validation de l'abonnement. +.TP +.I ~/.enemies-of-carlotta/foo@example.com +Le répertoire contient les données appartenant à la liste de diffusion +foo@example.com. +À l'exception du fichier de +.I config +qui se trouve dans ce répertoire, vous ne devriez pas éditer autre chose +à la main +.TP +.I ~/.enemies-of-carlotta/foo@example.com/config +Fichier de configuration pour la liste de diffusion. +Vous aurez peut-être besoin de l'éditer à la main si vous souhaitez +apporter des changement en ce qui concerne la modération ou bien les +propriétaires de la liste. +.SH "VOIR AUSSI" +Vous serez peut-être intéressé de visiter +la page d'accueil d' +.I «\ Enemies of Carlotta\ » +à l'adresse +.IR http://www.iki.fi/liw/eoc/ . +.SH "TRADUCTION" +Pierre Machard , 2003 diff --git a/eoc.py b/eoc.py new file mode 100644 index 0000000..b47b147 --- /dev/null +++ b/eoc.py @@ -0,0 +1,1675 @@ +"""Mailing list manager. + +This is a simple mailing list manager that mimicks the ezmlm-idx mail +address commands. See manual page for more information. +""" + +VERSION = "1.1.5" +PLUGIN_INTERFACE_VERSION = "1" + +import getopt +import md5 +import os +import shutil +import smtplib +import string +import sys +import time +import ConfigParser +try: + import email.Header + have_email_module = 1 +except ImportError: + have_email_module = 0 +import imp + +import qmqp + + +# The following values will be overriden by "make install". +TEMPLATE_DIRS = ["./templates"] +DOTDIR = "dot-eoc" + + +class EocException(Exception): + + def __init__(self, arg=None): + self.msg = repr(arg) + + def __str__(self): + return self.msg + +class UnknownList(EocException): + def __init__(self, list_name): + self.msg = "%s is not a known mailing list" % list_name + +class BadCommandAddress(EocException): + def __init__(self, address): + self.msg = "%s is not a valid command address" % address + +class BadSignature(EocException): + def __init__(self, address): + self.msg = "address %s has an invalid digital signature" % address + +class ListExists(EocException): + def __init__(self, list_name): + self.msg = "Mailing list %s alreadys exists" % list_name + +class ListDoesNotExist(EocException): + def __init__(self, list_name): + self.msg = "Mailing list %s does not exist" % list_name + +class MissingEnvironmentVariable(EocException): + def __init__(self, name): + self.msg = "Environment variable %s does not exist" % name + +class MissingTemplate(EocException): + def __init__(self, template): + self.msg = "Template %s does not exit" % template + + +# Names of commands EoC recognizes in e-mail addresses. +SIMPLE_COMMANDS = ["help", "list", "owner", "setlist", "setlistsilently", "ignore"] +SUB_COMMANDS = ["subscribe", "unsubscribe"] +HASH_COMMANDS = ["subyes", "subapprove", "subreject", "unsubyes", + "bounce", "probe", "approve", "reject", "setlistyes", + "setlistsilentyes"] +COMMANDS = SIMPLE_COMMANDS + SUB_COMMANDS + HASH_COMMANDS + + +def md5sum_as_hex(s): + return md5.new(s).hexdigest() + +environ = None + +def set_environ(new_environ): + global environ + environ = new_environ + +def get_from_environ(key): + global environ + if environ: + env = environ + else: + env = os.environ + if env.has_key(key): + return env[key].lower() + raise MissingEnvironmentVariable(key) + +class AddressParser: + + """A parser for incoming e-mail addresses.""" + + def __init__(self, lists): + self.set_lists(lists) + self.set_skip_prefix(None) + self.set_forced_domain(None) + + def set_lists(self, lists): + """Set the list of canonical list names we should know about.""" + self.lists = lists + + def set_skip_prefix(self, skip_prefix): + """Set the prefix to be removed from an address.""" + self.skip_prefix = skip_prefix + + def set_forced_domain(self, forced_domain): + """Set the domain part we should force the address to have.""" + self.forced_domain = forced_domain + + def clean(self, address): + """Remove cruft from the address and convert the rest to lower case.""" + if self.skip_prefix: + n = self.skip_prefix and len(self.skip_prefix) + if address[:n] == self.skip_prefix: + address = address[n:] + if self.forced_domain: + parts = address.split("@", 1) + address = "%s@%s" % (parts[0], self.forced_domain) + return address.lower() + + def split_address(self, address): + """Split an address to a local part and a domain.""" + parts = address.lower().split("@", 1) + if len(parts) != 2: + return (address, "") + else: + return parts + + # Does an address refer to a list? If not, return None, else return a list + # of additional parts (separated by hyphens) in the address. Note that [] + # is not the same as None. + + def additional_address_parts(self, address, listname): + addr_local, addr_domain = self.split_address(address) + list_local, list_domain = self.split_address(listname) + + if addr_domain != list_domain: + return None + + if addr_local.lower() == list_local.lower(): + return [] + + n = len(list_local) + if addr_local[:n] != list_local or addr_local[n] != "-": + return None + + return addr_local[n+1:].split("-") + + + # Parse an address we have received that identifies a list we manage. + # The address may contain command and signature parts. Return the name + # of the list, and a sequence of the additional parts (split at hyphens). + # Raise exceptions for errors. Note that the command will be valid, but + # cryptographic signatures in the address is not checked. + + def parse(self, address): + address = self.clean(address) + for listname in self.lists: + parts = self.additional_address_parts(address, listname) + if parts == None: + pass + elif parts == []: + return listname, parts + elif parts[0] in HASH_COMMANDS: + if len(parts) != 3: + raise BadCommandAddress(address) + return listname, parts + elif parts[0] in COMMANDS: + return listname, parts + + raise UnknownList(address) + + +class MailingListManager: + + def __init__(self, dotdir, sendmail="/usr/sbin/sendmail", lists=[], + smtp_server=None, qmqp_server=None): + self.dotdir = dotdir + self.sendmail = sendmail + self.smtp_server = smtp_server + self.qmqp_server = qmqp_server + + self.make_dotdir() + self.secret = self.make_and_read_secret() + + if not lists: + lists = filter(lambda s: "@" in s, os.listdir(dotdir)) + self.set_lists(lists) + + self.simple_commands = ["help", "list", "owner", "setlist", + "setlistsilently", "ignore"] + self.sub_commands = ["subscribe", "unsubscribe"] + self.hash_commands = ["subyes", "subapprove", "subreject", "unsubyes", + "bounce", "probe", "approve", "reject", + "setlistyes", "setlistsilentyes"] + self.commands = self.simple_commands + self.sub_commands + \ + self.hash_commands + + self.environ = None + + self.load_plugins() + + # Create the dot directory for us, if it doesn't exist already. + def make_dotdir(self): + if not os.path.isdir(self.dotdir): + os.makedirs(self.dotdir, 0700) + + # Create the "secret" file, with a random value used as cookie for + # verification addresses. + def make_and_read_secret(self): + secret_name = os.path.join(self.dotdir, "secret") + if not os.path.isfile(secret_name): + f = open("/dev/urandom", "r") + secret = f.read(32) + f.close() + f = open(secret_name, "w") + f.write(secret) + f.close() + else: + f = open(secret_name, "r") + secret = f.read() + f.close() + return secret + + # Load the plugins from DOTDIR/plugins/*.py. + def load_plugins(self): + self.plugins = [] + + dirname = os.path.join(DOTDIR, "plugins") + try: + plugins = os.listdir(dirname) + except OSError: + return + + plugins.sort() + plugins = map(os.path.splitext, plugins) + plugins = filter(lambda p: p[1] == ".py", plugins) + plugins = map(lambda p: p[0], plugins) + for name in plugins: + pathname = os.path.join(dirname, name + ".py") + f = open(pathname, "r") + module = imp.load_module(name, f, pathname, + (".py", "r", imp.PY_SOURCE)) + f.close() + if module.PLUGIN_INTERFACE_VERSION == PLUGIN_INTERFACE_VERSION: + self.plugins.append(module) + + # Call function named funcname (a string) in all plugins, giving as + # arguments all the remaining arguments preceded by ml. Return value + # of each function is the new list of arguments to the next function. + # Return value of this function is the return value of the last function. + def call_plugins(self, funcname, list, *args): + for plugin in self.plugins: + if plugin.__dict__.has_key(funcname): + args = apply(plugin.__dict__[funcname], (list,) + args) + if type(args) != type((0,)): + args = (args,) + return args + + # Set the list of listnames. The list of lists needs to be sorted in + # length order so that test@example.com is matched before + # test-list@example.com + def set_lists(self, lists): + temp = map(lambda s: (len(s), s), lists) + temp.sort() + self.lists = map(lambda t: t[1], temp) + + # Return the list of listnames. + def get_lists(self): + return self.lists + + # Decode an address that has been encoded to be part of a local part. + def decode_address(self, parts): + return string.join(string.join(parts, "-").split("="), "@") + + # Is local_part@domain an existing list? + def is_list_name(self, local_part, domain): + return ("%s@%s" % (local_part, domain)) in self.lists + + # Compute the verification checksum for an address. + def compute_hash(self, address): + return md5sum_as_hex(address + self.secret) + + # Is the verification signature in a parsed address bad? If so, return true, + # otherwise return false. + def signature_is_bad(self, dict, hash): + local_part, domain = dict["name"].split("@") + address = "%s-%s-%s@%s" % (local_part, dict["command"], dict["id"], + domain) + correct = self.compute_hash(address) + return correct != hash + + # Parse a command address we have received and check its validity + # (including signature, if any). Return a dictionary with keys + # "command", "sender" (address that was encoded into address, if + # any), "id" (group ID). + + def parse_recipient_address(self, address, skip_prefix, forced_domain): + ap = AddressParser(self.get_lists()) + ap.set_lists(self.get_lists()) + ap.set_skip_prefix(skip_prefix) + ap.set_forced_domain(forced_domain) + listname, parts = ap.parse(address) + + dict = { "name": listname } + + if parts == []: + dict["command"] = "post" + else: + command, args = parts[0], parts[1:] + dict["command"] = command + if command in SUB_COMMANDS: + dict["sender"] = self.decode_address(args) + elif command in HASH_COMMANDS: + dict["id"] = args[0] + hash = args[1] + if self.signature_is_bad(dict, hash): + raise BadSignature(address) + + return dict + + # Does an address refer to a mailing list? + def is_list(self, name, skip_prefix=None, domain=None): + try: + self.parse_recipient_address(name, skip_prefix, domain) + except BadCommandAddress: + return 0 + except BadSignature: + return 0 + except UnknownList: + return 0 + return 1 + + # Create a new list and return it. + def create_list(self, name): + if self.is_list(name): + raise ListExists(name) + self.set_lists(self.lists + [name]) + return MailingList(self, name) + + # Open an existing list. + def open_list(self, name): + if self.is_list(name): + return self.open_list_exact(name) + else: + x = name + "@" + for list in self.lists: + if list[:len(x)] == x: + return self.open_list_exact(list) + raise ListDoesNotExist(name) + + def open_list_exact(self, name): + for list in self.get_lists(): + if list.lower() == name.lower(): + return MailingList(self, list) + raise ListDoesNotExist(name) + + # Process an incoming message. + def incoming_message(self, skip_prefix, domain, moderate, post): + debug("Processing incoming message.") + debug("$SENDER = <%s>" % get_from_environ("SENDER")) + debug("$RECIPIENT = <%s>" % get_from_environ("RECIPIENT")) + dict = self.parse_recipient_address(get_from_environ("RECIPIENT"), + skip_prefix, + domain) + dict["force-moderation"] = moderate + dict["force-posting"] = post + debug("List is <%(name)s>, command is <%(command)s>." % dict) + list = self.open_list_exact(dict["name"]) + list.obey(dict) + + # Clean up bouncing address and do other janitorial work for all lists. + def cleaning_woman(self, send_mail=None): + now = time.time() + for listname in self.lists: + list = self.open_list_exact(listname) + if send_mail: + list.send_mail = send_mail + list.cleaning_woman(now) + + # Send a mail to the desired recipients. + def send_mail(self, envelope_sender, recipients, text): + debug("send_mail:\n sender=%s\n recipients=%s\n text=\n %s" % + (envelope_sender, str(recipients), + "\n ".join(text[:text.find("\n\n")].split("\n")))) + if recipients: + if self.smtp_server: + smtp = smtplib.SMTP(self.smtp_server) + smtp.sendmail(envelope_sender, recipients, text) + smtp.quit() + elif self.qmqp_server: + q = qmqp.QMQP(self.qmqp_server) + q.sendmail(envelope_sender, recipients, text) + q.quit() + else: + recipients = string.join(recipients, " ") + f = os.popen("%s -oi -f '%s' %s" % + (self.sendmail, + envelope_sender, + recipients), + "w") + f.write(text) + f.close() + else: + debug("send_mail: no recipients, not sending") + + + +class MailingList: + + posting_opts = ["auto", "free", "moderated"] + + def __init__(self, mlm, name): + self.mlm = mlm + self.name = name + + self.cp = ConfigParser.ConfigParser() + self.cp.add_section("list") + self.cp.set("list", "owners", "") + self.cp.set("list", "moderators", "") + self.cp.set("list", "subscription", "free") + self.cp.set("list", "posting", "free") + self.cp.set("list", "archived", "no") + self.cp.set("list", "mail-on-subscription-changes", "no") + self.cp.set("list", "mail-on-forced-unsubscribe", "no") + self.cp.set("list", "ignore-bounce", "no") + self.cp.set("list", "language", "") + self.cp.set("list", "pristine-headers", "") + + self.dirname = os.path.join(self.mlm.dotdir, name) + self.make_listdir() + self.cp.read(self.mkname("config")) + + self.subscribers = SubscriberDatabase(self.dirname, "subscribers") + self.moderation_box = MessageBox(self.dirname, "moderation-box") + self.subscription_box = MessageBox(self.dirname, "subscription-box") + self.bounce_box = MessageBox(self.dirname, "bounce-box") + + def make_listdir(self): + if not os.path.isdir(self.dirname): + os.mkdir(self.dirname, 0700) + self.save_config() + f = open(self.mkname("subscribers"), "w") + f.close() + + def mkname(self, relative): + return os.path.join(self.dirname, relative) + + def save_config(self): + f = open(self.mkname("config"), "w") + self.cp.write(f) + f.close() + + def read_stdin(self): + data = sys.stdin.read() + # Skip Unix mbox "From " mail start indicator + if data[:5] == "From ": + data = string.split(data, "\n", 1)[1] + return data + + def invent_boundary(self): + return "%s/%s" % (md5sum_as_hex(str(time.time())), + md5sum_as_hex(self.name)) + + def command_address(self, command): + local_part, domain = self.name.split("@") + return "%s-%s@%s" % (local_part, command, domain) + + def signed_address(self, command, id): + unsigned = self.command_address("%s-%s" % (command, id)) + hash = self.mlm.compute_hash(unsigned) + return self.command_address("%s-%s-%s" % (command, id, hash)) + + def ignore(self): + return self.command_address("ignore") + + def nice_7bit(self, str): + for c in str: + if (ord(c) < 32 and not c.isspace()) or ord(c) >= 127: + return False + return True + + def mime_encode_headers(self, text): + headers, body = text.split("\n\n", 1) + + list = [] + for line in headers.split("\n"): + if line[0].isspace(): + list[-1] += line + else: + list.append(line) + + headers = [] + for header in list: + if self.nice_7bit(header): + headers.append(header) + else: + if ": " in header: + name, content = header.split(": ", 1) + else: + name, content = header.split(":", 1) + hdr = email.Header.Header(content, "utf-8") + headers.append(name + ": " + hdr.encode()) + + return "\n".join(headers) + "\n\n" + body + + def template(self, template_name, dict): + lang = self.cp.get("list", "language") + if lang: + template_name_lang = template_name + "." + lang + else: + template_name_lang = template_name + + if not dict.has_key("list"): + dict["list"] = self.name + dict["local"], dict["domain"] = self.name.split("@") + if not dict.has_key("list"): + dict["list"] = self.name + + for dir in [os.path.join(self.dirname, "templates")] + TEMPLATE_DIRS: + pathname = os.path.join(dir, template_name_lang) + if not os.path.exists(pathname): + pathname = os.path.join(dir, template_name) + if os.path.exists(pathname): + f = open(pathname, "r") + data = f.read() + f.close() + return data % dict + + raise MissingTemplate(template_name) + + def send_template(self, envelope_sender, sender, recipients, + template_name, dict): + dict["From"] = "EoC <%s>" % sender + dict["To"] = string.join(recipients, ", ") + text = self.template(template_name, dict) + if not text: + return + if self.cp.get("list", "pristine-headers") != "yes": + text = self.mime_encode_headers(text) + self.mlm.send_mail(envelope_sender, recipients, text) + + def send_info_message(self, recipients, template_name, dict): + self.send_template(self.command_address("ignore"), + self.command_address("help"), + recipients, + template_name, + dict) + + def owners(self): + return self.cp.get("list", "owners").split() + + def moderators(self): + return self.cp.get("list", "moderators").split() + + def is_list_owner(self, address): + return address in self.owners() + + def obey_help(self): + self.send_info_message([get_from_environ("SENDER")], "help", {}) + + def obey_list(self): + recipient = get_from_environ("SENDER") + if self.is_list_owner(recipient): + addr_list = self.subscribers.get_all() + addr_text = string.join(addr_list, "\n") + self.send_info_message([recipient], "list", + { + "addresses": addr_text, + "count": len(addr_list), + }) + else: + self.send_info_message([recipient], "list-sorry", {}) + + def obey_setlist(self, origmail): + recipient = get_from_environ("SENDER") + if self.is_list_owner(recipient): + id = self.moderation_box.add(recipient, origmail) + if self.parse_setlist_addresses(origmail) == None: + self.send_bad_addresses_in_setlist(id) + self.moderation_box.remove(id) + else: + confirm = self.signed_address("setlistyes", id) + self.send_info_message(self.owners(), "setlist-confirm", + { + "confirm": confirm, + "origmail": origmail, + "boundary": self.invent_boundary(), + }) + + else: + self.send_info_message([recipient], "setlist-sorry", {}) + + def obey_setlistsilently(self, origmail): + recipient = get_from_environ("SENDER") + if self.is_list_owner(recipient): + id = self.moderation_box.add(recipient, origmail) + if self.parse_setlist_addresses(origmail) == None: + self.send_bad_addresses_in_setlist(id) + self.moderation_box.remove(id) + else: + confirm = self.signed_address("setlistsilentyes", id) + self.send_info_message(self.owners(), "setlist-confirm", + { + "confirm": confirm, + "origmail": origmail, + "boundary": self.invent_boundary(), + }) + else: + self.info_message([recipient], "setlist-sorry", {}) + + def parse_setlist_addresses(self, text): + body = text.split("\n\n", 1)[1] + lines = body.split("\n") + lines = filter(lambda line: line != "", lines) + badlines = filter(lambda line: "@" not in line, lines) + if badlines: + return None + else: + return lines + + def send_bad_addresses_in_setlist(self, id): + addr = self.moderation_box.get_address(id) + origmail = self.moderation_box.get(id) + self.send_info_message([addr], "setlist-badlist", + { + "origmail": origmail, + "boundary": self.invent_boundary(), + }) + + + def obey_setlistyes(self, dict): + if self.moderation_box.has(dict["id"]): + text = self.moderation_box.get(dict["id"]) + addresses = self.parse_setlist_addresses(text) + if addresses == None: + self.send_bad_addresses_in_setlist(id) + else: + removed_subscribers = [] + self.subscribers.lock() + old = self.subscribers.get_all() + for address in old: + if address.lower() not in map(string.lower, addresses): + self.subscribers.remove(address) + removed_subscribers.append(address) + else: + for x in addresses: + if x.lower() == address.lower(): + addresses.remove(x) + self.subscribers.add_many(addresses) + self.subscribers.save() + + for recipient in addresses: + self.send_info_message([recipient], "sub-welcome", {}) + for recipient in removed_subscribers: + self.send_info_message([recipient], "unsub-goodbye", {}) + self.send_info_message(self.owners(), "setlist-done", {}) + + self.moderation_box.remove(dict["id"]) + + def obey_setlistsilentyes(self, dict): + if self.moderation_box.has(dict["id"]): + text = self.moderation_box.get(dict["id"]) + addresses = self.parse_setlist_addresses(text) + if addresses == None: + self.send_bad_addresses_in_setlist(id) + else: + self.subscribers.lock() + old = self.subscribers.get_all() + for address in old: + if address not in addresses: + self.subscribers.remove(address) + else: + addresses.remove(address) + self.subscribers.add_many(addresses) + self.subscribers.save() + self.send_info_message(self.owners(), "setlist-done", {}) + + self.moderation_box.remove(dict["id"]) + + def obey_owner(self, text): + sender = get_from_environ("SENDER") + recipients = self.cp.get("list", "owners").split() + self.mlm.send_mail(sender, recipients, text) + + def obey_subscribe_or_unsubscribe(self, dict, template_name, command, + origmail): + + requester = get_from_environ("SENDER") + subscriber = dict["sender"] + if not subscriber: + subscriber = requester + if subscriber.find("@") == -1: + info("Trying to (un)subscribe address without @: %s" % subscriber) + return + if self.cp.get("list", "ignore-bounce") == "yes": + info("Will not (un)subscribe address: %s from static list" %subscriber) + return + if requester in self.owners(): + confirmers = self.owners() + else: + confirmers = [subscriber] + + id = self.subscription_box.add(subscriber, origmail) + confirm = self.signed_address(command, id) + self.send_info_message(confirmers, template_name, + { + "confirm": confirm, + "origmail": origmail, + "boundary": self.invent_boundary(), + }) + + def obey_subscribe(self, dict, origmail): + self.obey_subscribe_or_unsubscribe(dict, "sub-confirm", "subyes", + origmail) + + def obey_unsubscribe(self, dict, origmail): + self.obey_subscribe_or_unsubscribe(dict, "unsub-confirm", "unsubyes", + origmail) + + def obey_subyes(self, dict): + if self.subscription_box.has(dict["id"]): + if self.cp.get("list", "subscription") == "free": + recipient = self.subscription_box.get_address(dict["id"]) + self.subscribers.lock() + self.subscribers.add(recipient) + self.subscribers.save() + sender = self.command_address("help") + self.send_template(self.ignore(), sender, [recipient], + "sub-welcome", {}) + self.subscription_box.remove(dict["id"]) + if self.cp.get("list", "mail-on-subscription-changes")=="yes": + self.send_info_message(self.owners(), + "sub-owner-notification", + { + "address": recipient, + }) + else: + recipients = self.cp.get("list", "owners").split() + confirm = self.signed_address("subapprove", dict["id"]) + deny = self.signed_address("subreject", dict["id"]) + subscriber = self.subscription_box.get_address(dict["id"]) + origmail = self.subscription_box.get(dict["id"]) + self.send_template(self.ignore(), deny, recipients, + "sub-moderate", + { + "confirm": confirm, + "deny": deny, + "subscriber": subscriber, + "origmail": origmail, + "boundary": self.invent_boundary(), + }) + recipient = self.subscription_box.get_address(dict["id"]) + self.send_info_message([recipient], "sub-wait", {}) + + def obey_subapprove(self, dict): + if self.subscription_box.has(dict["id"]): + recipient = self.subscription_box.get_address(dict["id"]) + self.subscribers.lock() + self.subscribers.add(recipient) + self.subscribers.save() + self.send_info_message([recipient], "sub-welcome", {}) + self.subscription_box.remove(dict["id"]) + if self.cp.get("list", "mail-on-subscription-changes")=="yes": + self.send_info_message(self.owners(), "sub-owner-notification", + { + "address": recipient, + }) + + def obey_subreject(self, dict): + if self.subscription_box.has(dict["id"]): + recipient = self.subscription_box.get_address(dict["id"]) + self.send_info_message([recipient], "sub-reject", {}) + self.subscription_box.remove(dict["id"]) + + def obey_unsubyes(self, dict): + if self.subscription_box.has(dict["id"]): + recipient = self.subscription_box.get_address(dict["id"]) + self.subscribers.lock() + self.subscribers.remove(recipient) + self.subscribers.save() + self.send_info_message([recipient], "unsub-goodbye", {}) + self.subscription_box.remove(dict["id"]) + if self.cp.get("list", "mail-on-subscription-changes")=="yes": + self.send_info_message(self.owners(), + "unsub-owner-notification", + { + "address": recipient, + }) + + def store_into_archive(self, text): + if self.cp.get("list", "archived") == "yes": + archdir = os.path.join(self.dirname, "archive") + if not os.path.exists(archdir): + os.mkdir(archdir, 0700) + id = md5sum_as_hex(text) + f = open(os.path.join(archdir, id), "w") + f.write(text) + f.close() + + def list_headers(self): + local, domain = self.name.split("@") + list = [] + list.append("List-Id: <%s.%s>" % (local, domain)) + list.append("List-Help: " % (local, domain)) + list.append("List-Unsubscribe: " % + (local, domain)) + list.append("List-Subscribe: " % + (local, domain)) + list.append("List-Post: " % (local, domain)) + list.append("List-Owner: " % (local, domain)) + list.append("Precedence: bulk"); + return string.join(list, "\n") + "\n" + + def read_file(self, basename): + try: + f = open(os.path.join(self.dirname, basename), "r") + data = f.read() + f.close() + return data + except IOError: + return "" + + def headers_to_add(self): + headers_to_add = self.read_file("headers-to-add").rstrip() + if headers_to_add: + return headers_to_add + "\n" + else: + return "" + + def remove_some_headers(self, mail, headers_to_remove): + endpos = mail.find("\n\n") + if endpos == -1: + endpos = mail.find("\n\r\n") + if endpos == -1: + return mail + headers = mail[:endpos].split("\n") + body = mail[endpos:] + + remaining = [] + add_continuation_lines = 0 + for header in headers: + pos = header.find(":") + if pos == -1: + if add_continuation_lines: + remaining.append(header) + else: + name = header[:pos].lower() + if name in headers_to_remove: + add_continuation_lines = 0 + else: + add_continuation_lines = 1 + remaining.append(header) + + return "\n".join(remaining) + body + + def headers_to_remove(self, text): + headers_to_remove = self.read_file("headers-to-remove").split("\n") + headers_to_remove = map(lambda s: s.strip().lower(), + headers_to_remove) + return self.remove_some_headers(text, headers_to_remove) + + def append_footer(self, text): + if "base64" in text or "BASE64" in text: + import StringIO + for line in StringIO.StringIO(text): + if line.lower.beginswith("content-transfer-encoding:") and \ + "base64" in line.lower(): + return text + return text + self.template("footer", {}) + + def send_mail_to_subscribers(self, text): + text = self.headers_to_add() + self.list_headers() + \ + self.headers_to_remove(text) + text = self.append_footer(text) + text, = self.mlm.call_plugins("send_mail_to_subscribers_hook", + self, text) + if have_email_module and \ + self.cp.get("list", "pristine-headers") != "yes": + text = self.mime_encode_headers(text) + self.store_into_archive(text) + for group in self.subscribers.groups(): + bounce = self.signed_address("bounce", group) + addresses = self.subscribers.in_group(group) + self.mlm.send_mail(bounce, addresses, text) + + def post_into_moderate(self, poster, dict, text): + id = self.moderation_box.add(poster, text) + recipients = self.moderators() + if recipients == []: + recipients = self.owners() + + confirm = self.signed_address("approve", id) + deny = self.signed_address("reject", id) + self.send_template(self.ignore(), deny, recipients, "msg-moderate", + { + "confirm": confirm, + "deny": deny, + "origmail": text, + "boundary": self.invent_boundary(), + }) + self.send_info_message([poster], "msg-wait", {}) + + def should_be_moderated(self, posting, poster): + if posting == "moderated": + return 1 + if posting == "auto": + if poster.lower() not in \ + map(string.lower, self.subscribers.get_all()): + return 1 + return 0 + + def obey_post(self, dict, text): + if dict.has_key("force-moderation") and dict["force-moderation"]: + force_moderation = 1 + else: + force_moderation = 0 + if dict.has_key("force-posting") and dict["force-posting"]: + force_posting = 1 + else: + force_posting = 0 + posting = self.cp.get("list", "posting") + if posting not in self.posting_opts: + error("You have a weird 'posting' config. Please, review it") + poster = get_from_environ("SENDER") + if force_moderation: + self.post_into_moderate(poster, dict, text) + elif force_posting: + self.send_mail_to_subscribers(text) + elif self.should_be_moderated(posting, poster): + self.post_into_moderate(poster, dict, text) + else: + self.send_mail_to_subscribers(text) + + def obey_approve(self, dict): + if self.moderation_box.lock(dict["id"]): + if self.moderation_box.has(dict["id"]): + text = self.moderation_box.get(dict["id"]) + self.send_mail_to_subscribers(text) + self.moderation_box.remove(dict["id"]) + self.moderation_box.unlock(dict["id"]) + + def obey_reject(self, dict): + if self.moderation_box.lock(dict["id"]): + if self.moderation_box.has(dict["id"]): + self.moderation_box.remove(dict["id"]) + self.moderation_box.unlock(dict["id"]) + + def split_address_list(self, addrs): + domains = {} + for addr in addrs: + userpart, domain = addr.split("@") + if domains.has_key(domain): + domains[domain].append(addr) + else: + domains[domain] = [addr] + result = [] + if len(domains.keys()) == 1: + for addr in addrs: + result.append([addr]) + else: + result = domains.values() + return result + + def obey_bounce(self, dict, text): + if self.subscribers.has_group(dict["id"]): + self.subscribers.lock() + addrs = self.subscribers.in_group(dict["id"]) + if len(addrs) == 1: + if self.cp.get("list", "ignore-bounce") == "yes": + info("Address <%s> bounced, ignoring bounce as configured." % + addrs[0]) + self.subscribers.unlock() + return + debug("Address <%s> bounced, setting state to bounce." % + addrs[0]) + bounce_id = self.bounce_box.add(addrs[0], text[:4096]) + self.subscribers.set(dict["id"], "status", "bounced") + self.subscribers.set(dict["id"], "timestamp-bounced", + "%f" % time.time()) + self.subscribers.set(dict["id"], "bounce-id", + bounce_id) + else: + debug("Group %s bounced, splitting." % dict["id"]) + for new_addrs in self.split_address_list(addrs): + self.subscribers.add_many(new_addrs) + self.subscribers.remove_group(dict["id"]) + self.subscribers.save() + else: + debug("Ignoring bounce, group %s doesn't exist (anymore?)." % + dict["id"]) + + def obey_probe(self, dict, text): + id = dict["id"] + if self.subscribers.has_group(id): + self.subscribers.lock() + if self.subscribers.get(id, "status") == "probed": + self.subscribers.set(id, "status", "probebounced") + self.subscribers.save() + + def obey(self, dict): + text = self.read_stdin() + + if dict["command"] in ["help", "list", "subscribe", "unsubscribe", + "subyes", "subapprove", "subreject", + "unsubyes", "post", "approve"]: + sender = get_from_environ("SENDER") + if not sender: + debug("Ignoring bounce message for %s command." % + dict["command"]) + return + + if dict["command"] == "help": + self.obey_help() + elif dict["command"] == "list": + self.obey_list() + elif dict["command"] == "owner": + self.obey_owner(text) + elif dict["command"] == "subscribe": + self.obey_subscribe(dict, text) + elif dict["command"] == "unsubscribe": + self.obey_unsubscribe(dict, text) + elif dict["command"] == "subyes": + self.obey_subyes(dict) + elif dict["command"] == "subapprove": + self.obey_subapprove(dict) + elif dict["command"] == "subreject": + self.obey_subreject(dict) + elif dict["command"] == "unsubyes": + self.obey_unsubyes(dict) + elif dict["command"] == "post": + self.obey_post(dict, text) + elif dict["command"] == "approve": + self.obey_approve(dict) + elif dict["command"] == "reject": + self.obey_reject(dict) + elif dict["command"] == "bounce": + self.obey_bounce(dict, text) + elif dict["command"] == "probe": + self.obey_probe(dict, text) + elif dict["command"] == "setlist": + self.obey_setlist(text) + elif dict["command"] == "setlistsilently": + self.obey_setlistsilently(text) + elif dict["command"] == "setlistyes": + self.obey_setlistyes(dict) + elif dict["command"] == "setlistsilentyes": + self.obey_setlistsilentyes(dict) + elif dict["command"] == "ignore": + pass + + def get_bounce_text(self, id): + bounce_id = self.subscribers.get(id, "bounce-id") + if self.bounce_box.has(bounce_id): + bounce_text = self.bounce_box.get(bounce_id) + bounce_text = string.join(map(lambda s: "> " + s + "\n", + bounce_text.split("\n")), "") + else: + bounce_text = "Bounce message not available." + return bounce_text + + one_week = 7.0 * 24.0 * 60.0 * 60.0 + + def handle_bounced_groups(self, now): + for id in self.subscribers.groups(): + status = self.subscribers.get(id, "status") + t = float(self.subscribers.get(id, "timestamp-bounced")) + if status == "bounced": + if now - t > self.one_week: + sender = self.signed_address("probe", id) + recipients = self.subscribers.in_group(id) + self.send_template(sender, sender, recipients, + "bounce-warning", { + "bounce": self.get_bounce_text(id), + "boundary": self.invent_boundary(), + }) + self.subscribers.set(id, "status", "probed") + elif status == "probed": + if now - t > 2 * self.one_week: + debug(("Cleaning woman: probe didn't bounce " + + "for group <%s>, setting status to ok.") % id) + self.subscribers.set(id, "status", "ok") + self.bounce_box.remove( + self.subscribers.get(id, "bounce-id")) + elif status == "probebounced": + sender = self.command_address("help") + for address in self.subscribers.in_group(id): + if self.cp.get("list", "mail-on-forced-unsubscribe") \ + == "yes": + self.send_template(sender, sender, + self.owners(), + "bounce-owner-notification", + { + "address": address, + "bounce": self.get_bounce_text(id), + "boundary": self.invent_boundary(), + }) + + self.bounce_box.remove( + self.subscribers.get(id, "bounce-id")) + self.subscribers.remove(address) + debug("Cleaning woman: removing <%s>." % address) + self.send_template(sender, sender, [address], + "bounce-goodbye", {}) + + def join_nonbouncing_groups(self, now): + to_be_joined = [] + for id in self.subscribers.groups(): + status = self.subscribers.get(id, "status") + age1 = now - float(self.subscribers.get(id, "timestamp-bounced")) + age2 = now - float(self.subscribers.get(id, "timestamp-created")) + if status == "ok": + if age1 > self.one_week and age2 > self.one_week: + to_be_joined.append(id) + if to_be_joined: + addrs = [] + for id in to_be_joined: + addrs = addrs + self.subscribers.in_group(id) + self.subscribers.add_many(addrs) + for id in to_be_joined: + self.bounce_box.remove(self.subscribers.get(id, "bounce-id")) + self.subscribers.remove_group(id) + + def remove_empty_groups(self): + for id in self.subscribers.groups()[:]: + if len(self.subscribers.in_group(id)) == 0: + self.subscribers.remove_group(id) + + def cleaning_woman(self, now): + if self.subscribers.lock(): + self.handle_bounced_groups(now) + self.join_nonbouncing_groups(now) + self.subscribers.save() + +class SubscriberDatabase: + + def __init__(self, dirname, name): + self.dict = {} + self.filename = os.path.join(dirname, name) + self.lockname = os.path.join(dirname, "lock") + self.loaded = 0 + self.locked = 0 + + def lock(self): + if os.system("lockfile -l 60 %s" % self.lockname) == 0: + self.locked = 1 + self.load() + return self.locked + + def unlock(self): + os.remove(self.lockname) + self.locked = 0 + + def load(self): + if not self.loaded and not self.dict: + f = open(self.filename, "r") + for line in f.xreadlines(): + parts = line.split() + self.dict[parts[0]] = { + "status": parts[1], + "timestamp-created": parts[2], + "timestamp-bounced": parts[3], + "bounce-id": parts[4], + "addresses": parts[5:], + } + f.close() + self.loaded = 1 + + def save(self): + assert self.locked + assert self.loaded + f = open(self.filename + ".new", "w") + for id in self.dict.keys(): + f.write("%s " % id) + f.write("%s " % self.dict[id]["status"]) + f.write("%s " % self.dict[id]["timestamp-created"]) + f.write("%s " % self.dict[id]["timestamp-bounced"]) + f.write("%s " % self.dict[id]["bounce-id"]) + f.write("%s\n" % string.join(self.dict[id]["addresses"], " ")) + f.close() + os.remove(self.filename) + os.rename(self.filename + ".new", self.filename) + self.unlock() + + def get(self, id, attribute): + self.load() + if self.dict.has_key(id) and self.dict[id].has_key(attribute): + return self.dict[id][attribute] + return None + + def set(self, id, attribute, value): + assert self.locked + self.load() + if self.dict.has_key(id) and self.dict[id].has_key(attribute): + self.dict[id][attribute] = value + + def add(self, address): + return self.add_many([address]) + + def add_many(self, addresses): + assert self.locked + assert self.loaded + for addr in addresses[:]: + if addr.find("@") == -1: + info("Address '%s' does not contain an @, ignoring it." % addr) + addresses.remove(addr) + for id in self.dict.keys(): + old_ones = self.dict[id]["addresses"] + for addr in addresses: + for x in old_ones: + if x.lower() == addr.lower(): + old_ones.remove(x) + self.dict[id]["addresses"] = old_ones + id = self.new_group() + self.dict[id] = { + "status": "ok", + "timestamp-created": self.timestamp(), + "timestamp-bounced": "0", + "bounce-id": "..notexist..", + "addresses": addresses, + } + return id + + def new_group(self): + keys = self.dict.keys() + if keys: + keys = map(lambda x: int(x), keys) + keys.sort() + return "%d" % (keys[-1] + 1) + else: + return "0" + + def timestamp(self): + return "%.0f" % time.time() + + def get_all(self): + self.load() + list = [] + for values in self.dict.values(): + list = list + values["addresses"] + return list + + def groups(self): + self.load() + return self.dict.keys() + + def has_group(self, id): + self.load() + return self.dict.has_key(id) + + def in_group(self, id): + self.load() + return self.dict[id]["addresses"] + + def remove(self, address): + assert self.locked + self.load() + for id in self.dict.keys(): + group = self.dict[id] + for x in group["addresses"][:]: + if x.lower() == address.lower(): + group["addresses"].remove(x) + if len(group["addresses"]) == 0: + del self.dict[id] + + def remove_group(self, id): + assert self.locked + self.load() + del self.dict[id] + + +class MessageBox: + + def __init__(self, dirname, boxname): + self.boxdir = os.path.join(dirname, boxname) + if not os.path.isdir(self.boxdir): + os.mkdir(self.boxdir, 0700) + + def filename(self, id): + return os.path.join(self.boxdir, id) + + def add(self, address, message_text): + id = self.make_id(message_text) + filename = self.filename(id) + f = open(filename + ".address", "w") + f.write(address) + f.close() + f = open(filename + ".new", "w") + f.write(message_text) + f.close() + os.rename(filename + ".new", filename) + return id + + def make_id(self, message_text): + return md5sum_as_hex(message_text) + # XXX this might be unnecessarily long + + def remove(self, id): + filename = self.filename(id) + if os.path.isfile(filename): + os.remove(filename) + os.remove(filename + ".address") + + def has(self, id): + return os.path.isfile(self.filename(id)) + + def get_address(self, id): + f = open(self.filename(id) + ".address", "r") + data = f.read() + f.close() + return data.strip() + + def get(self, id): + f = open(self.filename(id), "r") + data = f.read() + f.close() + return data + + def lockname(self, id): + return self.filename(id) + ".lock" + + def lock(self, id): + if os.system("lockfile -l 600 %s" % self.lockname(id)) == 0: + return 1 + else: + return 0 + + def unlock(self, id): + try: + os.remove(self.lockname(id)) + except os.error: + pass + + + +class DevNull: + + def write(self, str): + pass + + +log_file_handle = None +def log_file(): + global log_file_handle + if log_file_handle is None: + try: + log_file_handle = open(os.path.join(DOTDIR, "logfile.txt"), "a") + except: + log_file_handle = DevNull() + return log_file_handle + +def timestamp(): + tuple = time.localtime(time.time()) + return time.strftime("%Y-%m-%d %H:%M:%S", tuple) + " [%d]" % os.getpid() + + +quiet = 0 + + +# No logging to stderr of debug messages. Some MTAs have a limit on how +# much data they accept via stderr and debug logs will fill that quickly. +def debug(msg): + log_file().write(timestamp() + " " + msg + "\n") + + +# Log to log file first, in case MTA's stderr buffer fills up and we lose +# logs. +def info(msg): + log_file().write(timestamp() + " " + msg + "\n") + sys.stderr.write(msg + "\n") + + +def error(msg): + info(msg) + sys.exit(1) + + +def usage(): + sys.stdout.write("""\ +Usage: enemies-of-carlotta [options] command +Mailing list manager. + +Options: + --name=listname@domain + --owner=address@domain + --moderator=address@domain + --subscription=free/moderated + --posting=free/moderated/auto + --archived=yes/no + --ignore-bounce=yes/no + --language=language code or empty + --mail-on-forced-unsubscribe=yes/no + --mail-on-subscription-changes=yes/no + --skip-prefix=string + --domain=domain.name + --smtp-server=domain.name + --quiet + --moderate + +Commands: + --help + --create + --subscribe + --unsubscribe + --list + --is-list + --edit + --incoming + --cleaning-woman + --show-lists + +For more detailed information, please read the enemies-of-carlotta(1) +manual page. +""") + sys.exit(0) + + +def no_act_send_mail(sender, recipients, text): + print "NOT SENDING MAIL FOR REAL!" + print "Sender:", sender + print "Recipients:", recipients + print "Mail:" + print "\n".join(map(lambda s: " " + s, text.split("\n"))) + + +def set_list_options(list, owners, moderators, subscription, posting, + archived, language, ignore_bounce, + mail_on_sub_changes, mail_on_forced_unsub): + if owners: + list.cp.set("list", "owners", string.join(owners, " ")) + if moderators: + list.cp.set("list", "moderators", string.join(moderators, " ")) + if subscription != None: + list.cp.set("list", "subscription", subscription) + if posting != None: + list.cp.set("list", "posting", posting) + if archived != None: + list.cp.set("list", "archived", archived) + if language != None: + list.cp.set("list", "language", language) + if ignore_bounce != None: + list.cp.set("list", "ignore-bounce", ignore_bounce) + if mail_on_sub_changes != None: + list.cp.set("list", "mail-on-subscription-changes", + mail_on_sub_changes) + if mail_on_forced_unsub != None: + list.cp.set("list", "mail-on-forced-unsubscribe", + mail_on_forced_unsub) + + +def main(args): + try: + opts, args = getopt.getopt(args, "h", + ["name=", + "owner=", + "moderator=", + "subscription=", + "posting=", + "archived=", + "language=", + "ignore-bounce=", + "mail-on-forced-unsubscribe=", + "mail-on-subscription-changes=", + "skip-prefix=", + "domain=", + "sendmail=", + "smtp-server=", + "qmqp-server=", + "quiet", + "moderate", + "post", + "sender=", + "recipient=", + "no-act", + + "set", + "get", + "help", + "create", + "destroy", + "subscribe", + "unsubscribe", + "list", + "is-list", + "edit", + "incoming", + "cleaning-woman", + "show-lists", + "version", + ]) + except getopt.GetoptError, detail: + error("Error parsing command line options (see --help):\n%s" % + detail) + + operation = None + list_name = None + owners = [] + moderators = [] + subscription = None + posting = None + archived = None + ignore_bounce = None + skip_prefix = None + domain = None + sendmail = "/usr/sbin/sendmail" + smtp_server = None + qmqp_server = None + moderate = 0 + post = 0 + sender = None + recipient = None + language = None + mail_on_forced_unsub = None + mail_on_sub_changes = None + no_act = 0 + global quiet + + for opt, arg in opts: + if opt == "--name": + list_name = arg + elif opt == "--owner": + owners.append(arg) + elif opt == "--moderator": + moderators.append(arg) + elif opt == "--subscription": + subscription = arg + elif opt == "--posting": + posting = arg + elif opt == "--archived": + archived = arg + elif opt == "--ignore-bounce": + ignore_bounce = arg + elif opt == "--skip-prefix": + skip_prefix = arg + elif opt == "--domain": + domain = arg + elif opt == "--sendmail": + sendmail = arg + elif opt == "--smtp-server": + smtp_server = arg + elif opt == "--qmqp-server": + qmqp_server = arg + elif opt == "--sender": + sender = arg + elif opt == "--recipient": + recipient = arg + elif opt == "--language": + language = arg + elif opt == "--mail-on-forced-unsubscribe": + mail_on_forced_unsub = arg + elif opt == "--mail-on-subscription-changes": + mail_on_sub_changes = arg + elif opt == "--moderate": + moderate = 1 + elif opt == "--post": + post = 1 + elif opt == "--quiet": + quiet = 1 + elif opt == "--no-act": + no_act = 1 + else: + operation = opt + + if operation is None: + error("No operation specified, see --help.") + + if list_name is None and operation not in ["--incoming", "--help", "-h", + "--cleaning-woman", + "--show-lists", + "--version"]: + error("%s requires a list name specified with --name" % operation) + + if operation in ["--help", "-h"]: + usage() + + if sender or recipient: + environ = os.environ.copy() + if sender: + environ["SENDER"] = sender + if recipient: + environ["RECIPIENT"] = recipient + set_environ(environ) + + mlm = MailingListManager(DOTDIR, sendmail=sendmail, + smtp_server=smtp_server, + qmqp_server=qmqp_server) + if no_act: + mlm.send_mail = no_act_send_mail + + if operation == "--create": + if not owners: + error("You must give at least one list owner with --owner.") + list = mlm.create_list(list_name) + set_list_options(list, owners, moderators, subscription, posting, + archived, language, ignore_bounce, + mail_on_sub_changes, mail_on_forced_unsub) + list.save_config() + debug("Created list %s." % list_name) + elif operation == "--destroy": + shutil.rmtree(os.path.join(DOTDIR, list_name)) + debug("Removed list %s." % list_name) + elif operation == "--edit": + list = mlm.open_list(list_name) + set_list_options(list, owners, moderators, subscription, posting, + archived, language, ignore_bounce, + mail_on_sub_changes, mail_on_forced_unsub) + list.save_config() + elif operation == "--subscribe": + list = mlm.open_list(list_name) + list.subscribers.lock() + for address in args: + if address.find("@") == -1: + error("Address '%s' does not contain an @." % address) + list.subscribers.add(address) + debug("Added subscriber <%s>." % address) + list.subscribers.save() + elif operation == "--unsubscribe": + list = mlm.open_list(list_name) + list.subscribers.lock() + for address in args: + list.subscribers.remove(address) + debug("Removed subscriber <%s>." % address) + list.subscribers.save() + elif operation == "--list": + list = mlm.open_list(list_name) + for address in list.subscribers.get_all(): + print address + elif operation == "--is-list": + if mlm.is_list(list_name, skip_prefix, domain): + debug("Indeed a mailing list: <%s>" % list_name) + else: + debug("Not a mailing list: <%s>" % list_name) + sys.exit(1) + elif operation == "--incoming": + mlm.incoming_message(skip_prefix, domain, moderate, post) + elif operation == "--cleaning-woman": + mlm.cleaning_woman() + elif operation == "--show-lists": + listnames = mlm.get_lists() + listnames.sort() + for listname in listnames: + print listname + elif operation == "--get": + list = mlm.open_list(list_name) + for name in args: + print list.cp.get("list", name) + elif operation == "--set": + list = mlm.open_list(list_name) + for arg in args: + if "=" not in arg: + error("Error: --set arguments must be of form name=value") + name, value = arg.split("=", 1) + list.cp.set("list", name, value) + list.save_config() + elif operation == "--version": + print "EoC, version %s" % VERSION + print "Home page: http://liw.iki.fi/liw/eoc/" + else: + error("Internal error: unimplemented option <%s>." % operation) + +if __name__ == "__main__": + try: + main(sys.argv[1:]) + except EocException, detail: + error("Error: %s" % detail) diff --git a/eocTests.py b/eocTests.py new file mode 100644 index 0000000..acf99b3 --- /dev/null +++ b/eocTests.py @@ -0,0 +1,1100 @@ +import unittest +import shutil +import os +import re +import time +import string + +import eoc + +DOTDIR = "dot-dir-for-testing" +eoc.DOTDIR = DOTDIR +eoc.quiet = 1 + +def no_op(*args): + pass + +class AddressCleaningTestCases(unittest.TestCase): + + def setUp(self): + self.ap = eoc.AddressParser(["foo@EXAMPLE.com", + "bar@lists.example.com"]) + + def verify(self, address, wanted, skip_prefix=None, forced_domain=None): + self.ap.set_skip_prefix(skip_prefix) + self.ap.set_forced_domain(forced_domain) + address = self.ap.clean(address) + self.failUnlessEqual(address, wanted) + + def testSimpleAddress(self): + self.verify("foo@example.com", "foo@example.com") + + def testUpperCaseAddress(self): + self.verify("FOO@EXAMPLE.COM", "foo@example.com") + + def testPrefixRemoval(self): + self.verify("foo@example.com", "foo@example.com", + skip_prefix="user-") + self.verify("user-foo@example.com", "foo@example.com", + skip_prefix="user-") + + def testForcedDomain(self): + self.verify("foo@example.com", "foo@example.com", + forced_domain="example.com") + self.verify("foo@whatever.example.com", "foo@example.com", + forced_domain="example.com") + + def testPrefixRemovalWithForcedDomain(self): + self.verify("foo@example.com", "foo@example.com", + skip_prefix="user-", + forced_domain="example.com") + self.verify("foo@whatever.example.com", "foo@example.com", + skip_prefix="user-", + forced_domain="example.com") + self.verify("user-foo@example.com", "foo@example.com", + skip_prefix="user-", + forced_domain="example.com") + self.verify("user-foo@whatever.example.com", "foo@example.com", + skip_prefix="user-", + forced_domain="example.com") + +class AddressParserTestCases(unittest.TestCase): + + def setUp(self): + self.ap = eoc.AddressParser(["foo@EXAMPLE.com", + "bar@lists.example.com"]) + + def verify_parser(self, address, wanted_listname, wanted_parts): + listname, parts = self.ap.parse(address) + self.failUnlessEqual(listname, wanted_listname) + self.failUnlessEqual(parts, wanted_parts) + + def testParser(self): + self.verify_parser("foo@example.com", + "foo@EXAMPLE.com", + []) + self.verify_parser("foo-subscribe@example.com", + "foo@EXAMPLE.com", + ["subscribe"]) + self.verify_parser("foo-subscribe-joe=example.com@example.com", + "foo@EXAMPLE.com", + ["subscribe", "joe=example.com"]) + self.verify_parser("foo-bounce-123-ABCDEF@example.com", + "foo@EXAMPLE.com", + ["bounce", "123", "abcdef"]) + +class ParseRecipientAddressBase(unittest.TestCase): + + def setUp(self): + self.lists = ["foo@example.com", + "bar@lists.example.com", + "foo-announce@example.com"] + self.mlm = eoc.MailingListManager(DOTDIR, lists=self.lists) + + def environ(self, sender, recipient): + eoc.set_environ({ + "SENDER": sender, + "RECIPIENT": recipient, + }) + +class ParseUnsignedAddressTestCases(ParseRecipientAddressBase): + + def testEmpty(self): + self.failUnlessRaises(eoc.UnknownList, + self.mlm.parse_recipient_address, + "", None, None) + + def verify(self, address, skip_prefix, forced_domain, wanted_dict): + dict = self.mlm.parse_recipient_address(address, skip_prefix, + forced_domain) + self.failUnlessEqual(dict, wanted_dict) + + def testSimpleAddresses(self): + self.verify("foo@example.com", + None, + None, + { "name": "foo@example.com", "command": "post" }) + self.verify("FOO@EXAMPLE.COM", + None, + None, + { "name": "foo@example.com", "command": "post" }) + self.verify("prefix-foo@example.com", + "prefix-", + None, + { "name": "foo@example.com", "command": "post" }) + self.verify("bar@example.com", + None, + "lists.example.com", + { "name": "bar@lists.example.com", "command": "post" }) + self.verify("prefix-bar@example.com", + "prefix-", + "lists.example.com", + { "name": "bar@lists.example.com", "command": "post" }) + + def testSubscription(self): + self.verify("foo-subscribe@example.com", + None, + None, + { "name": "foo@example.com", + "command": "subscribe", + "sender": "", + }) + self.verify("foo-subscribe-joe-user=example.com@example.com", + None, + None, + { "name": "foo@example.com", + "command": "subscribe", + "sender": "joe-user@example.com", + }) + self.verify("foo-unsubscribe@example.com", + None, + None, + { "name": "foo@example.com", + "command": "unsubscribe", + "sender": "", + }) + self.verify("foo-unsubscribe-joe-user=example.com@example.com", + None, + None, + { "name": "foo@example.com", + "command": "unsubscribe", + "sender": "joe-user@example.com", + }) + + def testPost(self): + for name in self.lists: + self.verify(name, None, None, { "name": name, "command": "post" }) + + def testSimpleCommands(self): + for name in self.lists: + for command in ["help", "list", "owner"]: + localpart, domain = name.split("@") + address = "%s-%s@%s" % (localpart, command, domain) + self.verify(address, None, None, + { "name": name, + "command": command + }) + +class ParseWellSignedAddressTestCases(ParseRecipientAddressBase): + + def try_good_signature(self, command): + s = "foo-announce-%s-1" % command + hash = self.mlm.compute_hash("%s@%s" % (s, "example.com")) + local_part = "%s-%s" % (s, hash) + dict = self.mlm.parse_recipient_address("%s@example.com" % local_part, + None, None) + self.failUnlessEqual(dict, + { + "name": "foo-announce@example.com", + "command": command, + "id": "1", + }) + + def testProperlySignedCommands(self): + self.try_good_signature("subyes") + self.try_good_signature("subapprove") + self.try_good_signature("subreject") + self.try_good_signature("unsubyes") + self.try_good_signature("bounce") + self.try_good_signature("approve") + self.try_good_signature("reject") + self.try_good_signature("probe") + +class ParseBadlySignedAddressTestCases(ParseRecipientAddressBase): + + def try_bad_signature(self, command_part): + self.failUnlessRaises(eoc.BadSignature, + self.mlm.parse_recipient_address, + "foo-announce-" + command_part + + "-123-badhash@example.com", + None, None) + + def testBadlySignedCommands(self): + self.try_bad_signature("subyes") + self.try_bad_signature("subapprove") + self.try_bad_signature("subreject") + self.try_bad_signature("unsubyes") + self.try_bad_signature("bounce") + self.try_bad_signature("approve") + self.try_bad_signature("reject") + self.try_bad_signature("probe") + +class DotDirTestCases(unittest.TestCase): + + def setUp(self): + self.secret_name = os.path.join(DOTDIR, "secret") + + def tearDown(self): + shutil.rmtree(DOTDIR) + + def dotdir_is_ok(self): + self.failUnless(os.path.isdir(DOTDIR)) + self.failUnless(os.path.isfile(self.secret_name)) + + def testNoDotDirExists(self): + self.failIf(os.path.exists(DOTDIR)) + mlm = eoc.MailingListManager(DOTDIR) + self.dotdir_is_ok() + + def testDotDirDoesExistButSecretDoesNot(self): + self.failIf(os.path.exists(DOTDIR)) + os.makedirs(DOTDIR) + self.failUnless(os.path.isdir(DOTDIR)) + self.failIf(os.path.exists(self.secret_name)) + mlm = eoc.MailingListManager(DOTDIR) + self.dotdir_is_ok() + +class ListBase(unittest.TestCase): + + def setUp(self): + if os.path.exists(DOTDIR): + shutil.rmtree(DOTDIR) + self.mlm = eoc.MailingListManager(DOTDIR) + + def tearDown(self): + self.mlm = None + shutil.rmtree(DOTDIR) + +class ListCreationTestCases(ListBase): + + def setUp(self): + ListBase.setUp(self) + self.names = None + + def listdir(self, listname): + return os.path.join(DOTDIR, listname) + + def listdir_has_file(self, listdir, filename): + self.failUnless(os.path.isfile(os.path.join(listdir, filename))) + self.names.remove(filename) + + def listdir_has_dir(self, listdir, dirname): + self.failUnless(os.path.isdir(os.path.join(listdir, dirname))) + self.names.remove(dirname) + + def listdir_may_have_dir(self, listdir, dirname): + if dirname in self.names: + self.listdir_has_dir(listdir, dirname) + + def listdir_is_ok(self, listname): + listdir = self.listdir(listname) + self.failUnless(os.path.isdir(listdir)) + self.names = os.listdir(listdir) + + self.listdir_has_file(listdir, "config") + self.listdir_has_file(listdir, "subscribers") + + self.listdir_has_dir(listdir, "bounce-box") + self.listdir_has_dir(listdir, "subscription-box") + + self.listdir_may_have_dir(listdir, "moderation-box") + self.listdir_may_have_dir(listdir, "templates") + + # Make sure there are no extras. + self.failUnlessEqual(self.names, []) + + def testCreateNew(self): + self.failIf(os.path.exists(self.listdir("foo@example.com"))) + ml = self.mlm.create_list("foo@example.com") + + self.failUnlessEqual(ml.__class__, eoc.MailingList) + self.failUnlessEqual(ml.dirname, self.listdir("foo@example.com")) + + self.listdir_is_ok("foo@example.com") + + def testCreateExisting(self): + list = self.mlm.create_list("foo@example.com") + self.failUnlessRaises(eoc.ListExists, + self.mlm.create_list, "foo@example.com") + self.listdir_is_ok("foo@example.com") + +class ListOptionTestCases(ListBase): + + def check(self, ml, wanted): + self.failUnlessEqual(ml.cp.sections(), ["list"]) + cpdict = {} + for key, value in ml.cp.items("list"): + cpdict[key] = value + self.failUnlessEqual(cpdict, wanted) + + def testDefaultOptionsOnCreateAndOpenExisting(self): + self.mlm.create_list("foo@example.com") + ml = self.mlm.open_list("foo@example.com") + self.check(ml, + { + "owners": "", + "moderators": "", + "subscription": "free", + "posting": "free", + "archived": "no", + "mail-on-subscription-changes": "no", + "mail-on-forced-unsubscribe": "no", + "ignore-bounce": "no", + "language": "", + "pristine-headers": "", + }) + + def testChangeOptions(self): + # Create a list, change some options, and save the result. + ml = self.mlm.create_list("foo@example.com") + self.failUnlessEqual(ml.cp.get("list", "owners"), "") + self.failUnlessEqual(ml.cp.get("list", "posting"), "free") + ml.cp.set("list", "owners", "owner@example.com") + ml.cp.set("list", "posting", "moderated") + ml.save_config() + + # Re-open the list and check that the new instance has read the + # values from the disk correctly. + ml2 = self.mlm.open_list("foo@example.com") + self.check(ml2, + { + "owners": "owner@example.com", + "moderators": "", + "subscription": "free", + "posting": "moderated", + "archived": "no", + "mail-on-subscription-changes": "no", + "mail-on-forced-unsubscribe": "no", + "ignore-bounce": "no", + "language": "", + "pristine-headers": "", + }) + +class SubscriberDatabaseTestCases(ListBase): + + def has_subscribers(self, ml, addrs): + subs = ml.subscribers.get_all() + subs.sort() + self.failUnlessEqual(subs, addrs) + + def testAddAndRemoveSubscribers(self): + addrs = ["joe@example.com", "MARY@example.com", "bubba@EXAMPLE.com"] + addrs.sort() + + ml = self.mlm.create_list("foo@example.com") + self.failUnlessEqual(ml.subscribers.get_all(), []) + + self.failUnless(ml.subscribers.lock()) + ml.subscribers.add_many(addrs) + self.has_subscribers(ml, addrs) + ml.subscribers.save() + self.failIf(ml.subscribers.locked) + ml = None + + ml2 = self.mlm.open_list("foo@example.com") + self.has_subscribers(ml2, addrs) + ml2.subscribers.lock() + ml2.subscribers.remove(addrs[0]) + self.has_subscribers(ml2, addrs[1:]) + + ml2.subscribers.save() + + ml3 = self.mlm.open_list("foo@example.com") + self.has_subscribers(ml3, addrs[1:]) + + def testSubscribeTwice(self): + ml = self.mlm.create_list("foo@example.com") + self.failUnlessEqual(ml.subscribers.get_all(), []) + ml.subscribers.lock() + ml.subscribers.add("user@example.com") + ml.subscribers.add("USER@example.com") + self.failUnlessEqual(map(string.lower, ml.subscribers.get_all()), + map(string.lower, ["USER@example.com"])) + + def testSubscriberAttributesAndGroups(self): + addrs = ["joe@example.com", "mary@example.com"] + addrs.sort() + ml = self.mlm.create_list("foo@example.com") + self.failUnlessEqual(ml.subscribers.groups(), []) + ml.subscribers.lock() + id = ml.subscribers.add_many(addrs) + self.failUnlessEqual(ml.subscribers.groups(), ["0"]) + self.failUnlessEqual(ml.subscribers.get(id, "status"), "ok") + ml.subscribers.set(id, "status", "bounced") + self.failUnlessEqual(ml.subscribers.get(id, "status"), "bounced") + subs = ml.subscribers.in_group(id) + subs.sort() + self.failUnlessEqual(subs, addrs) + +class ModerationBoxTestCases(ListBase): + + def testModerationBox(self): + ml = self.mlm.create_list("foo@example.com") + listdir = os.path.join(DOTDIR, "foo@example.com") + boxdir = os.path.join(listdir, "moderation-box") + + self.failUnlessEqual(boxdir, ml.moderation_box.boxdir) + self.failUnless(os.path.isdir(boxdir)) + + mailtext = "From: foo\nTo: bar\n\nhello\n" + id = ml.moderation_box.add("foo", mailtext) + self.failUnless(ml.moderation_box.has(id)) + self.failUnlessEqual(ml.moderation_box.get_address(id), "foo") + self.failUnlessEqual(ml.moderation_box.get(id), mailtext) + + filename = os.path.join(boxdir, id) + self.failUnless(os.path.isfile(filename)) + self.failUnless(os.path.isfile(filename + ".address")) + + ml.moderation_box.remove(id) + self.failIf(ml.moderation_box.has(id)) + self.failUnless(not os.path.exists(filename)) + +class IncomingBase(unittest.TestCase): + + def setUp(self): + if os.path.isdir(DOTDIR): + shutil.rmtree(DOTDIR) + self.mlm = eoc.MailingListManager(DOTDIR) + self.ml = None + ml = self.mlm.create_list("foo@EXAMPLE.com") + ml.cp.set("list", "owners", "listmaster@example.com") + ml.save_config() + ml.subscribers.lock() + ml.subscribers.add("USER1@example.com") + ml.subscribers.add("user2@EXAMPLE.com") + ml.subscribers.save() + self.write_file_in_listdir(ml, "headers-to-add", "X-Foo: foo\n") + self.write_file_in_listdir(ml, "headers-to-remove", "Received\n") + self.sent_mail = [] + + def tearDown(self): + shutil.rmtree(DOTDIR) + + def write_file_in_listdir(self, ml, basename, contents): + f = open(os.path.join(ml.dirname, basename), "w") + f.write(contents) + f.close() + + def configure_list(self, subscription, posting): + list = self.mlm.open_list("foo@example.com") + list.cp.set("list", "subscription", subscription) + list.cp.set("list", "posting", posting) + list.save_config() + + def environ(self, sender, recipient): + eoc.set_environ({ + "SENDER": sender, + "RECIPIENT": recipient, + }) + + def catch_sendmail(self, sender, recipients, text): + self.sent_mail.append({ + "sender": sender, + "recipients": recipients, + "text": text, + }) + + def send(self, sender, recipient, text="", force_moderation=0, + force_posting=0): + self.environ(sender, recipient) + dict = self.mlm.parse_recipient_address(recipient, None, None) + dict["force-moderation"] = force_moderation + dict["force-posting"] = force_posting + self.ml = self.mlm.open_list(dict["name"]) + if "\n\n" not in text: + text = "\n\n" + text + text = "Received: foobar\n" + text + self.ml.read_stdin = lambda t=text: t + self.mlm.send_mail = self.catch_sendmail + self.sent_mail = [] + self.ml.obey(dict) + + def sender_matches(self, mail, sender): + pat = "(?P
" + sender + ")" + m = re.match(pat, mail["sender"], re.I) + if m: + return m.group("address") + else: + return None + + def replyto_matches(self, mail, replyto): + pat = "(.|\n)*(?P
" + replyto + ")" + m = re.match(pat, mail["text"], re.I) + if m: + return m.group("address") + else: + return None + + def receiver_matches(self, mail, recipient): + return map(string.lower, mail["recipients"]) == [recipient.lower()] + + def body_matches(self, mail, body): + if body: + pat = re.compile("(.|\n)*" + body + "(.|\n)*") + m = re.match(pat, mail["text"]) + return m + else: + return 1 + + def headers_match(self, mail, header): + if header: + pat = re.compile("(.|\n)*" + header + "(.|\n)*", re.I) + m = re.match(pat, mail["text"]) + return m + else: + return 1 + + def match(self, sender, replyto, receiver, body=None, header=None, + anti_header=None): + ret = None + for mail in self.sent_mail: + if replyto is None: + m1 = self.sender_matches(mail, sender) + m3 = self.receiver_matches(mail, receiver) + m4 = self.body_matches(mail, body) + m5 = self.headers_match(mail, header) + m6 = self.headers_match(mail, anti_header) + no_anti_header = anti_header == None or m6 == None + if m1 != None and m3 and m4 and m5 and no_anti_header: + ret = m1 + self.sent_mail.remove(mail) + break + else: + m1 = self.sender_matches(mail, sender) + m2 = self.replyto_matches(mail, replyto) + m3 = self.receiver_matches(mail, receiver) + m4 = self.body_matches(mail, body) + m5 = self.headers_match(mail, header) + m6 = self.headers_match(mail, anti_header) + no_anti_header = anti_header == None or m6 == None + if m1 != None and m2 != None and m3 and m4 and m5 and \ + no_anti_header: + ret = m2 + self.sent_mail.remove(mail) + break + self.failUnless(ret != None) + return ret + + def no_more_mail(self): + self.failUnlessEqual(self.sent_mail, []) + + +class SimpleCommandAddressTestCases(IncomingBase): + + def testHelp(self): + self.send("outsider@example.com", "foo-help@example.com") + self.match("foo-ignore@example.com", None, "outsider@example.com", + "Subject: Help for") + self.no_more_mail() + + def testOwner(self): + self.send("outsider@example.com", "foo-owner@example.com", "abcde") + self.match("outsider@example.com", None, "listmaster@example.com", + "abcde") + self.no_more_mail() + + def testIgnore(self): + self.send("outsider@example.com", "foo-ignore@example.com", "abcde") + self.no_more_mail() + +class OwnerCommandTestCases(IncomingBase): + + def testList(self): + self.send("listmaster@example.com", "foo-list@example.com") + self.match("foo-ignore@example.com", None, "listmaster@example.com", + "[uU][sS][eE][rR][12]@" + + "[eE][xX][aA][mM][pP][lL][eE]\\.[cC][oO][mM]\n" + + "[uU][sS][eE][rR][12]@" + + "[eE][xX][aA][mM][pP][lL][eE]\\.[cC][oO][mM]\n") + self.no_more_mail() + + def testListDenied(self): + self.send("outsider@example.com", "foo-list@example.com") + self.match("foo-ignore@example.com", None, "outsider@example.com", + "Subject: Subscriber list denied") + self.no_more_mail() + + def testSetlist(self): + self.send("listmaster@example.com", "foo-setlist@example.com", + "From: foo\n\nnew1@example.com\nuser1@example.com\n") + a = self.match("foo-ignore@example.com", + "foo-setlistyes-[^@]*@example.com", + "listmaster@example.com", + "Subject: Please moderate subscriber list") + self.no_more_mail() + + self.send("listmaster@example.com", a) + self.match("foo-ignore@example.com", None, "listmaster@example.com", + "Subject: Subscriber list has been changed") + self.match("foo-ignore@example.com", None, "new1@example.com", + "Subject: Welcome to") + self.match("foo-ignore@example.com", None, "user2@EXAMPLE.com", + "Subject: Goodbye from") + self.no_more_mail() + + def testSetlistSilently(self): + self.send("listmaster@example.com", "foo-setlistsilently@example.com", + "From: foo\n\nnew1@example.com\nuser1@example.com\n") + a = self.match("foo-ignore@example.com", + "foo-setlistsilentyes-[^@]*@example.com", + "listmaster@example.com", + "Subject: Please moderate subscriber list") + self.no_more_mail() + + self.send("listmaster@example.com", a) + self.match("foo-ignore@example.com", None, "listmaster@example.com", + "Subject: Subscriber list has been changed") + self.no_more_mail() + + def testSetlistDenied(self): + self.send("outsider@example.com", "foo-setlist@example.com", + "From: foo\n\nnew1@example.com\nnew2@example.com\n") + self.match("foo-ignore@example.com", + None, + "outsider@example.com", + "Subject: You can't set the subscriber list") + self.no_more_mail() + + def testSetlistBadlist(self): + self.send("listmaster@example.com", "foo-setlist@example.com", + "From: foo\n\nBlah blah blah.\n") + self.match("foo-ignore@example.com", + None, + "listmaster@example.com", + "Subject: Bad address list") + self.no_more_mail() + + def testOwnerSubscribesSomeoneElse(self): + # Send subscription request. List sends confirmation request. + self.send("listmaster@example.com", + "foo-subscribe-outsider=example.com@example.com") + a = self.match("foo-ignore@example.com", + "foo-subyes-[^@]*@example.com", + "listmaster@example.com", + "Please confirm subscription") + self.no_more_mail() + + # Confirm sub. req. List sends welcome. + self.send("listmaster@example.com", a) + self.match("foo-ignore@example.com", + None, + "outsider@example.com", + "Welcome to the") + self.no_more_mail() + + def testOwnerUnubscribesSomeoneElse(self): + # Send unsubscription request. List sends confirmation request. + self.send("listmaster@example.com", + "foo-unsubscribe-outsider=example.com@example.com") + a = self.match("foo-ignore@example.com", + "foo-unsubyes-[^@]*@example.com", + "listmaster@example.com", + "Subject: Please confirm UNsubscription") + self.no_more_mail() + + # Confirm sub. req. List sends welcome. + self.send("listmaster@example.com", a) + self.match("foo-ignore@example.com", None, "outsider@example.com", + "Goodbye") + self.no_more_mail() + +class SubscriptionTestCases(IncomingBase): + + def confirm(self, recipient): + # List has sent confirmation request. Respond to it. + a = self.match("foo-ignore@example.com", + "foo-subyes-[^@]*@example.com", + recipient, + "Please confirm subscription") + self.no_more_mail() + + # Confirm sub. req. List response will be analyzed later. + self.send("something.random@example.com", a) + + def got_welcome(self, recipient): + self.match("foo-ignore@example.com", + None, + recipient, + "Welcome to the") + self.no_more_mail() + + def approve(self, user_recipient): + self.match("foo-ignore@example.com", None, user_recipient) + a = self.match("foo-ignore@example.com", + "foo-subapprove-[^@]*@example.com", + "listmaster@example.com") + self.send("listmaster@example.com", a) + + def reject(self, user_recipient): + self.match("foo-ignore@example.com", None, user_recipient) + a = self.match("foo-ignore@example.com", + "foo-subreject-[^@]*@example.com", + "listmaster@example.com") + self.send("listmaster@example.com", a) + + def testSubscribeToUnmoderatedWithoutAddressNotOnList(self): + self.configure_list("free", "free") + self.send("outsider@example.com", "foo-subscribe@example.com") + self.confirm("outsider@example.com") + self.got_welcome("outsider@example.com") + + def testSubscribeToUnmoderatedWithoutAddressAlreadyOnList(self): + self.configure_list("free", "free") + self.send("user1@example.com", "foo-subscribe@example.com") + self.confirm("user1@example.com") + self.got_welcome("user1@example.com") + + def testSubscribeToUnmoderatedWithAddressNotOnList(self): + self.configure_list("free", "free") + self.send("somebody.else@example.com", + "foo-subscribe-outsider=example.com@example.com") + self.confirm("outsider@example.com") + self.got_welcome("outsider@example.com") + + def testSubscribeToUnmoderatedWithAddressAlreadyOnList(self): + self.configure_list("free", "free") + self.send("somebody.else@example.com", + "foo-subscribe-user1=example.com@example.com") + self.confirm("user1@example.com") + self.got_welcome("user1@example.com") + + def testSubscribeToModeratedWithoutAddressNotOnListApproved(self): + self.configure_list("moderated", "moderated") + self.send("outsider@example.com", "foo-subscribe@example.com") + self.confirm("outsider@example.com") + self.approve("outsider@example.com") + self.got_welcome("outsider@example.com") + + def testSubscribeToModeratedWithoutAddressNotOnListRejected(self): + self.configure_list("moderated", "moderated") + self.send("outsider@example.com", "foo-subscribe@example.com") + self.confirm("outsider@example.com") + self.reject("outsider@example.com") + + def testSubscribeToModeratedWithoutAddressAlreadyOnListApproved(self): + self.configure_list("moderated", "moderated") + self.send("user1@example.com", "foo-subscribe@example.com") + self.confirm("user1@example.com") + self.approve("user1@example.com") + self.got_welcome("user1@example.com") + + def testSubscribeToModeratedWithoutAddressAlreadyOnListRejected(self): + self.configure_list("moderated", "moderated") + self.send("user1@example.com", "foo-subscribe@example.com") + self.confirm("user1@example.com") + self.reject("user1@example.com") + + def testSubscribeToModeratedWithAddressNotOnListApproved(self): + self.configure_list("moderated", "moderated") + self.send("somebody.else@example.com", + "foo-subscribe-outsider=example.com@example.com") + self.confirm("outsider@example.com") + self.approve("outsider@example.com") + self.got_welcome("outsider@example.com") + + def testSubscribeToModeratedWithAddressNotOnListRejected(self): + self.configure_list("moderated", "moderated") + self.send("somebody.else@example.com", + "foo-subscribe-outsider=example.com@example.com") + self.confirm("outsider@example.com") + self.reject("outsider@example.com") + + def testSubscribeToModeratedWithAddressAlreadyOnListApproved(self): + self.configure_list("moderated", "moderated") + self.send("somebody.else@example.com", + "foo-subscribe-user1=example.com@example.com") + self.confirm("user1@example.com") + self.approve("user1@example.com") + self.got_welcome("user1@example.com") + + def testSubscribeToModeratedWithAddressAlreadyOnListRejected(self): + self.configure_list("moderated", "moderated") + self.send("somebody.else@example.com", + "foo-subscribe-user1=example.com@example.com") + self.confirm("user1@example.com") + self.reject("user1@example.com") + +class UnsubscriptionTestCases(IncomingBase): + + def confirm(self, recipient): + # List has sent confirmation request. Respond to it. + a = self.match("foo-ignore@example.com", + "foo-unsubyes-[^@]*@example.com", + recipient, + "Please confirm UNsubscription") + self.no_more_mail() + + # Confirm sub. req. List response will be analyzed later. + self.send("something.random@example.com", a) + + def got_goodbye(self, recipient): + self.match("foo-ignore@example.com", + None, + recipient, + "Goodbye from") + self.no_more_mail() + + def testUnsubscribeWithoutAddressNotOnList(self): + self.send("outsider@example.com", "foo-unsubscribe@example.com") + self.confirm("outsider@example.com") + self.got_goodbye("outsider@example.com") + + def testUnsubscribeWithoutAddressOnList(self): + self.send("user1@example.com", "foo-unsubscribe@example.com") + self.confirm("user1@example.com") + self.got_goodbye("user1@example.com") + + def testUnsubscribeWithAddressNotOnList(self): + self.send("somebody.else@example.com", + "foo-unsubscribe-outsider=example.com@example.com") + self.confirm("outsider@example.com") + self.got_goodbye("outsider@example.com") + + def testUnsubscribeWithAddressOnList(self): + self.send("somebody.else@example.com", + "foo-unsubscribe-user1=example.com@example.com") + self.confirm("user1@example.com") + self.got_goodbye("user1@example.com") + +class PostTestCases(IncomingBase): + + msg = u"Subject: something \u00c4\n\nhello, world\n".encode("utf8") + + def approve(self, user_recipient): + self.match("foo-ignore@example.com", None, user_recipient) + a = self.match("foo-ignore@example.com", + "foo-approve-[^@]*@example.com", + "listmaster@example.com") + self.send("listmaster@example.com", a) + + def reject(self, user_recipient): + self.match("foo-ignore@example.com", None, user_recipient) + a = self.match("foo-ignore@example.com", + "foo-reject-[^@]*@example.com", + "listmaster@example.com") + self.send("listmaster@example.com", a) + + def check_headers_are_encoded(self): + ok_chars = "\t\r\n" + for code in range(32, 127): + ok_chars = ok_chars + chr(code) + for mail in self.sent_mail: + text = mail["text"] + self.failUnless("\n\n" in text) + headers = text.split("\n\n")[0] + for c in headers: + if c not in ok_chars: print headers + self.failUnless(c in ok_chars) + + def check_mail_to_list(self): + self.check_headers_are_encoded() + self.match("foo-bounce-.*@example.com", None, "USER1@example.com", + body="hello, world", + header="X-Foo: FOO", + anti_header="Received:") + self.match("foo-bounce-.*@example.com", None, "user2@EXAMPLE.com", + body="hello, world", + header="x-foo: foo", + anti_header="Received:") + self.no_more_mail() + + def check_that_moderation_box_is_empty(self): + ml = self.mlm.open_list("foo@example.com") + self.failUnlessEqual(os.listdir(ml.moderation_box.boxdir), []) + + def testSubscriberPostsToUnmoderated(self): + self.configure_list("free", "free") + self.send("user1@example.com", "foo@example.com", + self.msg) + self.check_mail_to_list() + + def testOutsiderPostsToUnmoderated(self): + self.configure_list("free", "free") + self.send("outsider@example.com", "foo@example.com", self.msg) + self.check_mail_to_list() + + def testSubscriberPostToAutomoderated(self): + self.configure_list("free", "auto") + self.check_that_moderation_box_is_empty() + self.send("user1@example.com", "foo@example.com", self.msg) + self.check_mail_to_list() + self.check_that_moderation_box_is_empty() + + def testOutsiderPostsToAutomoderatedRejected(self): + self.configure_list("free", "auto") + self.check_that_moderation_box_is_empty() + self.send("outsider@example.com", "foo@example.com", self.msg) + self.reject("outsider@example.com") + self.check_that_moderation_box_is_empty() + + def testOutsiderPostsToAutomoderatedApproved(self): + self.configure_list("free", "auto") + self.check_that_moderation_box_is_empty() + self.send("outsider@example.com", "foo@example.com", self.msg) + self.approve("outsider@example.com") + self.check_mail_to_list() + self.check_that_moderation_box_is_empty() + + def testSubscriberPostsToModeratedRejected(self): + self.configure_list("free", "moderated") + self.check_that_moderation_box_is_empty() + self.send("user1@example.com", "foo@example.com", self.msg) + self.reject("user1@example.com") + self.check_that_moderation_box_is_empty() + + def testOutsiderPostsToMderatedApproved(self): + self.configure_list("free", "moderated") + self.check_that_moderation_box_is_empty() + self.send("outsider@example.com", "foo@example.com", self.msg) + self.approve("outsider@example.com") + self.check_mail_to_list() + self.check_that_moderation_box_is_empty() + + def testSubscriberPostsWithRequestToBeModerated(self): + self.configure_list("free", "free") + + self.check_that_moderation_box_is_empty() + self.send("user1@example.com", "foo@example.com", self.msg, + force_moderation=1) + self.match("foo-ignore@example.com", + None, + "user1@example.com", + "Subject: Please wait") + a = self.match("foo-ignore@example.com", + "foo-approve-[^@]*@example.com", + "listmaster@example.com") + self.no_more_mail() + + self.send("listmaster@example.com", a) + self.check_mail_to_list() + self.check_that_moderation_box_is_empty() + + def testSubscriberPostsWithModerationOverride(self): + self.configure_list("moderated", "moderated") + self.send("user1@example.com", "foo@example.com", self.msg, + force_posting=1) + self.check_mail_to_list() + self.check_that_moderation_box_is_empty() + +class BounceTestCases(IncomingBase): + + def check_subscriber_status(self, must_be): + ml = self.mlm.open_list("foo@example.com") + for id in ml.subscribers.groups(): + self.failUnlessEqual(ml.subscribers.get(id, "status"), must_be) + + def bounce_sent_mail(self): + for m in self.sent_mail[:]: + self.send("something@example.com", m["sender"], "eek") + self.failUnlessEqual(len(self.sent_mail), 0) + + def send_mail_to_list_then_bounce_everything(self): + self.send("user@example.com", "foo@example.com", "hello") + for m in self.sent_mail[:]: + self.send("foo@example.com", m["sender"], "eek") + self.failUnlessEqual(len(self.sent_mail), 0) + + def testBounceOnceThenRecover(self): + self.check_subscriber_status("ok") + self.send_mail_to_list_then_bounce_everything() + + self.check_subscriber_status("bounced") + + ml = self.mlm.open_list("foo@example.com") + for id in ml.subscribers.groups(): + bounce_id = ml.subscribers.get(id, "bounce-id") + self.failUnless(bounce_id) + self.failUnless(ml.bounce_box.has(bounce_id)) + + bounce_ids = [] + now = time.time() + ml = self.mlm.open_list("foo@example.com") + ml.subscribers.lock() + for id in ml.subscribers.groups(): + timestamp = float(ml.subscribers.get(id, "timestamp-bounced")) + self.failUnless(abs(timestamp - now) < 10.0) + ml.subscribers.set(id, "timestamp-bounced", "69.0") + bounce_ids.append(ml.subscribers.get(id, "bounce-id")) + ml.subscribers.save() + + self.mlm.cleaning_woman(no_op) + self.check_subscriber_status("probed") + + for bounce_id in bounce_ids: + self.failUnless(ml.bounce_box.has(bounce_id)) + + self.mlm.cleaning_woman(no_op) + ml = self.mlm.open_list("foo@example.com") + self.failUnlessEqual(len(ml.subscribers.groups()), 2) + self.check_subscriber_status("ok") + for bounce_id in bounce_ids: + self.failUnless(not ml.bounce_box.has(bounce_id)) + + def testBounceProbeAlso(self): + self.check_subscriber_status("ok") + self.send_mail_to_list_then_bounce_everything() + self.check_subscriber_status("bounced") + + ml = self.mlm.open_list("foo@example.com") + for id in ml.subscribers.groups(): + bounce_id = ml.subscribers.get(id, "bounce-id") + self.failUnless(bounce_id) + self.failUnless(ml.bounce_box.has(bounce_id)) + + bounce_ids = [] + now = time.time() + ml = self.mlm.open_list("foo@example.com") + ml.subscribers.lock() + for id in ml.subscribers.groups(): + timestamp = float(ml.subscribers.get(id, "timestamp-bounced")) + self.failUnless(abs(timestamp - now) < 10.0) + ml.subscribers.set(id, "timestamp-bounced", "69.0") + bounce_ids.append(ml.subscribers.get(id, "bounce-id")) + ml.subscribers.save() + + self.sent_mail = [] + self.mlm.cleaning_woman(self.catch_sendmail) + self.check_subscriber_status("probed") + for bounce_id in bounce_ids: + self.failUnless(ml.bounce_box.has(bounce_id)) + self.bounce_sent_mail() + self.check_subscriber_status("probebounced") + + self.mlm.cleaning_woman(no_op) + ml = self.mlm.open_list("foo@example.com") + self.failUnlessEqual(len(ml.subscribers.groups()), 0) + for bounce_id in bounce_ids: + self.failUnless(not ml.bounce_box.has(bounce_id)) + + def testCleaningWomanJoinsAndBounceSplitsGroups(self): + # Check that each group contains one address and set the creation + # timestamp to an ancient time. + ml = self.mlm.open_list("foo@example.com") + bouncedir = os.path.join(ml.dirname, "bounce-box") + ml.subscribers.lock() + for id in ml.subscribers.groups(): + addrs = ml.subscribers.in_group(id) + self.failUnlessEqual(len(addrs), 1) + bounce_id = ml.subscribers.get(id, "bounce-id") + self.failUnlessEqual(bounce_id, "..notexist..") + bounce_id = "bounce-" + id + ml.subscribers.set(id, "bounce-id", bounce_id) + bounce_path = os.path.join(bouncedir, bounce_id) + self.failUnless(not os.path.isfile(bounce_path)) + f = open(bounce_path, "w") + f.close() + f = open(bounce_path + ".address", "w") + f.close() + self.failUnless(os.path.isfile(bounce_path)) + ml.subscribers.set(id, "timestamp-created", "1") + ml.subscribers.save() + + # Check that --cleaning-woman joins the two groups into one. + self.failUnlessEqual(len(ml.subscribers.groups()), 2) + self.mlm.cleaning_woman(no_op) + ml = self.mlm.open_list("foo@example.com") + self.failUnlessEqual(len(ml.subscribers.groups()), 1) + self.failUnlessEqual(os.listdir(bouncedir), []) + + # Check that a bounce splits the single group. + self.send_mail_to_list_then_bounce_everything() + ml = self.mlm.open_list("foo@example.com") + self.failUnlessEqual(len(ml.subscribers.groups()), 2) + + # Check that a --cleaning-woman immediately after doesn't join. + # (The groups are new, thus shouldn't be joined for a week.) + self.failUnlessEqual(len(ml.subscribers.groups()), 2) + self.mlm.cleaning_woman(no_op) + ml = self.mlm.open_list("foo@example.com") + self.failUnlessEqual(len(ml.subscribers.groups()), 2) diff --git a/fix-config b/fix-config new file mode 100755 index 0000000..7ca6524 --- /dev/null +++ b/fix-config @@ -0,0 +1,4 @@ +#!/bin/sh + +sed 's:^TEMPLATE_DIRS *=.*:TEMPLATE_DIRS = ["'$1'"]:' | +sed 's:^DOTDIR *=.*:DOTDIR = os.path.expanduser("~/.enemies-of-carlotta"):' diff --git a/qmqp.py b/qmqp.py new file mode 100644 index 0000000..bc5d194 --- /dev/null +++ b/qmqp.py @@ -0,0 +1,137 @@ +#!/usr/bin/python + +# This module that implements sending mail via QMQP. See +# +# http://cr.yp.to/proto/qmqp.html +# +# for a description of the protocol. +# +# This module was written by Jaakko Niemi for +# Enemies of Carlotta. It is licensed the same way as Enemies of Carlotta: +# GPL version 2. + +import socket, string + +class QMQPException(Exception): + '''Base class for all exceptions raised by this module.''' + +class QMQPTemporaryError(QMQPException): + '''Class for temporary errors''' + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return "QMQP-Server said: %s" % self.msg + +class QMQPPermanentError(QMQPException): + '''Class for permanent errors''' + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return "QMQP-Server said: %s" % self.msg + +class QMQPConnectionError(QMQPException): + '''Class for connection errors''' + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return "Error was: %s" % self.msg + +class QMQP: + '''I handle qmqp connection to a server''' + + file = None + + def __init__(self, host = 'localhost'): + '''Start''' + if host: + resp = self.connect(host) + + def encode(self, stringi): + ret = '%d:%s,' % (len(stringi), stringi) + return ret + + def decode(self, stringi): + stringi = string.split(stringi, ':', 1) + stringi[1] = string.rstrip(stringi[1], ',') + if len(stringi[1]) is not int(stringi[0]): + print 'malformed netstring encounterd' + return stringi[1] + + def connect(self, host = 'localhost'): + for sres in socket.getaddrinfo(host, 628, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = sres + try: + self.sock = socket.socket(af, socktype, proto) + self.sock.connect(sa) + except socket.error: + print 'connect failed' + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error + return 0 + + def send(self, stringi): + if self.sock: + try: + self.sock.sendall(stringi) + except socket.error, err: + self.sock.close() + raise QMQPConnectionError, err + else: + print 'not connected' + + def getreply(self): + if self.file is None: + self.file = self.sock.makefile('rb') + while 1: + line = self.file.readline() + if line == '': + self.sock.close() + print 'ERORORR' + break + line = self.decode(line) + return line + + def quit(self): + self.sock.close() + + def sendmail(self, from_addr, to_addrs, msg): + recipients = '' + msg = self.encode(msg) + from_addr = self.encode(from_addr) + +# I don't understand why len(to_addrs) <= 1 needs to be handled differently. +# Anyway, it doesn't seem to work with Postfix. --liw +# if len(to_addrs) > 1: +# for t in to_addrs: +# recipients = recipients + self.encode(t) +# else: +# recipients = self.encode(to_addrs[0]) + + for t in to_addrs: + recipients = recipients + self.encode(t) + output = self.encode(msg + from_addr + recipients) + self.send(output) + ret = self.getreply() + if ret[0] == 'K': + return ret[1:] + if ret[0] == 'Z': + raise QMQPTemporaryError, ret[1:] + if ret[0] == 'D': + raise QMQPPermanentError, ret[1:] + +if __name__ == '__main__': + a = QMQP() + maili = 'asfasdfsfdasfasd' + envelope_sender = 'liw@liw.iki.fi' + recips = [ 'liw@liw.iki.fi' ] + retcode = a.sendmail(envelope_sender, recips, maili) + print retcode + a.quit() diff --git a/templates/bounce-goodbye b/templates/bounce-goodbye new file mode 100644 index 0000000..5d5f7cd --- /dev/null +++ b/templates/bounce-goodbye @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Your mail bounces, goodbye from %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +You have been a subscriber to the %(list)s mailing list. + +Unfortunately, mail sent to you by the mailing list manager has bounced +so much that you have been automatically removed. Once your mail problems +are solved, feel free to re-subscribe. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. diff --git a/templates/bounce-goodbye.es b/templates/bounce-goodbye.es new file mode 100644 index 0000000..59245fd --- /dev/null +++ b/templates/bounce-goodbye.es @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Su mensaje rebota, adiós desde %(list)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Hasta ahora ha sido suscriptor de la lista %(list)s. + +Desafortunadamente, ha rebotado tanto un mensaje que le envió el +administrador de la lista que ha sido desuscrito de forma automática. +Una vez corrija sus problemas de correo, será bienvenido a resuscribirse. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +Gracias. diff --git a/templates/bounce-goodbye.fi b/templates/bounce-goodbye.fi new file mode 100644 index 0000000..9c707a8 --- /dev/null +++ b/templates/bounce-goodbye.fi @@ -0,0 +1,16 @@ +From: %(From)s +To: %(To)s +Subject: Postisi ei toimi, näkemiin listalta %(list)s +Content-type: text/plain; charset=utf-8 + +Olet ollut tilaajana %(list)s -listalla. + +Valitettavasti sinulle lähetetyt sähköpostit aiheuttavat virheilmoituksia +siinä määrin, että sinut on nyt automaattisesti poistettu listalta. Kunhan +sähköpostiongelmasi on korjattu, olet tervetullut takaisin tilaajaksi. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . diff --git a/templates/bounce-goodbye.fr b/templates/bounce-goodbye.fr new file mode 100644 index 0000000..a6f09e6 --- /dev/null +++ b/templates/bounce-goodbye.fr @@ -0,0 +1,23 @@ +From: %(From)s +To: %(To)s +Subject: Votre courriel ne peut être délivré, vous êtes désabonné de la liste %(list)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Vous vous étiez abonné à la liste de diffusion %(list)s. + +Malheureusement, les courriels qui vous ont été envoyés par le gestionnaire +de la liste de diffusion ne sont jamais arrivés à destination. Ce phénomène +s'étant reproduit un grand nombre de fois, vous avez été automatiquement +désabonné de la liste. Une fois que vos problèmes de courriels seront +corrigés, n'hésitez pas à vous réabonner. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. diff --git a/templates/bounce-goodbye.sv b/templates/bounce-goodbye.sv new file mode 100644 index 0000000..b2195d7 --- /dev/null +++ b/templates/bounce-goodbye.sv @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Prenumerationen på %(list)s avbruten p.g.a. för många studsar +Content-type: text/plain; charset=utf-8 + +Hej! + +Du har prenumererat på epostlistan %(list)s. + +Tyvärr har post skickad till dig av epostlistehanteraren studsat +så mycket att du har blivit automatiskt borttagen. När problemen +är lösta är du välkommen att prenumerera på nytt. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/bounce-owner-notification b/templates/bounce-owner-notification new file mode 100644 index 0000000..441efc3 --- /dev/null +++ b/templates/bounce-owner-notification @@ -0,0 +1,35 @@ +From: %(From)s +To: %(To)s +Subject: Address removed from %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the + + %(list)s + +mailing list to the human owners of the list. + +The following address has been removed from the list due to bouncing: + + %(address)s + +The bounce message is attached at the bottom of this mail. Unless there is +something really strange going on, this mail is only for your information +and you need take no action. + +Thanks. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-owner-notification.es b/templates/bounce-owner-notification.es new file mode 100644 index 0000000..3827372 --- /dev/null +++ b/templates/bounce-owner-notification.es @@ -0,0 +1,36 @@ +From: %(From)s +To: %(To)s +Subject: Dirección eliminada de %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +El gestor que opera la lista de correo + + %(list)s + + +ha enviado este mensaje a los operadores humanos de la lista. + +La siguiente dirección ha sido eliminada de la lista debido a rebotes: + + %(address)s + +Al final de este mensaje se adjunta el mensaje que rebotó. A menos que +esté pasando algo realmente extraño, este mensaje es sólo informativo, y +no necesita tomar ninguna acción al respecto. + +Gracias. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-owner-notification.fi b/templates/bounce-owner-notification.fi new file mode 100644 index 0000000..ef959d1 --- /dev/null +++ b/templates/bounce-owner-notification.fi @@ -0,0 +1,30 @@ +From: %(From)s +To: %(To)s +Subject: Osoite poistettu listalta %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Tämän meilin on lähettänyt postituslistaohjelmisto, joka hoitaa +listaa + + %(list)s + +listan omistajille. Osoite + + %(address)s + +on poistettu listalta toimimattomuuden takia. Virheilmoitus on tämän +viestin lopussa. Tämä meili on vain tiedoksi, jos mitään erityisen +omituista ei ole tapahtumassa. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-owner-notification.fr b/templates/bounce-owner-notification.fr new file mode 100644 index 0000000..80a911a --- /dev/null +++ b/templates/bounce-owner-notification.fr @@ -0,0 +1,36 @@ +From: %(From)s +To: %(To)s +Subject: Adresse supprimée de la liste %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion + + %(list)s + +aux personnes qui sont responsables de la liste. + +L'adresse suivante a été enlevée de la liste en raison de problème dans +la livraison du courriel destiné à : + + %(address)s + +Le message qui n'a pu être délivré se trouve en bas de ce courriel. À +moins qu'il n'y ait quelque chose de vraiment particulier, ce courriel n'est +présent qu'à titre informatif; vous n'avez pas à vous en souciez. + +Merci. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-owner-notification.sv b/templates/bounce-owner-notification.sv new file mode 100644 index 0000000..754d3b1 --- /dev/null +++ b/templates/bounce-owner-notification.sv @@ -0,0 +1,35 @@ +From: %(From)s +To: %(To)s +Subject: Adress borttagen från %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter listan + + %(list)s + +till listans ägare. + +Följande adress har tagits bort från listan p.g.a. att posten studsar: + + %(address)s + +Studsmeddelandet finns i slutet av det här brevet. Såvida inte något +riktigt märkligt pågår är det här brevet bara till för att informera +dig och du behöver inte vidta någon åtgärd. + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-warning b/templates/bounce-warning new file mode 100644 index 0000000..dc98901 --- /dev/null +++ b/templates/bounce-warning @@ -0,0 +1,37 @@ +From: %(From)s +To: %(To)s +Subject: Your mail is bouncing, %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=us-ascii + +Hello, + +You are subscribed to the %(list)s mailig list. + +Mail sent to you by the mailing list software has bounced at least once +in the past week or so. If this was a temporary error, you can ignore +this warning. However, if your mail continues to bounce, you will +eventually be automatically unsubscribed from the list. Sorry about +the inconvenience. + +The first bounce message is attached. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-warning.es b/templates/bounce-warning.es new file mode 100644 index 0000000..c39c34c --- /dev/null +++ b/templates/bounce-warning.es @@ -0,0 +1,34 @@ +From: %(From)s +To: %(To)s +Subject: Sus mensajes están rebotando, %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Usted es suscriptor de la lista de correo %(list)s. + +Al menos uno de los mensajes que le envió la lista durante la última +semana rebotó. Si fue un problema temporal, puede ignorar este aviso. +Sin embargo, si su correo continúa rebotando, acabará siendo desuscrito +de forma automática de esta lista. Sentimos el inconveniente. + +Más abajo encontrará el primer mensaje que rebotó. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-warning.fi b/templates/bounce-warning.fi new file mode 100644 index 0000000..83a18f1 --- /dev/null +++ b/templates/bounce-warning.fi @@ -0,0 +1,32 @@ +From: %(From)s +To: %(To)s +Subject: Postisi aiheuttaa virheilmoituksia, %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Olet tilaajana listalla %(list)s . + +Sinulle lähetetty sähköposti on aiheuttanut ainakin yhden virheilmoituksen +viimeisen viikon aikana. Jos tämä oli tilapäinen ongelma, voit unohtaa +koko asian. Jos virheet kuitenkin jatkuvat, sinut poistetaan lopulta +listalta. Valitamme. + +Ensimmäinen virheilmoitus on tämän viestin lopussa. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-warning.fr b/templates/bounce-warning.fr new file mode 100644 index 0000000..2b3bb91 --- /dev/null +++ b/templates/bounce-warning.fr @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Subject: Votre courriel ne peut être délivré, %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Vous êtes abonné à la liste de diffusion %(list)s . + +Les courriels qui vous ont été envoyés par le gestionnaire +de la liste de diffusion ne sont jamais arrivés à destination. +Ce phénomène s'est produit au moins une fois depuis la semaine dernière. +S'il s'agit d'une erreur temporaire, vous pouvez ignorer ce message +d'avertissement. Cependant, si les échecs se reproduisent, il se peut +que vous soyez automatiquement désabonné de la liste de diffusion. Nous +en sommes désolé. + +Le premier message qui n'a pu être délivré est cité ci-dessous. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/bounce-warning.sv b/templates/bounce-warning.sv new file mode 100644 index 0000000..c16f111 --- /dev/null +++ b/templates/bounce-warning.sv @@ -0,0 +1,37 @@ +From: %(From)s +To: %(To)s +Subject: Din epost studsar, %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Du prenumererar på epostlistan %(list)s. + +Epost skickad till dig av epostlistehanteraren har studsat åtminstone +en gång under den senaste veckan eller så. Om detta berodde på ett +tillfälligt fel kan du bortse från den här varningen. Fortsätter dina +brev att studsa kommer du i slutändan automatiskt att bli borttagen +från listan över prenumeranter. Vi beklagar detta. + +Den första studsen återfinns nedan. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=bounce.txt + +%(bounce)s--%(boundary)s-- + diff --git a/templates/footer b/templates/footer new file mode 100644 index 0000000..697ef92 --- /dev/null +++ b/templates/footer @@ -0,0 +1,3 @@ + +-- +To unsubscribe, send mail to %(local)s-unsubscribe@%(domain)s. diff --git a/templates/footer.es b/templates/footer.es new file mode 100644 index 0000000..99458e5 --- /dev/null +++ b/templates/footer.es @@ -0,0 +1,3 @@ + +-- +Para desuscribirse, mande un mensaje a %(local)s-unsubscribe@%(domain)s diff --git a/templates/footer.fi b/templates/footer.fi new file mode 100644 index 0000000..f88f90f --- /dev/null +++ b/templates/footer.fi @@ -0,0 +1,3 @@ + +-- +Poistu listalta: %(local)s-unsubscribe@%(domain)s . diff --git a/templates/footer.fr b/templates/footer.fr new file mode 100644 index 0000000..43b584e --- /dev/null +++ b/templates/footer.fr @@ -0,0 +1,3 @@ + +-- +Pour ne plus recevoir de message : mailto:%(local)s-unsubscribe@%(domain)s diff --git a/templates/footer.sv b/templates/footer.sv new file mode 100644 index 0000000..e3f3557 --- /dev/null +++ b/templates/footer.sv @@ -0,0 +1,4 @@ + +-- +Om du vill avsluta prenumerationen, skicka ett brev till +%(local)s-unsubscribe@%(domain)s. diff --git a/templates/help b/templates/help new file mode 100644 index 0000000..9ebe603 --- /dev/null +++ b/templates/help @@ -0,0 +1,38 @@ +From: %(From)s +To: %(To)s +Subject: Help for the %(list)s mailing list +Content-type: text/plain; charset=us-ascii + +Hello, + +this is the help text for the %(list)s mailing list. + +The list is operated by the EoC mailing list manager. It understands +the following command addresses: + + %(local)s-help@%(domain)s + + Sends this help text. + + %(local)s-subscribe@%(domain)s + + Subscribe to the list. You will get a confirmation request. + + %(local)s-subscribe-foo=bar@%(domain)s + + Subscribe the address foo@bar to the list. foo@bar will get the + confirmation request. + + %(local)s-unsubscribe@%(domain)s + + Unsubscribe from the list. You will get a confirmation request. + + %(local)s-unsubscribe-foo=bar@%(domain)s + + Unsubscribe the address foo@bar from the list. foo@bar will get the + confirmation request. + +If you have problems that are not solved by this help text, please contact +the human owner of the list at %(local)s-owner@%(domain)s. + +Thank you. diff --git a/templates/help.es b/templates/help.es new file mode 100644 index 0000000..3836db1 --- /dev/null +++ b/templates/help.es @@ -0,0 +1,39 @@ +From: %(From)s +To: %(To)s +Subject: Ayuda de la lista de correo %(list)s +Content-type: text/plain; charset=utf-8 + +Hola, + +éste es el texto de ayuda de la lista de correo %(list)s. + +La lista la lleva el gestor de listas de correo EoC, que +comprende las siguientes órdenes mediante direcciones de correo: + + %(local)s-help@%(domain)s + + Envía este texto de ayuda. + + %(local)s-subscribe@%(domain)s + + Suscribirse a la lista. Recibirá una petición de confirmación. + + %(local)s-subscribe-foo=bar@%(domain)s + + Suscribir la dirección foo@bar a la lista. foo@bar recibirá la + petición de confirmación. + + %(local)s-unsubscribe@%(domain)s + + Desuscribirse de la lista. Recibirá una petición de confirmación. + + %(local)s-unsubscribe-foo=bar@%(domain)s + + Desuscribir la dirección foo@bar de la lista. foo@bar recibirá la + petición de confirmación. + +Si tiene algún problema que no resuelva este texto de ayuda, póngase en +contacto por favor con la persona que supervisa la lista en +%(local)s-owner@%(domain)s . + +Gracias. diff --git a/templates/help.fi b/templates/help.fi new file mode 100644 index 0000000..bb41161 --- /dev/null +++ b/templates/help.fi @@ -0,0 +1,34 @@ +From: %(From)s +To: %(To)s +Subject: Ohjeita listalle %(list)s +Content-type: text/plain; charset=utf-8 + +Tämä on postituslistan %(list)s ohjeteksti. + +Listaa pyörittää postituslistaohjelmisto nimeltä EoC. Se ymmärtää +seuraavat komento-osoitteet: + + %(local)s-help@%(domain)s + + Lähettää tämän ohjetekstin. + + %(local)s-subscribe@%(domain)s + + Tilauspyyntö. Vastauksena tulee vahvistuspyyntö. + + %(local)s-subscribe-foo=bar@%(domain)s + + Tilauspyyntö siten, että listalle lisätään osoite foo@bar . + foo@bar saa myös vahvistuspyynnön. + + %(local)s-unsubscribe@%(domain)s + + Listalta poistumispyyntö. Tämäkin pitää vahvistaa. + + %(local)s-unsubscribe-foo=bar@%(domain)s + + Pyyntö poistaa foo@bar listalta. foo@bar saa vahvistuspyynnön. + +Jos sinulla on ongelmia, jotka eivät ratkea tämän ohjetekstin avulla, +voit kysyä apua listan omistajilta (jotka ovat ihmisiä) osoitteesta +%(local)s-owner@%(domain)s. diff --git a/templates/help.fr b/templates/help.fr new file mode 100644 index 0000000..6731007 --- /dev/null +++ b/templates/help.fr @@ -0,0 +1,41 @@ +From: %(From)s +To: %(To)s +Subject: Message d'aide de la liste de diffusion %(list)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Voici le message d'aide pour la liste de diffusion %(list)s. + +La liste fonctionne grâce au gestionnaire de liste de diffusion +« EoC ». Il est capable de traiter les commandes suivantes : + + %(local)s-help@%(domain)s + + Envoie ce message d'aide. + + %(local)s-subscribe@%(domain)s + + Vous abonne à la liste. Vous recevrez une demande de + confirmation. + + %(local)s-subscribe-foo=bar@%(domain)s + + Abonne l'adresse foo@bar à la liste de diffusion. foo@bar + recevra une demande de confirmation. + + %(local)s-unsubscribe@%(domain)s + + Vous désabonne de la liste de diffusion. Vous recevrez une + demande de confirmation. + + %(local)s-unsubscribe-foo=bar@%(domain)s + + Désabonne l'adresse foo@bar de la liste de diffusion. foo@bar + recevra une demande de confirmation. + +Si vous rencontrez des problèmes qui ne sont pas traités dans ce message, +veuillez prendre contact avec la personne responsable de la liste de +diffusion à l'adresse : %(local)s-owner@%(domain)s. + +Merci. diff --git a/templates/help.sv b/templates/help.sv new file mode 100644 index 0000000..0a1a84c --- /dev/null +++ b/templates/help.sv @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Subject: Hjälp för epostlistan %(list)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Detta är hjälptexten till epostlistan %(list)s. + +Listan sköts av epostlistehanteraren EoC. Den förstår följande +kommandoadresser: + + %(local)s-help@%(domain)s + + Skickar den här hjälptexten. + + %(local)s-subscribe@%(domain)s + + Prenumerera på listan. Du kommer att få ett meddelande som + ber dig bekräfta att du vill prenumerera. + + %(local)s-subscribe-foo=bar@%(domain)s + + Anmäl adressen foo@bar som prenumerant. foo@bar kommer att + få en bekräftelseförfrågan. + + %(local)s-unsubscribe@%(domain)s + + Säg upp prenumerationen på listan. Du kommer att bli ombedd + att bekräfta. + + %(local)s-unsubscribe-foo=bar@%(domain)s + + Säg upp prenumerationen för foo@bar. foo@bar kommer att få + bekräfta. + +Om du har problem som inte den här hjälptexten kan lösa, kontakta +listans ägare på adressen %(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/list b/templates/list new file mode 100644 index 0000000..242b9d6 --- /dev/null +++ b/templates/list @@ -0,0 +1,10 @@ +From: %(From)s +To: %(To)s +Subject: Subscribers for %(list)s +Content-type: text/plain; charset=us-ascii + +Subscribers for the %(list)s list, as requested. + +%(addresses)s + +Total: %(count)s addresses. diff --git a/templates/list-sorry b/templates/list-sorry new file mode 100644 index 0000000..a57f88e --- /dev/null +++ b/templates/list-sorry @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Subscriber list denied for %(list)s +Content-type: text/plain; charset=us-ascii + +Sorry, you are not the list owner for %(list)s. diff --git a/templates/list-sorry.es b/templates/list-sorry.es new file mode 100644 index 0000000..483bd4f --- /dev/null +++ b/templates/list-sorry.es @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Lista de suscriptores de %(list)s denegada +Content-type: text/plain; charset=utf-8 + +Lo siento, no está en la lista de dueños de %(list)s . diff --git a/templates/list-sorry.fi b/templates/list-sorry.fi new file mode 100644 index 0000000..445495a --- /dev/null +++ b/templates/list-sorry.fi @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Et saa tilaajaluetteloa listalle %(list)s +Content-type: text/plain; charset=utf-8 + +Valitan, et ole listan %(list)s omistaja. diff --git a/templates/list-sorry.fr b/templates/list-sorry.fr new file mode 100644 index 0000000..73b106f --- /dev/null +++ b/templates/list-sorry.fr @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Impossible d'obtenir la liste des abonnés à %(list)s +Content-type: text/plain; charset=utf-8 + +Désolé, vous n'êtes pas le responsable de la liste %(list)s. diff --git a/templates/list-sorry.sv b/templates/list-sorry.sv new file mode 100644 index 0000000..b23e9ad --- /dev/null +++ b/templates/list-sorry.sv @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Begäran om prenumerantlista för %(list)s nekad +Content-type: text/plain; charset=iso-8859-1 + +Tyvärr, du är inte ägaren till listan %(list)s. diff --git a/templates/list.es b/templates/list.es new file mode 100644 index 0000000..de5e4e7 --- /dev/null +++ b/templates/list.es @@ -0,0 +1,10 @@ +From: %(From)s +To: %(To)s +Subject: Suscriptores de la lista %(list)s +Content-type: text/plain; charset=utf-8 + +Tal como me pidió, estos son los suscriptores de la lista %(list)s . + +%(addresses)s + +Total: %(count)s direcciones. diff --git a/templates/list.fi b/templates/list.fi new file mode 100644 index 0000000..39c24e5 --- /dev/null +++ b/templates/list.fi @@ -0,0 +1,10 @@ +From: %(From)s +To: %(To)s +Subject: Tilaajaluettelo: %(list)s +Content-type: text/plain; charset=utf-8 + +Pyynnöstä luettelen listan %(list)s tilaajat. + +%(addresses)s + +Yhteensä %(count)s osoitetta. diff --git a/templates/list.fr b/templates/list.fr new file mode 100644 index 0000000..7a553bd --- /dev/null +++ b/templates/list.fr @@ -0,0 +1,11 @@ +From: %(From)s +To: %(To)s +Subject: Liste des abonnés pour la liste %(list)s +Content-type: text/plain; charset=utf-8 + +Voici la liste des abonnés à la liste de diffusion +%(list)s + +%(addresses)s + +Total: %(count)s adresses. diff --git a/templates/list.sv b/templates/list.sv new file mode 100644 index 0000000..c40a896 --- /dev/null +++ b/templates/list.sv @@ -0,0 +1,10 @@ +From: %(From)s +To: %(To)s +Subject: Prenumeranter på %(list)s +Content-type: text/plain; charset=utf-8 + +Här är listan över prenumeranter på listan %(list)s, enligt begäran. + +%(addresses)s + +Totalt: %(count)s adresser. diff --git a/templates/msg-moderate b/templates/msg-moderate new file mode 100644 index 0000000..f188f05 --- /dev/null +++ b/templates/msg-moderate @@ -0,0 +1,37 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Please moderate message to %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the + + %(list)s + +mailing list to the human moderators of the list. + +Should the message at the bottom be allowed to be sent to the list? +If so, reply to this mail or send mail to + + %(confirm)s + +If not, send mail to + + %(deny)s + +Thanks. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/msg-moderate.es b/templates/msg-moderate.es new file mode 100644 index 0000000..303befd --- /dev/null +++ b/templates/msg-moderate.es @@ -0,0 +1,37 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Por favor, modere este mensaje enviado a %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje lo ha enviado el gestor de listas de correo que lleva + + %(list)s + +a los humanos que la supervisan. + +¿Debería permitirse enviar a la lista el mensaje que sigue? En caso +afirmativo, responda a este mensaje o envíe un mensaje a + + %(confirm)s + +Si no, envíe un mensaje a + + %(deny)s + +Gracias. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/msg-moderate.fi b/templates/msg-moderate.fi new file mode 100644 index 0000000..462d16f --- /dev/null +++ b/templates/msg-moderate.fi @@ -0,0 +1,35 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Hyväksy viesti listalle %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa + + %(list)s + +käsittelevä ohjelmisto listan omistajille. + +Pitäisikö alla oleva viesti hyväksyä ja lähettää kaikille tilaajille? +Jos pitäisi, vastaa tähän viestiin tai lähetä viesti osoitteeseen + + %(confirm)s + +Jos ei pitäisi, lähetä viesti osoitteeseen + + %(deny)s + +Kiitos. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/msg-moderate.fr b/templates/msg-moderate.fr new file mode 100644 index 0000000..ae77cd0 --- /dev/null +++ b/templates/msg-moderate.fr @@ -0,0 +1,39 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Veuillez modérer le message pour %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion + + %(list)s + +aux personnes qui sont responsables de la liste. + + +Le message qui ce trouve à la fin doit-il être envoyé à la liste +de diffusion ? +Si oui, répondez à ce message ou envoyez un courriel à + + %(confirm)s + +Si non, envoyez un courriel à + + %(deny)s + +Merci. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/msg-moderate.sv b/templates/msg-moderate.sv new file mode 100644 index 0000000..268fd80 --- /dev/null +++ b/templates/msg-moderate.sv @@ -0,0 +1,37 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Var god moderera meddelande till %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter listan + + %(list)s + +till listans ägare. + +Ska meddelandet som visas nedan få skickas till listan? +I så fall, svara på det här brevet eller skicka ett tomt meddelande till + + %(confirm)s + +Om inte, skicka ett meddelande till + + %(deny)s + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/msg-wait b/templates/msg-wait new file mode 100644 index 0000000..12d41b0 --- /dev/null +++ b/templates/msg-wait @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Please wait for moderation to %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +Your message to the list has been sent to the moderators for approval. +This might take a while. Please be patient. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. diff --git a/templates/msg-wait.es b/templates/msg-wait.es new file mode 100644 index 0000000..99d6993 --- /dev/null +++ b/templates/msg-wait.es @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Por favor, aguarde a la moderación de %(list)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje se lo ha enviado el gestor de listas de correo que opera +sobre %(list)s . + +He enviado a los moderadores el mensaje que envió a la lista para que +lo aprueben. Esto puede tardar un poco. Por favor, sea paciente. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +Gracias. diff --git a/templates/msg-wait.fi b/templates/msg-wait.fi new file mode 100644 index 0000000..6fa2b8f --- /dev/null +++ b/templates/msg-wait.fi @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Odota omistajan vahvistusta: %(list)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Listalle lähettämäsi viesti on lähetetty listan omistajille hyväksyntää +varten. Tämä voi kestää hetken. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . + diff --git a/templates/msg-wait.fr b/templates/msg-wait.fr new file mode 100644 index 0000000..2050a0f --- /dev/null +++ b/templates/msg-wait.fr @@ -0,0 +1,22 @@ +From: %(From)s +To: %(To)s +Subject: Veuillez attendre la modération pour votre envoi sur %(list)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion +%(list)s . + +Votre message à destination de la liste a été envoyé aux modérateurs +pour qu'ils l'approuvent. Cela peut prendre un certain temps. Soyez +patient. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. diff --git a/templates/msg-wait.sv b/templates/msg-wait.sv new file mode 100644 index 0000000..6c028da --- /dev/null +++ b/templates/msg-wait.sv @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Var god vänta på moderering av din postning till %(list)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter epostlistan +%(list)s. + +Ditt meddelande till listan har skickats till moderatorerna för god- +kännande. Det kan ta ett litet tag. Ha tålamod! + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/setlist-badlist b/templates/setlist-badlist new file mode 100644 index 0000000..1cb161c --- /dev/null +++ b/templates/setlist-badlist @@ -0,0 +1,33 @@ +From: %(From)s +To: %(To)s +Subject: Bad address list for %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +The address list you sent was syntactically incorrect. You need to have +exactly one address per line, and nothing else. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-badlist.es b/templates/setlist-badlist.es new file mode 100644 index 0000000..314d182 --- /dev/null +++ b/templates/setlist-badlist.es @@ -0,0 +1,33 @@ +From: %(From)s +To: %(To)s +Subject: Dirección incorrecta para %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje se lo ha enviado el gestor de listas de correo que opera +sobre %(list)s . + +La sintaxis de la dirección de lista a la que ha enviado el mensaje es +incorrecta. Debe haber una única dirección por línea, y nada más. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +Gracias. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-badlist.fi b/templates/setlist-badlist.fi new file mode 100644 index 0000000..d4843ab --- /dev/null +++ b/templates/setlist-badlist.fi @@ -0,0 +1,29 @@ +From: %(From)s +To: %(To)s +Subject: Virheelinen tilaajalista listalle %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Lähettämäsi osoiteluettelo oli muodollisesti epäpätevä. Luettelossa +pitää olla täsmälleen yksi osoite per rivi, eikä muuta. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-badlist.fr b/templates/setlist-badlist.fr new file mode 100644 index 0000000..010a6ba --- /dev/null +++ b/templates/setlist-badlist.fr @@ -0,0 +1,35 @@ +From: %(From)s +To: %(To)s +Subject: Mauvaise adresse de liste pour %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion +%(list)s . + +L'adresse à laquelle vous avez envoyé ce message contenait une erreur de +syntaxe. Vous devez expressément avoir une adresse par ligne, et rien +d'autre. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-badlist.sv b/templates/setlist-badlist.sv new file mode 100644 index 0000000..36c2a99 --- /dev/null +++ b/templates/setlist-badlist.sv @@ -0,0 +1,33 @@ +From: %(From)s +To: %(To)s +Subject: Felaktig adresslista för %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter listan +%(list)s. + +Adresslistan du skickade är syntaktiskt felaktig. Du måste skriva exakt +en adress per rad och inget annat. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-confirm b/templates/setlist-confirm new file mode 100644 index 0000000..c2577de --- /dev/null +++ b/templates/setlist-confirm @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Please moderate subscriber list for %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +One of the owners of the list (or someone pretending to be one) has asked +for the subscriber list for the list to be changed. The request mail, +with the new set of describers, is attached. If you accept the +change, please respond to this mail. Otherwise please just ignore it. + +Note that the whole subscriber list will be changed to the list of +addresses below. All old subscriptions will be forgotten, unless they +are also on the new list of subscribers. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-confirm.es b/templates/setlist-confirm.es new file mode 100644 index 0000000..55a280f --- /dev/null +++ b/templates/setlist-confirm.es @@ -0,0 +1,41 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Por favor, modere la lista de suscriptores de %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje se lo ha enviado el gestor de listas de correo que opera +sobre %(list)s . + +Uno de los miembros de la lista (o alguien que pretende serlo) ha pedido +que se cambie el registro de suscriptores. Se incluye más adelante el +mensaje de la petición, con el nuevo conjunto de suscriptores. Si acepta +el cambio, responda a este mensaje. En caso contrario, limítese a +ignorarlo. + +Tenga en cuenta que se cambiará la lista de suscriptores completa por la +lista que aparece debajo. Olvidaré todos los suscriptores antiguos a menos +que también estén en la lista nueva. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +Gracias. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-confirm.fi b/templates/setlist-confirm.fi new file mode 100644 index 0000000..4332714 --- /dev/null +++ b/templates/setlist-confirm.fi @@ -0,0 +1,34 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Varmista tilaajaluettelon muutos listalle %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain: charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Listan omistaja (tai sellaisena esiintyvä) on pyytänyt tilaajalistaa +vaihdettavaksi allaolevan meilin mukaiseksi. Jos hyväksyt tämän +muutokset, vastaa tähän meiliin. Muuten jätä se huomiotta. + +Huomaa, että koko tilaajalista vaihtuu alla olevan listan mukaiseksi. +Listalla jo olevat osoitteet unohdetaan, jos niitä ei ole alla. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-confirm.fr b/templates/setlist-confirm.fr new file mode 100644 index 0000000..fc35071 --- /dev/null +++ b/templates/setlist-confirm.fr @@ -0,0 +1,42 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Veuillez modérer la liste des abonnés pour %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion +%(list)s . + +L'un des propriétaires de la liste (ou quelqu'un se faisant passer pour +l'un d'entres eux) a demandé à ce que la liste des abonnés de cette +liste soit modifiée. Le courriel en question, avec la nouvelle liste des +abonnés se trouve ci-dessous. Si vous acceptez les changements, veuillez +répondre à ce courriel. Dans le cas contraire, ignorez-le. + +Veuillez noter que la totalité des abonnés à la liste sera remplacée +par la liste des adresses ci-dessous. Tous les anciens abonnements +seront supprimés, à moins qu'il ne se trouve dans la nouvelle liste. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-confirm.sv b/templates/setlist-confirm.sv new file mode 100644 index 0000000..4ce558c --- /dev/null +++ b/templates/setlist-confirm.sv @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Var god kontrollera ny prenumerantlista för %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter epostlistan +%(list)s. + +En av listägarna (eller någon som låtsas vara en) har begärt att +prenumerantlistan ändras. Brevet innehållande begäran återfinns nedan. +Om du accepterar ändringen, svara på det här brevet. I annat fall, +ignorera det bara. + +Observera att hela prenumerantlistan kommer att ändras till nedanstående +adresslista. Alla tidigare prenumeranter som inte finns med på den nya +listan kommer att glömmas. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/setlist-done b/templates/setlist-done new file mode 100644 index 0000000..19c9d5e --- /dev/null +++ b/templates/setlist-done @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Subscriber list has been changed for %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +For your information: On request from a list owner, the subscriber list +for the list has been replaced with a new one. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. diff --git a/templates/setlist-done.es b/templates/setlist-done.es new file mode 100644 index 0000000..8b97596 --- /dev/null +++ b/templates/setlist-done.es @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Se ha cambiado la lista de suscriptores de %(list)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje se lo ha enviado el gestor de listas de correo que opera +sobre %(list)s . + +Para su información: Bajo petición de uno de los gestores de la lista, +se ha cambiado la lista de suscriptores por una nueva. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +Gracias. diff --git a/templates/setlist-done.fi b/templates/setlist-done.fi new file mode 100644 index 0000000..8109d7f --- /dev/null +++ b/templates/setlist-done.fi @@ -0,0 +1,16 @@ +From: %(From)s +To: %(To)s +Subject: Tilaajalista vaihdettu listalle %(list)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Tiedoksi: Listan (erään) omistajan pyynnöstä tilaajalista on vaihdettu +uuteen. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . diff --git a/templates/setlist-done.fr b/templates/setlist-done.fr new file mode 100644 index 0000000..a6591a0 --- /dev/null +++ b/templates/setlist-done.fr @@ -0,0 +1,22 @@ +From: %(From)s +To: %(To)s +Subject: La liste des abonnés de la liste %(list)s a changé +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion +%(list)s . + +Pour votre information : à la demande d'un des propriétaires de la +liste, la liste des abonnés pour la liste a été remplacée par une +nouvelle. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. diff --git a/templates/setlist-done.sv b/templates/setlist-done.sv new file mode 100644 index 0000000..d30cd7d --- /dev/null +++ b/templates/setlist-done.sv @@ -0,0 +1,20 @@ +From: %(From)s +To: %(To)s +Subject: Prenumerantlista har ändrats för %(list)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter listan +%(list)s. + +För din kännedom: På begäran från en listägare har listan med +prenumeranter ersatts med en ny. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/setlist-sorry b/templates/setlist-sorry new file mode 100644 index 0000000..7172adb --- /dev/null +++ b/templates/setlist-sorry @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: You can't set the subscriber list for %(list)s +Content-type: text/plain; charset=us-ascii + +Sorry, you are not the list owner for %(list)s. diff --git a/templates/setlist-sorry.es b/templates/setlist-sorry.es new file mode 100644 index 0000000..a665cb9 --- /dev/null +++ b/templates/setlist-sorry.es @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: No puede cambiar la lista de suscriptores de %(list)s +Content-type: text/plain; charset=utf-8 + +Lo siento, no está en la lista de gestores de %(list)s. diff --git a/templates/setlist-sorry.fi b/templates/setlist-sorry.fi new file mode 100644 index 0000000..8bbd9d3 --- /dev/null +++ b/templates/setlist-sorry.fi @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Et voi vaihtaa tilaajalistaa listalle %(list)s +Content-type: text/plain; charset=utf-8 + +Et ole listan %(list)s omistaja. diff --git a/templates/setlist-sorry.fr b/templates/setlist-sorry.fr new file mode 100644 index 0000000..d7df206 --- /dev/null +++ b/templates/setlist-sorry.fr @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Vous ne pouvez pas définir la liste des abonnés pour %(list)s +Content-type: text/plain; charset=utf-8 + +Désolé, mais vous n'êtes pas l'un des propriétaires de la liste %(list)s. diff --git a/templates/setlist-sorry.sv b/templates/setlist-sorry.sv new file mode 100644 index 0000000..4217b23 --- /dev/null +++ b/templates/setlist-sorry.sv @@ -0,0 +1,6 @@ +From: %(From)s +To: %(To)s +Subject: Du kan inte ställa in prenumerantlistan för %(list)s +Content-type: text/plain; charset=utf-8 + +Tyvärr, du är inte någon av ägarna till %(list)s. diff --git a/templates/sub-confirm b/templates/sub-confirm new file mode 100644 index 0000000..c306afe --- /dev/null +++ b/templates/sub-confirm @@ -0,0 +1,44 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Please confirm subscription to %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +Someone has asked you to be added as a subscriber to the list. See the +mail they sent at the bottom. If you did not send it yourself, please +contact the sender or their administrator and ask what is going on. + +If you reply to this mail, you confirm that you wish to be added as a +subscriber. The contents of the reply doesn't matter. Usually, you can +just use the normal reply feature in your mail program. Alternatively, +you can send mail to the address below: + + %(confirm)s + +If you don't want to subscribe, just ignore this mail. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-confirm.es b/templates/sub-confirm.es new file mode 100644 index 0000000..18a11f2 --- /dev/null +++ b/templates/sub-confirm.es @@ -0,0 +1,43 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Por favor, confirme su suscripción a %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje se lo ha enviado el gestor automático que opera la +lista de correo %(list)s . + +Alguien ha pedido que usted sea añadido como suscriptor de la lista. +Al final verá el mensaje enviado. Si no lo envió usted mismo, por favor, +póngase en contacto con quien lo hizo o con su administrador, para saber +qué sucede. + +Si responde a este mensaje, confirmará que desea ser añadido como +suscriptor. No importa el contenido de la respuesta. Por lo común, +basta con usar la función normal de respuesta de su programa de correo. +De forma alternativa, puede enviar un mensaje a la siguiente dirección: + + %(confirm)s + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +Gracias. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-confirm.fi b/templates/sub-confirm.fi new file mode 100644 index 0000000..1e6f57f --- /dev/null +++ b/templates/sub-confirm.fi @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Vahvista tilauspyyntö listalle %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Joku on pyytänyt, että sinut lisätään listan tilaajaksi. Pyyntö on +tämän viestin lopussa. Jos et lähettänyt sitä itse, ole hyvä ja kysy +lähettäjältä tai heidän ylläpidoltaan mistä on kyse. + +Jos vastaat tähän viestiin, vahvistat, että haluat tilata listan. +Vastausviestisi sisällöllä ei ole väliä. Vastaamisen pitäisi onnistua +sähköpostiohjelmasi normaalilla vastaustoiminnolla. Vaihtoehtoisesti +voit lähettää viestin tähän osoitteeseen: + + %(confirm)s + +Jos et halua tilata listaa, sinun ei tarvitse tehdä mitään. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-confirm.fr b/templates/sub-confirm.fr new file mode 100644 index 0000000..3d1f2b3 --- /dev/null +++ b/templates/sub-confirm.fr @@ -0,0 +1,48 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Veuillez confirmer votre abonnement à %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion +de la liste de diffusion %(list)s . + +Quelqu'un vient de demander de vous abonner à cette liste de diffusion. +Regardez le message qui se trouve à la fin. Si vous n'avez pas envoyez +vous-même directement ce message, veuillez prendre contact avec +l'expéditeur de ce message ou bien avec l'administrateur pour demander +des explications. + +En répondant à ce courriel, vous confirmerez que vous souhaitez vous +abonner à cette liste. Le contenu du message n'a pas d'importance. +Typiquement, vous pouvez utiliser la fonction répondre de votre +logiciel de lecture des courriers électroniques. Vous pouvez également +envoyer un courriel à l'adresse ci-dessous : + + %(confirm)s + +Si vous ne souhaitez pas vous abonner, ignorez simplement ce courriel. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-confirm.sv b/templates/sub-confirm.sv new file mode 100644 index 0000000..f48266f --- /dev/null +++ b/templates/sub-confirm.sv @@ -0,0 +1,45 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Bekräfta prenumeration på %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter epostlistan +%(list)s. + +Du, eller någon i ditt ställe, har begärt att du läggs till som +prenumerant på listan. Meddelandet i fråga finns längst ner. Om du inte +skickade det själv, kontakta avsändaren eller dennes administratör och +fråga vad som pågår. + +Genom att besvara det här brevet bekräftar du att du vill vara med som +prenumerant på listan. Innehållet i svaret spelar ingen roll. Vanligtvis +går det bra att använda den vanliga svara-funktionen i ditt epostprogram. +Alternativt kan du skicka ett tomt meddelande till följande adress: + + %(confirm)s + +Om du inte vill prenumerera, strunta bara i det här brevet. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-moderate b/templates/sub-moderate new file mode 100644 index 0000000..6fc83d3 --- /dev/null +++ b/templates/sub-moderate @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Please moderate subscription to %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the + + %(list)s + +mailing list to the human owners of the list. + +Should the following address be allowed to subscribe to the list? + + %(subscriber)s + +If so, reply to this mail or send mail to + + %(confirm)s + +If you wish to reject the subscriber, send mail to + + %(deny)s + +Thanks. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-moderate.es b/templates/sub-moderate.es new file mode 100644 index 0000000..ef2d45a --- /dev/null +++ b/templates/sub-moderate.es @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Por favor, modere esta suscripción a %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje lo ha enviado el gestor que opera la lista de correo + + %(list)s + +a sus administradores humanos. + +¿Debería permitirse a la siguiente dirección suscribirse a la lista? + + %(subscriber)s + +En caso afirmativo, responda a este mensaje o envíe uno a + + %(confirm)s + +Si desea rechazar al suscriptor, envíe un mensaje a + + %(deny)s + +Gracias. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-moderate.fi b/templates/sub-moderate.fi new file mode 100644 index 0000000..6e3aaf0 --- /dev/null +++ b/templates/sub-moderate.fi @@ -0,0 +1,36 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Käsittele tilauspyyntö: %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa + + %(list)s + +käsittelevä ohjelmisto listan omistajille. + +Pitäisikö seuraava osoite lisätä tilaajalistalle? + + %(subscriber)s + +Jos pitäisi, vastaa tähän viestiin tai lähetä viesti osoitteeseen + + %(confirm)s + +Jos ei pitäisi, lähetä viesti osoitteeseen + + %(deny)s + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-moderate.fr b/templates/sub-moderate.fr new file mode 100644 index 0000000..3a8bcf7 --- /dev/null +++ b/templates/sub-moderate.fr @@ -0,0 +1,41 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Veuillez modérer l'abonnement à %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion + + %(list)s + +aux personnes qui sont responsables de la liste. + +L'adresse suivante doit-elle être autorisée à s'abonner à la liste ? + + %(subscriber)s + +Si oui, répondez à ce courriel ou envoyez un message à + + %(confirm)s + +Si vous souhaitez rejeter l'abonnement, envoyez un message à + + %(deny)s + + +Merci. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-moderate.sv b/templates/sub-moderate.sv new file mode 100644 index 0000000..ec2ce95 --- /dev/null +++ b/templates/sub-moderate.sv @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Godkänn ny prenumeration på %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter listan + + %(list)s + +till listans ägare. + +Ska följande adress få lov att prenumerera på listan? + + %(subscriber)s + +I så fall, svara på det här brevet eller skicka ett tomt brev till + + %(confirm)s + +Om du vill avvisa ansökan, skicka ett tomt brev till + + %(deny)s + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/sub-owner-notification b/templates/sub-owner-notification new file mode 100644 index 0000000..83453a0 --- /dev/null +++ b/templates/sub-owner-notification @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Address added to %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the + + %(list)s + +mailing list to the human owners of the list. + +The following address has been added to the list: + + %(address)s + +Unless there is something really strange going on, this mail is only +for your information and you need take no action. + +Thanks. diff --git a/templates/sub-owner-notification.es b/templates/sub-owner-notification.es new file mode 100644 index 0000000..701017e --- /dev/null +++ b/templates/sub-owner-notification.es @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Dirección añadida a %(list)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este es un mensaje del gestor de listas de correo que opera sobre + + %(list)s + +a sus administradores humanos. + +Se ha añadido a la lista la siguiente dirección: + + %(address)s + +A menos que pase algo realmente extraño, este mensaje es sólo +informativo y no hace falta que tome ninguna acción al respecto. + +Gracias. diff --git a/templates/sub-owner-notification.fi b/templates/sub-owner-notification.fi new file mode 100644 index 0000000..bc0c0d0 --- /dev/null +++ b/templates/sub-owner-notification.fi @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Tilaaja lisätty: %(list)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa + + %(list)s + +käsittelevä ohjelmisto listan omistajille. + +Seuraava osoite on lisätty listalle: + + %(address)s + +Tämä tiedoksi teille. Teidän ei tarvitse tehdä mitään asialle, paitsi jos +jotain oikein omituista on tapahtunut. diff --git a/templates/sub-owner-notification.fr b/templates/sub-owner-notification.fr new file mode 100644 index 0000000..548d827 --- /dev/null +++ b/templates/sub-owner-notification.fr @@ -0,0 +1,22 @@ +From: %(From)s +To: %(To)s +Subject: Adresse ajoutée à %(list)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion + + %(list)s + +aux personnes qui sont responsables de la liste. + +L'adresse suivante a été ajoutée à la liste : + + %(address)s + +À moins que quelque chose de particulier vous semble suspect, ce message +vous est uniquement envoyé à titre informatif et ne nécessite pas +traitement particulier. + +Merci. diff --git a/templates/sub-owner-notification.sv b/templates/sub-owner-notification.sv new file mode 100644 index 0000000..83453a0 --- /dev/null +++ b/templates/sub-owner-notification.sv @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Address added to %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the + + %(list)s + +mailing list to the human owners of the list. + +The following address has been added to the list: + + %(address)s + +Unless there is something really strange going on, this mail is only +for your information and you need take no action. + +Thanks. diff --git a/templates/sub-reject b/templates/sub-reject new file mode 100644 index 0000000..ffd1915 --- /dev/null +++ b/templates/sub-reject @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Subscription denied to %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +You tried to subscribe to the list, but the moderator has rejected +your subscription. Sorry. + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. diff --git a/templates/sub-reject.es b/templates/sub-reject.es new file mode 100644 index 0000000..873eca5 --- /dev/null +++ b/templates/sub-reject.es @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Suscripción a %(list)s rechazada +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje se lo ha enviado el gestor de listas de correo que opera +sobre %(list)s . + +Ha intentado suscribirse a la lista, pero el moderador rechazó la +suscripción. Lo siento. + +Si tiene problemas, haga el favor de ponerse en contacto con los +administradores humanos de la lista en %(local)s-owner@%(domain)s . + +Gracias. diff --git a/templates/sub-reject.fi b/templates/sub-reject.fi new file mode 100644 index 0000000..6cf271e --- /dev/null +++ b/templates/sub-reject.fi @@ -0,0 +1,12 @@ +From: %(From)s +To: %(To)s +Subject: Tilaus evätty: %(list)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Yritit tilata listan, mutta listan omistajat eväsivät pyyntösi. + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . diff --git a/templates/sub-reject.fr b/templates/sub-reject.fr new file mode 100644 index 0000000..548fbd4 --- /dev/null +++ b/templates/sub-reject.fr @@ -0,0 +1,18 @@ +From: %(From)s +To: %(To)s +Subject: Rejet de l'abonnement à %(list)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel vous a été envoyé par le gestionnaire de la liste de +diffusion %(list)s . + +Vous avez demandez à vous abonner à la liste, mais le modérateur a +rejet votre demande d'abonnement. Désolé. + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. diff --git a/templates/sub-reject.sv b/templates/sub-reject.sv new file mode 100644 index 0000000..a7a99ec --- /dev/null +++ b/templates/sub-reject.sv @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Prenumeration på %(list)s avvisad +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter epostlistan +%(list)s. + +Du försökte prenumerera på listan, men en moderator har avslagit din +begäran. Beklagar. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/sub-wait b/templates/sub-wait new file mode 100644 index 0000000..1ef973d --- /dev/null +++ b/templates/sub-wait @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Please wait for moderation to %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +You have confirmed that you want to subscribe to the list. List +subscription is moderated, however, and the list owner needs to manually +process your request. This might take a while. Please be patient. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. diff --git a/templates/sub-wait.es b/templates/sub-wait.es new file mode 100644 index 0000000..28e300f --- /dev/null +++ b/templates/sub-wait.es @@ -0,0 +1,22 @@ +From: %(From)s +To: %(To)s +Subject: Por favor, espere por la moderación de %(list)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje se lo ha enviado el gestor de listas de correo que opera +sobre %(list)s . + +Ha confirmado que desea suscribirse a la lista. La suscripción está +moderada, sin embargo, y hace falta que el administrador de la lista +procese manualmente la petición. Esto puede llevar un tiempo. Por favor, +sea paciente. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s . + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s . + +Gracias. diff --git a/templates/sub-wait.fi b/templates/sub-wait.fi new file mode 100644 index 0000000..f53673c --- /dev/null +++ b/templates/sub-wait.fi @@ -0,0 +1,16 @@ +From: %(From)s +To: %(To)s +Subject: Odota omistajan vahvistusta: %(list)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Olet vahvistanut, että haluat tilata listan. Listan tilaus on rajoitettua +ja listan omistajien tarvitsee vielä käsitellä tilauspyyntösi käsin. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . diff --git a/templates/sub-wait.fr b/templates/sub-wait.fr new file mode 100644 index 0000000..619584c --- /dev/null +++ b/templates/sub-wait.fr @@ -0,0 +1,23 @@ +From: %(From)s +To: %(To)s +Subject: Veuillez attendre la modération pour %(list)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion +%(list)s . + +Vous avez confirmé que vous souhaitiez vous abonner à la liste. +L'inscription à la liste est malgré tout modérée, et le propriétaire +de la liste devra manuellement accepter votre requête. Cela peut prendre +un certain temps. Soyez patient. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. diff --git a/templates/sub-wait.sv b/templates/sub-wait.sv new file mode 100644 index 0000000..6a95f7e --- /dev/null +++ b/templates/sub-wait.sv @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Var god vänta på godkännande av prenumerationen på %(list)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter epostlistan +%(list)s. + +Du har bekräftat att du vill prenumerera på listan. Prenumerationen är +dock inte öppen för alla, och därför måste listans ägare manuellt +godkänna din begäran. Det kan ta en stund, så ha tålamod! + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/sub-welcome b/templates/sub-welcome new file mode 100644 index 0000000..3e18880 --- /dev/null +++ b/templates/sub-welcome @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Welcome to %(list)s +Content-type: text/plain; charset=us-ascii + +Welcome to the %(list)s mailing list. + +If you wish to unsubscribe, send mail to +%(local)s-unsubscribe@%(domain)s . + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. diff --git a/templates/sub-welcome.es b/templates/sub-welcome.es new file mode 100644 index 0000000..0a8e13e --- /dev/null +++ b/templates/sub-welcome.es @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Bienvenido a %(list)s +Content-type: text/plain; charset=utf-8 + +Bienvenido de la lista de c orreo %(list)s . + +Si desea cancelar la suscripción, envíe un mensaje a +%(local)s-unsubscribe@%(domain)s . + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s. + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s. + +Gracias. diff --git a/templates/sub-welcome.fi b/templates/sub-welcome.fi new file mode 100644 index 0000000..b32c978 --- /dev/null +++ b/templates/sub-welcome.fi @@ -0,0 +1,15 @@ +From: %(From)s +To: %(To)s +Subject: Tervetuloa listalle %(list)s +Content-type: text/plain; charset=utf-8 + +Tervetuloa postituslistalle %(list)s . + +Jos haluat poistua listalta, lähetä postia osoitteeseen +%(local)s-unsubscribe@%(domain)s . + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . diff --git a/templates/sub-welcome.fr b/templates/sub-welcome.fr new file mode 100644 index 0000000..818275a --- /dev/null +++ b/templates/sub-welcome.fr @@ -0,0 +1,18 @@ +From: %(From)s +To: %(To)s +Subject: Bienvenue sur %(list)s +Content-type: text/plain; charset=utf-8 + +Bienvenue sur la liste de diffusion %(list)s . + +Si vous souhaitez vous désabonner, envoyez un courriel à +%(local)s-unsubscribe@%(domain)s. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. diff --git a/templates/sub-welcome.sv b/templates/sub-welcome.sv new file mode 100644 index 0000000..0b848b9 --- /dev/null +++ b/templates/sub-welcome.sv @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Välkommen till %(list)s +Content-type: text/plain; charset=utf-8 + +Välkommen till epostlistan %(list)s! + +Om du vill avsluta prenumerationen, skicka ett tomt brev till +%(local)s-unsubscribe@%(domain)s. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/unsub-confirm b/templates/unsub-confirm new file mode 100644 index 0000000..0187b3a --- /dev/null +++ b/templates/unsub-confirm @@ -0,0 +1,43 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Please confirm UNsubscription to %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the +%(list)s mailing list. + +Someone has asked you to be removed as a subscriber from the list. See +the mail they sent at the bottom. If you did not send it yourself, +please contact the sender or their administrator and ask what is going on. + +If you reply to this mail, you confirm that you wish to be removed as a +subscriber. Usually, you can just use the normal reply feature in your +mail program. Alternatively, you can send mail to the address below: + + %(confirm)s + +You don't need to do anything to continue your subscription. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/unsub-confirm.es b/templates/unsub-confirm.es new file mode 100644 index 0000000..52d596b --- /dev/null +++ b/templates/unsub-confirm.es @@ -0,0 +1,45 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Por favor, confirme la cancelación de suscripción a %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje lo ha enviado el gestor de listas de correo que opera +sobre %(list)s . + +Alguien ha pedido que se cancele su suscripción a la lista. Lea el mensaje +que se envió al final del todo. Si no lo envió usted mismo, por favor, +póngase en contacto con quien lo hizo, o sus administradores, y pregúnteles +qué está pasando. + +Si responde a este mensaje, confirmará que desea la cancelación. Por lo +común, basta con usar la función normal de respuesta de su programa de +correo. De forma alternativa, puede enviar mensajes a las siguientes +direcciones: + + %(confirm)s + +Si desea continuar con su suscripción, no hace falta que haga nada. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s. + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s. + +Gracias. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/unsub-confirm.fi b/templates/unsub-confirm.fi new file mode 100644 index 0000000..f886b84 --- /dev/null +++ b/templates/unsub-confirm.fi @@ -0,0 +1,40 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Vahvista poistumispyyntö: %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa %(list)s +operoiva ohjelmisto. + +Joku on pyytänyt, että sinut poistetaan listalta. Pyyntö on tämän viestin +lopussa. Jos et lähettänyt sitä itse, ole hyvä ja kysy lähettäjältä tai +heidän ylläpidoltaan mistä on kyse. + +Jos vastaat tähän viestiin, vahvistat, että haluat poistua listalta. +Vastausviestisi sisällöllä ei ole väliä. Vastaamisen pitäisi onnistua +sähköpostiohjelmasi normaalilla vastaustoiminnolla. Vaihtoehtoisesti +voit lähettää viestin tähän osoitteeseen: + + %(confirm)s + +Jos et halua tilata listaa, sinun ei tarvitse tehdä mitään. + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/unsub-confirm.fr b/templates/unsub-confirm.fr new file mode 100644 index 0000000..4615e07 --- /dev/null +++ b/templates/unsub-confirm.fr @@ -0,0 +1,46 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Veuillez confirmer votre DÉsabonnement à %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion +%(list)s . + +Quelqu'un a demandé à ce que vous soyez désabonné de la liste. Regardez +le message qui ce trouve en dessous. Si vous n'avez pas envoyé ce message +vous-même, veuillez prendre contact avec l'expéditeur ou son +administrateur et demandez des explications. + +En répondant à ce courriel, vous confirmerez que vous souhaitez vous +désabonner. Généralement, il suffit simplement d'utiliser la fonction +répondre de votre logiciel de lecture des courriers électroniques. Vous +pouvez également envoyez un courriel à l'adresse indiquée ci-dessous : + + %(confirm)s + +Si vous souhaitez toujours être abonné, aucune action n'est nécessaire. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/unsub-confirm.sv b/templates/unsub-confirm.sv new file mode 100644 index 0000000..873b289 --- /dev/null +++ b/templates/unsub-confirm.sv @@ -0,0 +1,45 @@ +From: %(From)s +To: %(To)s +Reply-To: %(confirm)s +Subject: Bekräfta avslut på prenumeration på %(list)s +Content-type: multipart/mixed; boundary="%(boundary)s" +MIME-Version: 1.0 + +This is a multipart message in MIME format + +--%(boundary)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter epostlistan +%(list)s. + +Du, eller någon i ditt ställe, har begärt att din prenumeration på +listan ska avslutas. Meddelandet i fråga finns längst ner. Om du inte +skickade det själv, kontakta avsändaren eller dennes administratör och +fråga vad som pågår. + +Genom att besvara det här brevet bekräftar du att du inte längre vill +prenumerera på listan. Innehållet i svaret spelar ingen roll. Vanligtvis +går det bra att använda den vanliga svara-funktionen i ditt epostprogram. +Alternativt kan du skicka ett tomt meddelande till följande adress: + + %(confirm)s + +Om du vill fortsätta prenumerera behöver du inte göra någonting alls. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till to %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. + +--%(boundary)s +Content-type: message/rfc822 +Content-disposition: inline; filename=original.txt + +%(origmail)s--%(boundary)s-- + diff --git a/templates/unsub-goodbye b/templates/unsub-goodbye new file mode 100644 index 0000000..9eb4b6b --- /dev/null +++ b/templates/unsub-goodbye @@ -0,0 +1,14 @@ +From: %(From)s +To: %(To)s +Subject: Goodbye from %(list)s +Content-type: text/plain; charset=us-ascii + +Goodbye from the %(list)s mailing list. + +For instructions on using the mailing list manager software, send +mail to %(local)s-help@%(domain)s . + +If you have problems, please contact the human owners of the list +at %(local)s-owner@%(domain)s . + +Thank you. diff --git a/templates/unsub-goodbye.es b/templates/unsub-goodbye.es new file mode 100644 index 0000000..309d6af --- /dev/null +++ b/templates/unsub-goodbye.es @@ -0,0 +1,14 @@ +From: %(From)s +To: %(To)s +Subject: Despedida de %(list)s +Content-type: text/plain; charset=utf-8 + +Nos despedimos de usted desde la lista de correo %(list)s. + +Si desea instrucciones sobre el uso del software gestor de listas, envíe +un mensaje a %(local)s-help@%(domain)s. + +Si tiene problemas, por favor, póngase en contacto con las personas que +gestionan la lista en %(local)s-owner@%(domain)s. + +Gracias. diff --git a/templates/unsub-goodbye.fi b/templates/unsub-goodbye.fi new file mode 100644 index 0000000..af1d710 --- /dev/null +++ b/templates/unsub-goodbye.fi @@ -0,0 +1,12 @@ +From: %(From)s +To: %(To)s +Subject: Näkemiin listalta %(list)s +Content-type: text/plain; charset=utf-8 + +Näkemiin postituslistalta %(list)s . + +Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta +%(local)s-help@%(domain)s . + +Listan omistajat saat kiinni osoitteesta +%(local)s-owner@%(domain)s . diff --git a/templates/unsub-goodbye.fr b/templates/unsub-goodbye.fr new file mode 100644 index 0000000..7de36e9 --- /dev/null +++ b/templates/unsub-goodbye.fr @@ -0,0 +1,15 @@ +From: %(From)s +To: %(To)s +Subject: Confirmation de votre désabonnement de %(list)s +Content-type: text/plain; charset=utf-8 + +Votre désabonnement de la liste de diffusion %(list)s est confirmé. + +Pour obtenir des informations sur l'utilisation du logiciel de gestion de +la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s . + +Si vous rencontrez des problèmes, veuillez prendre contact avec la +personne responsable de la liste de diffusion à l'adresse : +%(local)s-owner@%(domain)s . + +Merci. diff --git a/templates/unsub-goodbye.sv b/templates/unsub-goodbye.sv new file mode 100644 index 0000000..91e3081 --- /dev/null +++ b/templates/unsub-goodbye.sv @@ -0,0 +1,14 @@ +From: %(From)s +To: %(To)s +Subject: Farväl från %(list)s +Content-type: text/plain; charset=utf-8 + +Din prenumeration på %(list)s har härmed avslutats. + +För instruktioner om hur man använder epostlistehanteraren, skicka +ett brev till %(local)s-help@%(domain)s. + +Om du har problem, kontakta personerna som äger listan på +%(local)s-owner@%(domain)s. + +Tack. diff --git a/templates/unsub-owner-notification b/templates/unsub-owner-notification new file mode 100644 index 0000000..e18ee36 --- /dev/null +++ b/templates/unsub-owner-notification @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Address removed from %(list)s +Content-type: text/plain; charset=us-ascii + +Hello, + +This mail has been sent by the mailing list manager operating the + + %(list)s + +mailing list to the human owners of the list. + +The following address has unsubscribed itself from the list: + + %(address)s + +Unless there is something really strange going on, this mail is only +for your information and you need take no action. + +Thanks. diff --git a/templates/unsub-owner-notification.es b/templates/unsub-owner-notification.es new file mode 100644 index 0000000..97b01aa --- /dev/null +++ b/templates/unsub-owner-notification.es @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Dirección eliminada de %(list)s +Content-type: text/plain; charset=utf-8 + +Hola, + +Este mensaje lo ha enviado el gestor de listas de correo que opera sobre + + %(list)s + +a sus administradores humanos. + +Se ha cancelado la suscripción a la lista de la siguiente dirección: + + %(address)s + +A menos que pase algo realmente extraño, este mensaje sólo es informativo +y no necesita tomar ninguna acción al respecto. + +Gracias. diff --git a/templates/unsub-owner-notification.fi b/templates/unsub-owner-notification.fi new file mode 100644 index 0000000..a72ea23 --- /dev/null +++ b/templates/unsub-owner-notification.fi @@ -0,0 +1,17 @@ +From: %(From)s +To: %(To)s +Subject: Osoite poistettu listalta %(list)s +Content-type: text/plain; charset=utf-8 + +Tämän viestin on lähettänyt postituslistaa + + %(list)s + +käsittelevä ohjelmisto listan omistajille. + +Seuraava osoite on poistanut itsensä listalta: + + %(address)s + +Tämä tiedoksi teille. Teidän ei tarvitse tehdä mitään asialle, paitsi jos +jotain oikein omituista on tapahtunut. diff --git a/templates/unsub-owner-notification.fr b/templates/unsub-owner-notification.fr new file mode 100644 index 0000000..0272399 --- /dev/null +++ b/templates/unsub-owner-notification.fr @@ -0,0 +1,22 @@ +From: %(From)s +To: %(To)s +Subject: Adresse supprimée sur %(list)s +Content-type: text/plain; charset=utf-8 + +Bonjour, + +Ce courriel a été envoyé par le gestionnaire de la liste de diffusion + + %(list)s + +aux personnes qui sont responsables de la liste. + +L'adresse suivante s'est désabonnée d'elle-même de la liste : + + %(address)s + +À moins que quelque chose de particulier vous semble suspect, ce message +vous est uniquement envoyé à titre informatif et ne nécessite pas +traitement particulier. + +Merci. diff --git a/templates/unsub-owner-notification.sv b/templates/unsub-owner-notification.sv new file mode 100644 index 0000000..cfec27c --- /dev/null +++ b/templates/unsub-owner-notification.sv @@ -0,0 +1,21 @@ +From: %(From)s +To: %(To)s +Subject: Adress borttagen från %(list)s +Content-type: text/plain; charset=utf-8 + +Hej! + +Det här brevet skickades av epostlistehanteraren som sköter listan + + %(list)s + +till listans ägare. + +Följande adress har avslutat sin prenumeration på listan: + + %(address)s + +Såvida inte något riktigt märkligt pågår är det här brevet bara till +för att informera dig och du behöver inte vidta någon åtgärd. + +Tack. \ No newline at end of file diff --git a/testrun.py b/testrun.py new file mode 100644 index 0000000..53c1dbd --- /dev/null +++ b/testrun.py @@ -0,0 +1,9 @@ +import unittest +import os + +suite = unittest.TestSuite() +for n in filter(lambda fn: fn[-8:] == "Tests.py", os.listdir(".")): + suite.addTest(unittest.defaultTestLoader.loadTestsFromName(n[:-3])) + +runner = unittest.TextTestRunner() +runner.run(suite)