--- /dev/null
+ 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.
+\f
+ 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.)
+\f
+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.
+\f
+ 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.
+\f
+ 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
+\f
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 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.
--- /dev/null
+2005-12-03 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * templates/*: Changed content dispositions from attachment to
+ inline so that they are shown by default in mailers.
+
+2005-04-16 Lars Wirzenius <liw@iki.fi>
+
+ * templates/bounce-owner-notification, templates/bounce-warning,
+ templates/setlist-confirm: Say "attached" instead of "appended"
+ or "below".
+
+2005-04-16 Lars Wirzenius <liw@iki.fi>
+
+ * eoc.py: nice_7bit: Don't treat white space characters as
+ requiring MIME encoding.
+
+2005-04-10 Lars Wirzenius <liw@iki.fi>
+
+ * Making release version 1.1.5.
+
+2005-04-10 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py, eocTests.py, enemies-of-carlotta.1: Added option
+ "pristine-headers" to disable header MIME encoding.
+
+2005-04-10 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 1.1.4.
+
+2004-12-06 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * TODO: Added.
+
+2004-12-05 Lars Wirzenius <liw@iki.fi>
+
+ * enemies-of-carlotta.1.es: Updated Spanish manual page from
+ Ricardo Javier Cardenes.
+
+2004-12-04 Lars Wirzenius <liw@iki.fi>
+
+ * README.CVS: Wrote.
+
+2004-11-27 Lars Wirzenius <liw@iki.fi>
+
+ * Making release version 1.1.3.
+
+2004-11-27 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py: --show-lists now alphabetizes the output.
+
+2004-11-27 Lars Wirzenius <liw@iki.fi>
+
+ * templates/bounce-probe*, templates/sub-already*,
+ templates/unsub-already*: Unused, removed.
+
+2004-11-27 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py: Added --version option.
+
+2004-11-27 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py: Added command line options --get and --set.
+
+ * enemies-of-carlotta.1: Documented them.
+
+2004-11-27 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 1.1.2.
+
+2004-09-18 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py, eocTest.py: Added support for $listdir/headers-to-add
+ (but it still needs to be documented).
+
+2004-09-03 Lars Wirzenius <liw@iki.fi>
+
+ * eoc.py, eocTest.py: Some refactoring and new unit tests.
+
+2004-08-28 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 1.1.0.
+
+2004-07-25 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eocTests.py: Added some test cases for recipient address
+ parsing.
+
+2004-07-09 Lars Wirzenius <liw@iki.fi>
+
+ * eoc.py: Refactoring changes to make code nicer. No functional
+ changes.
+
+2004-07-09 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * templates/*.sv: Added Swedish translation from Magnus Holmgren.
+
+2004-02-21 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * enemies-of-carlotta.1: Updated.
+
+ * eoc.py: Added --sender and --recipient options on suggestion
+ from Tommi Virtanen.
+
+2004-02-21 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py: Added some safeguards against subscribing addresses
+ without @ characters.
+
+2004-02-21 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 1.0.3.
+
+2003-09-07 Lars Wirzenius <liw@iki.fi>
+
+ * enemies-of-carlotta.1: Removed erroneous quotation marks in
+ the Qmail section. Added section documenting all mail commands.
+
+2003-09-06 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 1.0.2.
+
+2003-06-20 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 1.0.1.
+
+2003-05-11 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 0.23.
+
+2003-04-13 Lars Wirzenius <liw@iki.fi>
+
+ * eoc.py: -reject now actually removes the rejected message. Oops.
+
+2003-04-13 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release 0.22.
+
+2003-03-22 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Making release version 0.21.
+
+2003-03-15 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * enemies-of-carlotta.1.fr: Proofread translation by Gérard
+ Delafond, sent by Pierre Machard.
+
+2003-03-15 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * templates/footer.fi: Reworded to use only us-ascii letters.
+
+2003-03-08 Lars Wirzenius <liw@iki.fi>
+
+ * BENCHMARKS, eoc-benchmark, eoc-benchmark-procmailrc: Added.
+
+2003-02-25 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py: Use os.path.isfile instead of os.path.exists to see
+ whether a MessageBox contains a file.
+
+2003-02-16 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py: Applied patch from Ricardo Javier Cardenes
+ <ricardo@conysis.com> 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 <liw@iki.fi>
+
+ * Releasing version 0.16.1.
+
+ * eoc.py: Bounce message quoting had a stupid bug: it didn't
+ add newlines.
+
+2003-01-11 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Releasing version 0.15.
+
+2002-12-08 Lars Wirzenius <liw@iki.fi>
+
+ * Added --moderate option for asking a message to be
+ moderated. To be used for spam filtering, at least.
+
+2002-12-08 Lars Wirzenius <liw@iki.fi>
+
+ * eocTests.py: Set the quiet flag, so that "make check" doesn't
+ output debuggning messages.
+
+2002-12-08 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Releasing version 0.14.
+
+2002-12-08 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Added support for a footer to be appended to each mail.
+
+2002-10-26 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * Releasing version 0.13.
+
+2002-10-26 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * eoc.py: Added --quiet and --sendmail options.
+
+ * eocTests.py: Related changes.
+
+2002-10-21 Lars Wirzenius <liw@iki.fi>
+
+ * Releasing version 0.12.
+
+2002-10-21 Lars Wirzenius <liw@iki.fi>
+
+ * --incoming always reads stdin.
+
+2002-10-11 Lars Wirzenius <liw@iki.fi>
+
+ * 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 <liw@iki.fi>
+
+ * 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.
--- /dev/null
+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)
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+#!/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:])
--- /dev/null
+.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/ .
--- /dev/null
+.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.
--- /dev/null
+.\" 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 <pmachard@tuxfamily.org>, 2003
--- /dev/null
+"""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: <mailto:%s-help@%s>" % (local, domain))
+ list.append("List-Unsubscribe: <mailto:%s-unsubscribe@%s>" %
+ (local, domain))
+ list.append("List-Subscribe: <mailto:%s-subscribe@%s>" %
+ (local, domain))
+ list.append("List-Post: <mailto:%s@%s>" % (local, domain))
+ list.append("List-Owner: <mailto:%s-owner@%s>" % (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)
--- /dev/null
+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<address>" + 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<address>" + 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)
--- /dev/null
+#!/bin/sh
+
+sed 's:^TEMPLATE_DIRS *=.*:TEMPLATE_DIRS = ["'$1'"]:' |
+sed 's:^DOTDIR *=.*:DOTDIR = os.path.expanduser("~/.enemies-of-carlotta"):'
--- /dev/null
+#!/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 <liiwi@lonesom.pp.fi> 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()
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+
+--
+To unsubscribe, send mail to %(local)s-unsubscribe@%(domain)s.
--- /dev/null
+
+--
+Para desuscribirse, mande un mensaje a %(local)s-unsubscribe@%(domain)s
--- /dev/null
+
+--
+Poistu listalta: %(local)s-unsubscribe@%(domain)s .
--- /dev/null
+
+--
+Pour ne plus recevoir de message : mailto:%(local)s-unsubscribe@%(domain)s
--- /dev/null
+
+--
+Om du vill avsluta prenumerationen, skicka ett brev till
+%(local)s-unsubscribe@%(domain)s.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
+
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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--
+
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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 .
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+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)