Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial import of the rumbster code

  • Loading branch information...
commit 6b528d1163f279fd7c5cb75e8c2219ab8efd2d05 1 parent ab36dd6
@aesterline aesterline authored
Showing with 7,629 additions and 0 deletions.
  1. +515 −0 COPYING
  2. +56 −0 README
  3. +12 −0 Rakefile
  4. +40 −0 lib/message_observers.rb
  5. +24 −0 lib/rumbster.rb
  6. +42 −0 lib/smtp_protocol.rb
  7. +159 −0 lib/smtp_states.rb
  8. +102 −0 test/message_observers_test.rb
  9. +69 −0 test/rumbster_test.rb
  10. +64 −0 test/smtp_protocol_test.rb
  11. +217 −0 test/smtp_states_test.rb
  12. +4 −0 vendor/tmail.rb
  13. +3 −0  vendor/tmail/.cvsignore
  14. +19 −0 vendor/tmail/Makefile
  15. +222 −0 vendor/tmail/address.rb
  16. +52 −0 vendor/tmail/base64.rb
  17. +39 −0 vendor/tmail/compat.rb
  18. +50 −0 vendor/tmail/config.rb
  19. +447 −0 vendor/tmail/encode.rb
  20. +895 −0 vendor/tmail/header.rb
  21. +14 −0 vendor/tmail/info.rb
  22. +1 −0  vendor/tmail/loader.rb
  23. +869 −0 vendor/tmail/mail.rb
  24. +386 −0 vendor/tmail/mailbox.rb
  25. +1 −0  vendor/tmail/mbox.rb
  26. +260 −0 vendor/tmail/net.rb
  27. +122 −0 vendor/tmail/obsolete.rb
  28. +1,475 −0 vendor/tmail/parser.rb
  29. +372 −0 vendor/tmail/parser.y
  30. +356 −0 vendor/tmail/port.rb
  31. +22 −0 vendor/tmail/scanner.rb
  32. +243 −0 vendor/tmail/scanner_r.rb
  33. +256 −0 vendor/tmail/stringio.rb
  34. +197 −0 vendor/tmail/textutils.rb
  35. +1 −0  vendor/tmail/tmail.rb
  36. +23 −0 vendor/tmail/utils.rb
View
515 COPYING
@@ -0,0 +1,515 @@
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 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.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations
+below.
+
+ When we speak of free software, we are referring to freedom of use,
+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 and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+^L
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it
+becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+^L
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, 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 library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete 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 distribute a copy of this License along with the
+Library.
+
+ 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 Library or any portion
+of it, thus forming a work based on the Library, 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) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+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 Library, 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 Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+^L
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you 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.
+
+ If distribution of 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 satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+^L
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be 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.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+^L
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library 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.
+
+ 9. 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 Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+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 with
+this License.
+^L
+ 11. 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 Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library 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 Library.
+
+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.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library 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.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser 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 Library
+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 Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+^L
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+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
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "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
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. 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 LIBRARY 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
+LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+^L
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms
+of the ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library.
+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 library's name and a brief idea of what it
+does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; 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.
+
+You should also get your employer (if you work as a programmer) or
+your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James
+Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
View
56 README
@@ -0,0 +1,56 @@
+Rumbster README
+===============
+
+ Rumbster is a fake smtp server for email testing in Ruby.
+ Rumbster was developed to as a way to acceptance test email
+ sending applications.
+
+Requirements
+------------
+
+ * Ruby 1.8.2 or later (may work with earlier versions)
+
+License
+-------
+
+ GNU LGPL, Lesser General Public License version 2.1
+ For details of LGPL, see file "COPYING".
+
+Example Usage
+-------------
+
+A good source for usage information is the unit tests in the
+test directory. Below is an example of the usage.
+
+class TestEmails < Test::Unit::TestCase
+
+ def setup
+ @rumbster = Rumbster.new(port)
+ @message_observer = MailMessageObserver.new
+
+ @rumbster.add_observer @message_observer
+ @rumbster.add_observer FileMessageObserver.new('some/directory')
+
+ @rumbster.start
+ end
+
+ def teardown
+ @rumbster.stop
+ end
+
+ def test_email_is_sent
+ send_email
+ assert_equal 1, @message_observer.messages.size
+ assert_equal 'junk@junk.com', @message_observer.messages.first.to
+ end
+end
+
+Bug Report
+----------
+
+ Any bug reports are welcome.
+ If you encounter a bug, please email me.
+
+ Adam Esterline
+ adam@esterlines.com
+ http://adamesterline.com
View
12 Rakefile
@@ -0,0 +1,12 @@
+require 'rake'
+require 'rake/testtask'
+
+desc "Default Task"
+task :default => [ :test ]
+
+# Run the unit tests
+Rake::TestTask.new { |t|
+ t.libs << "test"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
View
40 lib/message_observers.rb
@@ -0,0 +1,40 @@
+$:.unshift(File.join(File.dirname(__FILE__), '..', 'vendor'))
+
+require 'fileutils'
+require 'tmail'
+
+class FileMessageObserver
+ include FileUtils
+
+ def initialize(message_directory, system_time = SystemTime.new)
+ @message_directory = message_directory
+ @system_time = system_time
+ mkdir_p(message_directory)
+ end
+
+ def update(message_string)
+ mail = TMail::Mail.parse(message_string)
+
+ file_name = File.join(@message_directory, "#{@system_time.current_time_in_seconds}_#{mail.to}.txt")
+ File.open(file_name, 'w') {|file| file << message_string }
+ end
+end
+
+class MailMessageObserver
+ attr_reader :messages
+
+ def initialize
+ @messages = []
+ end
+
+ def update(message_string)
+ @messages << TMail::Mail.parse(message_string)
+ end
+
+end
+
+class SystemTime
+ def current_time_in_seconds
+ Time.now.to_i
+ end
+end
View
24 lib/rumbster.rb
@@ -0,0 +1,24 @@
+require 'gserver'
+require 'smtp_protocol'
+
+class Rumbster < GServer
+
+ def initialize(port=25, *args)
+ super(port, *args)
+
+ @observers = []
+ end
+
+ def serve(io)
+ @protocol = SmtpProtocol.create
+ @observers.each do |observer|
+ @protocol.add_observer(observer)
+ end
+ @protocol.serve(io)
+ end
+
+ def add_observer(observer)
+ @observers.push(observer)
+ end
+
+ end
View
42 lib/smtp_protocol.rb
@@ -0,0 +1,42 @@
+require 'observer'
+require 'smtp_states'
+
+class SmtpProtocol
+ include Observable
+
+ def SmtpProtocol.create
+ initial_state = :init
+
+ states = {
+ :init => InitState.new,
+ :connect => ConnectState.new,
+ :connected => ConnectedState.new,
+ :read_mail => ReadMailState.new,
+ :quit => QuitState.new
+ }
+
+ SmtpProtocol.new(initial_state, states)
+ end
+
+ def initialize(initial_state, states)
+ states.each_value { |state| state.protocol = self }
+
+ @states = states
+ @state = @states[initial_state]
+ end
+
+ def state=(new_state)
+ @state = @states[new_state]
+ end
+
+ def serve(io)
+ @state.serve(io)
+ end
+
+ def new_message_received(message)
+ changed
+ notify_observers(message)
+ end
+
+end
+
View
159 lib/smtp_states.rb
@@ -0,0 +1,159 @@
+class NotInitializedError < RuntimeError; end
+
+module State
+ attr_accessor :protocol
+
+ def serve(io)
+ raise NotInitializedError.new if @protocol.nil?
+
+ service_request(io)
+ @protocol.state = @next_state
+ @protocol.serve(io)
+ end
+end
+
+module Messages
+
+ def greeting(io)
+ io.puts '220 ruby ESMTP'
+ end
+
+ def helo_response(io)
+ io.puts '250 ruby'
+ end
+
+ def ok(io)
+ io.puts '250 ok'
+ end
+
+ def go_ahead(io)
+ io.puts '354 go ahead'
+ end
+
+ def goodbye(io)
+ io.puts '221 ruby goodbye'
+ end
+
+end
+
+class InitState
+
+ include State, Messages
+
+ def initialize(protocol = nil, next_state = :connect)
+ @protocol = protocol
+ @next_state = next_state
+ end
+
+ private
+
+ def service_request(io)
+ greeting(io)
+ end
+
+end
+
+class ConnectState
+
+ include State, Messages
+
+ def initialize(protocol = nil, next_state = :connected)
+ @protocol = protocol
+ @next_state = next_state
+ end
+
+ private
+
+ def service_request(io)
+ read_client_helo(io)
+ helo_response(io)
+ end
+
+ def read_client_helo(io)
+ io.readline
+ end
+
+end
+
+class ConnectedState
+
+ include State, Messages
+
+ def initialize(protocol = nil)
+ @protocol = protocol
+ @next_state = :connected
+ end
+
+ private
+
+ def service_request(io)
+ request = io.readline
+
+ if request.strip.eql? "DATA"
+ @next_state = :read_mail
+ go_ahead(io)
+ else
+ ok(io)
+ end
+ end
+
+end
+
+class ReadMailState
+
+ include State, Messages
+
+ def initialize(protocol = nil)
+ @protocol = protocol
+ @next_state = :quit
+ end
+
+ private
+
+ def service_request(io)
+ message = read_message(io)
+ @protocol.new_message_received(message)
+ ok(io)
+ end
+
+ def not_end_of_message(line)
+ not line.strip.eql?('.')
+ end
+
+ def read_message(io)
+ message = ''
+
+ line = io.readline
+ while not_end_of_message(line)
+ message << line
+ line = io.readline
+ end
+
+ message
+ end
+
+end
+
+class QuitState
+
+ include Messages
+ attr_accessor :protocol
+
+ def initialize(protocol = nil)
+ @protocol = protocol
+ end
+
+ def serve(io)
+ raise NotInitializedError.new if @protocol.nil?
+
+ read_quit(io)
+ goodbye(io)
+ end
+
+ private
+
+ def read_quit(io)
+ io.readline
+ end
+
+end
View
102 test/message_observers_test.rb
@@ -0,0 +1,102 @@
+$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'test/unit'
+require 'fileutils'
+require 'message_observers'
+
+class MessageObserversTest < Test::Unit::TestCase
+
+ include FileUtils
+
+ def setup
+ @directory = File.join(File.dirname(__FILE__), '..', 'messages')
+ end
+
+ def teardown
+ rm_r(@directory) if File.exists?(@directory)
+ end
+
+ def test_directory_is_created_when_directory_is_not_present
+ file_observer = FileMessageObserver.new(@directory)
+
+ assert File.exists?(@directory)
+ end
+
+ def test_if_directory_is_already_present_observer_can_still_be_created
+ mkdir_p(@directory)
+
+ assert File.exists?(@directory)
+ assert_not_nil FileMessageObserver.new(@directory)
+ end
+
+ def test_message_received_is_saved_to_message_directory
+ file_observer = FileMessageObserver.new(@directory)
+ file_observer.update "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
+
+ assert_one_new_file_added_to @directory
+ end
+
+ def test_message_is_saved_in_a_file_named_time_stamp_underscore_to_dot_txt
+ file_observer = FileMessageObserver.new(@directory, TestSystemTime.new(1))
+ file_observer.update "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
+
+ expected_file_name = File.join(@directory, "1_junk@junk.com.txt")
+
+ assert File.exists?(expected_file_name)
+ end
+
+ def test_message_contents_are_saved_to_the_file
+ message = "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
+
+ file_observer = FileMessageObserver.new(@directory, TestSystemTime.new(1))
+ file_observer.update message
+
+ expected_file_name = File.join(@directory, "1_junk@junk.com.txt")
+
+ assert_equal read_file_contents_into_a_string(expected_file_name), message
+ end
+
+ def test_messages_are_saved_for_later_inspection
+ message = "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
+
+ mail_observer = MailMessageObserver.new
+ mail_observer.update message
+ mail_observer.update message
+
+ assert_equal 2, mail_observer.messages.size
+ end
+
+ def test_message_is_converted_to_tmail
+ message = "To: junk@junk.com\nFrom: junk2@junk2.com\nSubject: What's up\n\nHi"
+
+ mail_observer = MailMessageObserver.new
+ mail_observer.update message
+
+ assert_equal 'What\'s up', mail_observer.messages.first.subject
+ end
+
+ private
+
+ def read_file_contents_into_a_string(file_name)
+ File.open(file_name) {|file| file.readlines.join }
+ end
+
+
+ def assert_one_new_file_added_to(directory)
+ number_of_dot_and_dot_dot_entries_in_a_directory_when_it_is_first_created = 2
+ number_of_entries_with_one_file = number_of_dot_and_dot_dot_entries_in_a_directory_when_it_is_first_created + 1
+
+ assert_equal number_of_entries_with_one_file, Dir.entries(@directory).size
+ end
+end
+
+class TestSystemTime
+ def initialize(time)
+ @time = time
+ end
+
+ def current_time_in_seconds
+ @time
+ end
+
+end
View
69 test/rumbster_test.rb
@@ -0,0 +1,69 @@
+$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'test/unit'
+require 'rumbster'
+require 'net/smtp'
+require 'gserver'
+
+
+class RumbsterTest < Test::Unit::TestCase
+
+ def setup
+ @observer = RumbsterObserver.new
+ @server = Rumbster.new(10025)
+ @server.add_observer(@observer)
+
+ @server.start
+ end
+
+ def teardown
+ @server.stop
+ end
+
+ def test_single_receiver_message_sent_by_client_is_received_by_listener
+ message = "From: <junk@junkster.com>\r\nTo: junk@junk.com\r\nSubject: hi\r\n\r\nThis is a test\r\n"
+ to = 'his_address@example.com'
+ send_message to, message
+
+ assert_equal message, @observer.message
+ end
+
+ def test_multiple_receiver_message_sent_by_client_is_received_by_listener
+ message = "From: <junk@junkster.com>\r\nTo: junk@junk.com\r\nSubject: hi\r\n\r\nThis is a test\r\n"
+ to = ['his_address@example.com', 'her_address@example.com']
+ send_message to, message
+
+ assert_equal message, @observer.message
+ end
+
+ def test_multiple_receiver_messages_sent_by_client_are_received_by_listener
+ message_1 = "From: <junk_1@junkster.com>\r\nTo: junk_1@junk.com\r\nSubject: hi\r\n\r\nThis is a test_1\r\n"
+ to_1 = ['his_address_1@example.com', 'her_address_1@example.com']
+ send_message to_1, message_1
+
+ assert_equal message_1, @observer.message
+
+ message_2 = "From: <junk_2@junkster.com>\r\nTo: junk_2@junk.com\r\nSubject: hi\r\n\r\nThis is a test_2\r\n"
+ to_2 = ['his_address_2@example.com', 'her_address_2@example.com']
+ send_message to_2, message_2
+
+ assert_equal message_2, @observer.message
+ end
+
+ private
+
+ def send_message(to, message)
+ Net::SMTP.start('localhost', 10025) do |smtp|
+ smtp.send_message message, 'your@mail.address', to
+ end
+ end
+
+end
+
+class RumbsterObserver
+ attr_accessor :message
+
+ def update(message)
+ @message = message
+ end
+end
View
64 test/smtp_protocol_test.rb
@@ -0,0 +1,64 @@
+$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'test/unit'
+require 'smtp_protocol'
+
+class SmtpProtocolTest < Test::Unit::TestCase
+
+ def test_protocol_sets_protocol_property_on_each_state
+ init_state = TestState.new
+
+ protocol = SmtpProtocol.new(:init, {:init => init_state})
+
+ assert_same protocol, init_state.protocol
+ end
+
+ def test_protocol_starts_in_the_initial_state
+ init_state = TestState.new
+
+ protocol = SmtpProtocol.new(:init, {:init => init_state})
+ protocol.serve(nil)
+
+ assert init_state.called
+ end
+
+ def test_state_is_changed_when_a_new_state_is_set
+ next_state = TestState.new
+
+ protocol = SmtpProtocol.new(:init, {:init => TestState.new, :next => next_state})
+ protocol.state = :next
+ protocol.serve(nil)
+
+ assert next_state.called
+ end
+
+ def test_listeners_are_notified_when_a_new_message_is_received
+ protocol = SmtpProtocol.new(:init, {:init => TestState.new})
+ observer = TestObserver.new
+ message = 'hi'
+
+ protocol.add_observer(observer)
+
+ protocol.new_message_received(message)
+
+ assert_same message, observer.received_message
+ end
+
+end
+
+class TestObserver
+ attr_accessor :received_message
+
+ def update(message)
+ @received_message = message
+ end
+end
+
+class TestState
+ attr_accessor :called, :protocol
+
+ def serve(io)
+ @called = true
+ end
+
+end
View
217 test/smtp_states_test.rb
@@ -0,0 +1,217 @@
+$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'test/unit'
+require 'stringio'
+require 'smtp_protocol'
+
+class SmtpStatesTest < Test::Unit::TestCase
+
+ def setup
+ buffer = ''
+ @server_stream = StringIO.new(buffer)
+ @client_stream = StringIO.new(buffer)
+ @protocol = TestProtocol.new
+ end
+
+ def test_greeting_is_returned_upon_initial_client_connection
+ init_state = InitState.new(@protocol)
+ init_state.serve(@server_stream)
+
+ assert_equal "220 ruby ESMTP\n", @client_stream.readline
+ end
+
+ def test_initial_state_passes_protocol_connect_as_the_next_state_in_the_chain
+ init_state = InitState.new(@protocol)
+ init_state.serve(@server_stream)
+
+ assert_equal :connect, @protocol.state
+ end
+
+ def test_initial_state_passes_protocol_the_same_io_as_it_received
+ init_state = InitState.new(@protocol)
+ init_state.serve(@server_stream)
+
+ assert_same @server_stream, @protocol.io
+ end
+
+ def test_initial_state_raises_not_initialized_when_protocol_is_not_set
+ init_state = InitState.new
+
+ assert_raise NotInitializedError do
+ init_state.serve(@server_stream)
+ end
+ end
+
+ def test_helo_is_accepted_while_in_connect_state
+ @client_stream.puts "HELO test.client"
+ connect_state = ConnectState.new(@protocol)
+
+ connect_state.serve(@server_stream)
+
+ assert_equal "250 ruby\n", @client_stream.readline
+ end
+
+ def test_connect_state_passes_protocol_connected_as_the_next_state_in_the_chain
+ @client_stream.puts "HELO test.client"
+ connect_state = ConnectState.new(@protocol)
+
+ connect_state.serve(@server_stream)
+
+ assert_equal :connected, @protocol.state
+ end
+
+ def test_connect_state_passes_protocol_the_same_io_as_it_received
+ @client_stream.puts "HELO test.client"
+ connect_state = ConnectState.new(@protocol)
+
+ connect_state.serve(@server_stream)
+
+ assert_same @server_stream, @protocol.io
+ end
+
+ def test_connect_state_raises_not_initialized_when_protocol_is_not_set
+ connect_state = ConnectState.new
+
+ assert_raise NotInitializedError do
+ connect_state.serve(@server_stream)
+ end
+ end
+
+ def test_from_okayed_while_in_connected_state
+ @client_stream.puts "MAIL FROM:<adam@esterlines.com>"
+ connected_state = ConnectedState.new(@protocol)
+
+ connected_state.serve(@server_stream)
+
+ assert_equal "250 ok\n", @client_stream.readline
+ end
+
+ def test_connected_state_passes_protocol_connected_as_the_next_state_when_client_sends_from_request
+ @client_stream.puts "MAIL FROM:<junk@junkster.com>"
+ connected_state = ConnectedState.new(@protocol)
+
+ connected_state.serve(@server_stream)
+
+ assert_equal :connected, @protocol.state
+ end
+
+ def test_rcpt_okayed_while_in_connected_state
+ @client_stream.puts "RCPT TO:<junk@junkster.com>"
+ connected_state = ConnectedState.new(@protocol)
+
+ connected_state.serve(@server_stream)
+
+ assert_equal "250 ok\n", @client_stream.readline
+ end
+
+ def test_connected_state_passes_protocol_connected_as_the_next_state_when_client_sends_rcpt_request
+ @client_stream.puts "RCPT TO:<foo@foo.com>"
+ connected_state = ConnectedState.new(@protocol)
+
+ connected_state.serve(@server_stream)
+
+ assert_equal :connected, @protocol.state
+ end
+
+ def test_connected_state_passes_protocol_the_same_io_as_it_received
+ @client_stream.puts "MAIL FROM:<foo@foo.com>"
+ connected_state = ConnectedState.new(@protocol)
+
+ connected_state.serve(@server_stream)
+
+ assert_same @server_stream, @protocol.io
+ end
+
+ def test_data_request_given_the_go_ahead_while_in_connected_state
+ @client_stream.puts "DATA"
+ connected_state = ConnectedState.new(@protocol)
+
+ connected_state.serve(@server_stream)
+
+ assert_equal "354 go ahead\n", @client_stream.readline
+ end
+
+ def test_connected_state_passes_protocol_read_mail_as_the_next_state_when_client_sends_data_request
+ @client_stream.puts "DATA"
+ connected_state = ConnectedState.new(@protocol)
+
+ connected_state.serve(@server_stream)
+
+ assert_equal :read_mail, @protocol.state
+ end
+
+ def test_connected_state_raises_not_initialized_when_protocol_is_not_set
+ connected_state = ConnectedState.new
+
+ assert_raise NotInitializedError do
+ connected_state.serve(@server_stream)
+ end
+ end
+
+ def test_read_mail_state_reads_until_a_single_dot_is_found_on_a_line_then_returns_an_ok_message
+ @client_stream.puts "To: junk@junk.com\nFrom: junk2@junk2.com\n\nHi\n.\n"
+ read_mail_state = ReadMailState.new(@protocol)
+
+ read_mail_state.serve(@server_stream)
+
+ assert_equal "250 ok\n", @client_stream.readline
+ end
+
+ def test_read_mail_state_passes_read_message_to_protocol
+ message = "To: junk@junk.com\nFrom: junk2@junk2.com\n\nHi\n"
+ @client_stream.puts "#{message}.\n"
+ read_mail_state = ReadMailState.new(@protocol)
+
+ read_mail_state.serve(@server_stream)
+
+ assert_equal message, @protocol.new_message
+ end
+
+ def test_read_mail_state_passes_protocol_quit_as_the_next_state_when_mail_message_is_read
+ @client_stream.puts "To: junk@junk.com\nFrom: junk2@junk2.com\n\nHi\n.\n"
+ read_mail_state = ReadMailState.new(@protocol)
+
+ read_mail_state.serve(@server_stream)
+
+ assert_equal :quit, @protocol.state
+ end
+
+ def test_read_mail_state_raises_not_initialized_when_protocol_is_not_set
+ read_mail_state = ReadMailState.new
+
+ assert_raise NotInitializedError do
+ read_mail_state.serve(@server_stream)
+ end
+ end
+
+ def test_quit_state_reads_client_quit_and_says_goodbye
+ @client_stream.puts "QUIT"
+ quit_state = QuitState.new(@protocol)
+
+ quit_state.serve(@server_stream)
+
+ assert_equal "221 ruby goodbye\n", @client_stream.readline
+ end
+
+ def test_quit_state_raises_not_initialized_when_protocol_is_not_set
+ quit_state = QuitState.new
+
+ assert_raise NotInitializedError do
+ quit_state.serve(@server_stream)
+ end
+ end
+
+end
+
+class TestProtocol
+ attr_accessor :state, :io
+ attr_reader :new_message
+
+ def serve (io)
+ @io = io
+ end
+
+ def new_message_received(message)
+ @new_message = message
+ end
+end
View
4 vendor/tmail.rb
@@ -0,0 +1,4 @@
+require 'tmail/info'
+require 'tmail/mail'
+require 'tmail/mailbox'
+require 'tmail/obsolete'
View
3  vendor/tmail/.cvsignore
@@ -0,0 +1,3 @@
+parser.rb
+output.parser
+stringio.rb
View
19 vendor/tmail/Makefile
@@ -0,0 +1,19 @@
+#
+# lib/tmail/Makefile
+#
+
+debug:
+ rm -f parser.rb
+ make parser.rb DEBUG=true
+
+parser.rb: parser.y
+ if [ "$(DEBUG)" = true ]; then \
+ racc -v -g -o$@ parser.y ;\
+ else \
+ racc -E -o$@ parser.y ;\
+ fi
+
+clean:
+ rm -f parser.rb parser.output
+
+distclean: clean
View
222 vendor/tmail/address.rb
@@ -0,0 +1,222 @@
+#
+# address.rb
+#
+# Copyright (c) 1998-2004 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2.1.
+#
+
+require 'tmail/encode'
+require 'tmail/parser'
+
+module TMail
+
+ class Address
+
+ include TextUtils
+
+ def Address.parse(str)
+ Parser.parse :ADDRESS, str
+ end
+
+ def address_group?
+ false
+ end
+
+ def initialize(local, domain)
+ if domain
+ domain.each do |s|
+ raise SyntaxError, 'empty word in domain' if s.empty?
+ end
+ end
+ @local = local
+ @domain = domain
+ @name = nil
+ @routes = []
+ end
+
+ attr_reader :name
+
+ def name=(str)
+ @name = str
+ @name = nil if str and str.empty?
+ end
+
+ alias phrase name
+ alias phrase= name=
+
+ attr_reader :routes
+
+ def inspect
+ "#<#{self.class} #{address()}>"
+ end
+
+ def local
+ return nil unless @local
+ return '""' if @local.size == 1 and @local[0].empty?
+ @local.map {|i| quote_atom(i) }.join('.')
+ end
+
+ def domain
+ return nil unless @domain
+ join_domain(@domain)
+ end
+
+ def spec
+ s = self.local
+ d = self.domain
+ if s and d
+ s + '@' + d
+ else
+ s
+ end
+ end
+
+ alias address spec
+
+
+ def ==(other)
+ other.respond_to? :spec and self.spec == other.spec
+ end
+
+ alias eql? ==
+
+ def hash
+ @local.hash ^ @domain.hash
+ end
+
+ def dup
+ obj = self.class.new(@local.dup, @domain.dup)
+ obj.name = @name.dup if @name
+ obj.routes.replace @routes
+ obj
+ end
+
+ include StrategyInterface
+
+ def accept(strategy, dummy1 = nil, dummy2 = nil)
+ unless @local
+ strategy.meta '<>' # empty return-path
+ return
+ end
+
+ spec_p = (not @name and @routes.empty?)
+ if @name
+ strategy.phrase @name
+ strategy.space
+ end
+ tmp = spec_p ? '' : '<'
+ unless @routes.empty?
+ tmp << @routes.map {|i| '@' + i }.join(',') << ':'
+ end
+ tmp << self.spec
+ tmp << '>' unless spec_p
+ strategy.meta tmp
+ strategy.lwsp ''
+ end
+
+ end
+
+
+ class AddressGroup
+
+ include Enumerable
+
+ def address_group?
+ true
+ end
+
+ def initialize(name, addrs)
+ @name = name
+ @addresses = addrs
+ end
+
+ attr_reader :name
+
+ def ==(other)
+ other.respond_to? :to_a and @addresses == other.to_a
+ end
+
+ alias eql? ==
+
+ def hash
+ map {|i| i.hash }.hash
+ end
+
+ def [](idx)
+ @addresses[idx]
+ end
+
+ def size
+ @addresses.size
+ end
+
+ def empty?
+ @addresses.empty?
+ end
+
+ def each(&block)
+ @addresses.each(&block)
+ end
+
+ def to_a
+ @addresses.dup
+ end
+
+ alias to_ary to_a
+
+ def include?(a)
+ @addresses.include? a
+ end
+
+ def flatten
+ set = []
+ @addresses.each do |a|
+ if a.respond_to? :flatten
+ set.concat a.flatten
+ else
+ set.push a
+ end
+ end
+ set
+ end
+
+ def each_address(&block)
+ flatten.each(&block)
+ end
+
+ def add(a)
+ @addresses.push a
+ end
+
+ alias push add
+
+ def delete(a)
+ @addresses.delete a
+ end
+
+ include StrategyInterface
+
+ def accept(strategy, dummy1 = nil, dummy2 = nil)
+ strategy.phrase @name
+ strategy.meta ':'
+ strategy.space
+ first = true
+ each do |mbox|
+ if first
+ first = false
+ else
+ strategy.meta ','
+ end
+ strategy.space
+ mbox.accept strategy
+ end
+ strategy.meta ';'
+ strategy.lwsp ''
+ end
+
+ end
+
+end # module TMail
View
52 vendor/tmail/base64.rb
@@ -0,0 +1,52 @@
+#
+# base64.rb
+#
+# Copyright (c) 1998-2004 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2.1.
+#
+
+module TMail
+
+ module Base64
+
+ module_function
+
+ def rb_folding_encode(str, eol = "\n", limit = 60)
+ [str].pack('m')
+ end
+
+ def rb_encode(str)
+ [str].pack('m').tr( "\r\n", '' )
+ end
+
+ def rb_decode(str, strict = false)
+ str.unpack('m')
+ end
+
+ begin
+ require 'tmail/base64.so'
+ alias folding_encode c_folding_encode
+ alias encode c_encode
+ alias decode c_decode
+ class << self
+ alias folding_encode c_folding_encode
+ alias encode c_encode
+ alias decode c_decode
+ end
+ rescue LoadError
+ alias folding_encode rb_folding_encode
+ alias encode rb_encode
+ alias decode rb_decode
+ class << self
+ alias folding_encode rb_folding_encode
+ alias encode rb_encode
+ alias decode rb_decode
+ end
+ end
+
+ end
+
+end
View
39 vendor/tmail/compat.rb
@@ -0,0 +1,39 @@
+unless Enumerable.method_defined?(:map)
+ module Enumerable
+ alias map collect
+ end
+end
+
+unless Enumerable.method_defined?(:select)
+ module Enumerable
+ alias select find_all
+ end
+end
+
+unless Enumerable.method_defined?(:reject)
+ module Enumerable
+ def reject
+ result = []
+ each do |i|
+ result.push i unless yield(i)
+ end
+ result
+ end
+ end
+end
+
+unless Enumerable.method_defined?(:sort_by)
+ module Enumerable
+ def sort_by
+ map {|i| [yield(i), i] }.sort.map {|val, i| i }
+ end
+ end
+end
+
+unless File.respond_to?(:read)
+ def File.read(fname)
+ File.open(fname) {|f|
+ return f.read
+ }
+ end
+end
View
50 vendor/tmail/config.rb
@@ -0,0 +1,50 @@
+#
+# config.rb
+#
+# Copyright (c) 1998-2004 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2.1.
+#
+
+module TMail
+
+ class Config
+
+ def initialize(strict)
+ @strict_parse = strict
+ @strict_base64decode = strict
+ end
+
+ def strict_parse?
+ @strict_parse
+ end
+
+ attr_writer :strict_parse
+
+ def strict_base64decode?
+ @strict_base64decode
+ end
+
+ attr_writer :strict_base64decode
+
+ def new_body_port(mail)
+ StringPort.new
+ end
+
+ alias new_preamble_port new_body_port
+ alias new_part_port new_body_port
+
+ end
+
+ DEFAULT_CONFIG = Config.new(false)
+ DEFAULT_STRICT_CONFIG = Config.new(true)
+
+ def Config.to_config(arg)
+ return DEFAULT_STRICT_CONFIG if arg == true
+ return DEFAULT_CONFIG if arg == false
+ arg or DEFAULT_CONFIG
+ end
+
+end
View
447 vendor/tmail/encode.rb
@@ -0,0 +1,447 @@
+#
+# encode.rb
+#
+# Copyright (c) 1998-2004 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2.1.
+#
+
+require 'nkf'
+require 'tmail/base64.rb'
+require 'tmail/stringio'
+require 'tmail/textutils'
+
+
+module TMail
+
+ module StrategyInterface
+
+ def create_dest(obj)
+ case obj
+ when nil
+ StringOutput.new
+ when String
+ StringOutput.new(obj)
+ when IO, StringOutput
+ obj
+ else
+ raise TypeError, 'cannot handle this type of object for dest'
+ end
+ end
+ module_function :create_dest
+
+ def encoded(eol = "\r\n", charset = 'j', dest = nil)
+ accept_strategy Encoder, eol, charset, dest
+ end
+
+ def decoded(eol = "\n", charset = 'e', dest = nil)
+ accept_strategy Decoder, eol, charset, dest
+ end
+
+ alias to_s decoded
+
+ def accept_strategy(klass, eol, charset, dest = nil)
+ dest ||= ''
+ accept klass.new(create_dest(dest), charset, eol)
+ dest
+ end
+
+ end
+
+
+ ###
+ ### MIME B encoding decoder
+ ###
+
+ class Decoder
+
+ include TextUtils
+
+ encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
+ ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
+
+ OUTPUT_ENCODING = {
+ 'EUC' => 'e',
+ 'SJIS' => 's',
+ }
+
+ def self.decode(str, encoding = nil)
+ encoding ||= (OUTPUT_ENCODING[$KCODE] || 'j')
+ opt = '-m' + encoding
+ str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
+ end
+
+ def initialize(dest, encoding = nil, eol = "\n")
+ @f = StrategyInterface.create_dest(dest)
+ @encoding = (/\A[ejs]/ =~ encoding) ? encoding[0,1] : nil
+ @eol = eol
+ end
+
+ def decode(str)
+ self.class.decode(str, @encoding)
+ end
+ private :decode
+
+ def terminate
+ end
+
+ def header_line(str)
+ @f << decode(str)
+ end
+
+ def header_name(nm)
+ @f << nm << ': '
+ end
+
+ def header_body(str)
+ @f << decode(str)
+ end
+
+ def space
+ @f << ' '
+ end
+
+ alias spc space
+
+ def lwsp(str)
+ @f << str
+ end
+
+ def meta(str)
+ @f << str
+ end
+
+ def text(str)
+ @f << decode(str)
+ end
+
+ def phrase(str)
+ @f << quote_phrase(decode(str))
+ end
+
+ def kv_pair(k, v)
+ @f << k << '=' << v
+ end
+
+ def puts(str = nil)
+ @f << str if str
+ @f << @eol
+ end
+
+ def write(str)
+ @f << str
+ end
+
+ end
+
+
+ ###
+ ### MIME B-encoding encoder
+ ###
+
+ #
+ # FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
+ #
+ class Encoder
+
+ include TextUtils
+
+ BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
+
+ def Encoder.encode(str)
+ e = new()
+ e.header_body str
+ e.terminate
+ e.dest.string
+ end
+
+ SPACER = "\t"
+ MAX_LINE_LEN = 70
+
+ OPTIONS = {
+ 'EUC' => '-Ej -m0',
+ 'SJIS' => '-Sj -m0',
+ 'UTF8' => nil, # FIXME
+ 'NONE' => nil
+ }
+
+ def initialize(dest = nil, encoding = nil, eol = "\r\n", limit = nil)
+ @f = StrategyInterface.create_dest(dest)
+ @opt = OPTIONS[$KCODE]
+ @eol = eol
+ reset
+ end
+
+ def normalize_encoding(str)
+ if @opt
+ then NKF.nkf(@opt, str)
+ else str
+ end
+ end
+
+ def reset
+ @text = ''
+ @lwsp = ''
+ @curlen = 0
+ end
+
+ def terminate
+ add_lwsp ''
+ reset
+ end
+
+ def dest
+ @f
+ end
+
+ def puts(str = nil)
+ @f << str if str
+ @f << @eol
+ end
+
+ def write(str)
+ @f << str
+ end
+
+ #
+ # add
+ #
+
+ def header_line(line)
+ scanadd line
+ end
+
+ def header_name(name)
+ add_text name.split(/-/).map {|i| i.capitalize }.join('-')
+ add_text ':'
+ add_lwsp ' '
+ end
+
+ def header_body(str)
+ scanadd normalize_encoding(str)
+ end
+
+ def space
+ add_lwsp ' '
+ end
+
+ alias spc space
+
+ def lwsp(str)
+ add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
+ end
+
+ def meta(str)
+ add_text str
+ end
+
+ def text(str)
+ scanadd normalize_encoding(str)
+ end
+
+ def phrase(str)
+ str = normalize_encoding(str)
+ if CONTROL_CHAR =~ str
+ scanadd str
+ else
+ add_text quote_phrase(str)
+ end
+ end
+
+ # FIXME: implement line folding
+ #
+ def kv_pair(k, v)
+ v = normalize_encoding(v)
+ if token_safe?(v)
+ add_text k + '=' + v
+ elsif not CONTROL_CHAR =~ v
+ add_text k + '=' + quote_token(v)
+ else
+ # apply RFC2231 encoding
+ kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
+ add_text kv
+ end
+ end
+
+ def encode_value(str)
+ str.gsub(RFC2231_UNSAFE) {|s| '%%%02X' % s[0] }
+ end
+
+ private
+
+ def scanadd(str, force = false)
+ types = ''
+ strs = []
+
+ until str.empty?
+ if m = /\A[^\e\t\r\n ]+/.match(str)
+ types << (force ? 'j' : 'a')
+ strs.push m[0]
+
+ elsif m = /\A[\t\r\n ]+/.match(str)
+ types << 's'
+ strs.push m[0]
+
+ elsif m = /\A\e../.match(str)
+ esc = m[0]
+ str = m.post_match
+ if esc != "\e(B" and m = /\A[^\e]+/.match(str)
+ types << 'j'
+ strs.push m[0]
+ end
+
+ else
+ raise 'TMail FATAL: encoder scan fail'
+ end
+ str = m.post_match
+ end
+
+ do_encode types, strs
+ end
+
+ def do_encode(types, strs)
+ #
+ # result : (A|E)(S(A|E))*
+ # E : W(SW)*
+ # W : (J|A)+ but must contain J # (J|A)*J(J|A)*
+ # A : <<A character string not to be encoded>>
+ # J : <<A character string to be encoded>>
+ # S : <<LWSP>>
+ #
+ # An encoding unit is `E'.
+ # Input (parameter `types') is (J|A)(J|A|S)*(J|A)
+ #
+ if BENCODE_DEBUG
+ puts
+ puts '-- do_encode ------------'
+ puts types.split(//).join(' ')
+ p strs
+ end
+
+ e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
+
+ while m = e.match(types)
+ pre = m.pre_match
+ concat_A_S pre, strs[0, pre.size] unless pre.empty?
+ concat_E m[0], strs[m.begin(0) ... m.end(0)]
+ types = m.post_match
+ strs.slice! 0, m.end(0)
+ end
+ concat_A_S types, strs
+ end
+
+ def concat_A_S(types, strs)
+ i = 0
+ types.each_byte do |t|
+ case t
+ when ?a then add_text strs[i]
+ when ?s then add_lwsp strs[i]
+ else
+ raise "TMail FATAL: unknown flag: #{t.chr}"
+ end
+ i += 1
+ end
+ end
+
+ METHOD_ID = {
+ ?j => :extract_J,
+ ?e => :extract_E,
+ ?a => :extract_A,
+ ?s => :extract_S
+ }
+
+ def concat_E(types, strs)
+ if BENCODE_DEBUG
+ puts '---- concat_E'
+ puts "types=#{types.split(//).join(' ')}"
+ puts "strs =#{strs.inspect}"
+ end
+
+ flush() unless @text.empty?
+
+ chunk = ''
+ strs.each_with_index do |s,i|
+ mid = METHOD_ID[types[i]]
+ until s.empty?
+ unless c = __send__(mid, chunk.size, s)
+ add_with_encode chunk unless chunk.empty?
+ flush
+ chunk = ''
+ fold
+ c = __send__(mid, 0, s)
+ raise 'TMail FATAL: extract fail' unless c
+ end
+ chunk << c
+ end
+ end
+ add_with_encode chunk unless chunk.empty?
+ end
+
+ def extract_J(chunksize, str)
+ size = max_bytes(chunksize, str.size) - 6
+ size = (size % 2 == 0) ? (size) : (size - 1)
+ return nil if size <= 0
+ "\e$B#{str.slice!(0, size)}\e(B"
+ end
+
+ def extract_A(chunksize, str)
+ size = max_bytes(chunksize, str.size)
+ return nil if size <= 0
+ str.slice!(0, size)
+ end
+
+ alias extract_S extract_A
+
+ def max_bytes(chunksize, ssize)
+ (restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
+ end
+
+ #
+ # free length buffer
+ #
+
+ def add_text(str)
+ @text << str
+ # puts '---- text -------------------------------------'
+ # puts "+ #{str.inspect}"
+ # puts "txt >>>#{@text.inspect}<<<"
+ end
+
+ def add_with_encode(str)
+ @text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
+ end
+
+ def add_lwsp(lwsp)
+ # puts '---- lwsp -------------------------------------'
+ # puts "+ #{lwsp.inspect}"
+ fold if restsize() <= 0
+ flush
+ @lwsp = lwsp
+ end
+
+ def flush
+ # puts '---- flush ----'
+ # puts "spc >>>#{@lwsp.inspect}<<<"
+ # puts "txt >>>#{@text.inspect}<<<"
+ @f << @lwsp << @text
+ @curlen += (@lwsp.size + @text.size)
+ @text = ''
+ @lwsp = ''
+ end
+
+ def fold
+ # puts '---- fold ----'
+ @f << @eol
+ @curlen = 0
+ @lwsp = SPACER
+ end
+
+ def restsize
+ MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
+ end
+
+ end
+
+end # module TMail
View
895 vendor/tmail/header.rb
@@ -0,0 +1,895 @@
+#
+# header.rb
+#
+# Copyright (c) 1998-2004 Minero Aoki
+#
+# This program is free software.
+# You can distribute/modify this program under the terms of
+# the GNU Lesser General Public License version 2.1.
+#
+
+require 'tmail/encode'
+require 'tmail/address'
+require 'tmail/parser'
+require 'tmail/config'
+require 'tmail/textutils'
+
+
+module TMail
+
+ class HeaderField
+
+ include TextUtils
+
+ class << self
+
+ alias newobj new
+
+ def new(name, body, conf = DEFAULT_CONFIG)
+ klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
+ klass.newobj body, conf
+ end
+
+ def new_from_port(port, name, conf = DEFAULT_CONFIG)
+ re = Regexp.new('\A(' + Regexp.quote(name) + '):', 'i')
+ str = nil
+ port.ropen {|f|
+ f.each do |line|
+ if m = re.match(line) then str = m.post_match.strip
+ elsif str and /\A[\t ]/ =~ line then str << ' ' << line.strip
+ elsif /\A-*\s*\z/ =~ line then break
+ elsif str then break
+ end
+ end
+ }
+ new(name, str, Config.to_config(conf))
+ end
+
+ def internal_new(name, conf)
+ FNAME_TO_CLASS[name].newobj('', conf, true)
+ end
+
+ end # class << self
+
+ def initialize(body, conf, intern = false)
+ @body = body
+ @config = conf
+
+ @illegal = false
+ @parsed = false
+ if intern
+ @parsed = true
+ parse_init
+ end
+ end
+
+ def inspect
+ "#<#{self.class} #{@body.inspect}>"
+ end
+
+ def illegal?
+ @illegal
+ end
+
+ def empty?
+ ensure_parsed
+ return true if @illegal
+ isempty?
+ end
+
+ private
+
+ def ensure_parsed
+ return if @parsed
+ @parsed = true
+ parse
+ end
+
+ # defabstract parse
+ # end
+
+ def clear_parse_status
+ @parsed = false
+ @illegal = false
+ end
+
+ public
+
+ def body
+ ensure_parsed
+ v = Decoder.new(s = '')
+ do_accept v
+ v.terminate
+ s
+ end
+
+ def body=(str)
+ @body = str
+ clear_parse_status
+ end
+
+ include StrategyInterface
+
+ def accept(strategy, dummy1 = nil, dummy2 = nil)
+ ensure_parsed
+ do_accept strategy
+ strategy.terminate
+ end
+
+ # abstract do_accept
+
+ end
+
+
+ class UnstructuredHeader < HeaderField
+
+ def body
+ ensure_parsed
+ @body
+ end
+
+ def body=(arg)
+ ensure_parsed
+ @body = arg
+ end
+
+ private
+
+ def parse_init
+ end
+
+ def parse
+ @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
+ end
+
+ def isempty?
+ not @body
+ end
+
+ def do_accept(strategy)
+ strategy.text @body
+ end
+
+ end
+
+
+ class StructuredHeader < HeaderField
+
+ def comments
+ ensure_parsed
+ @comments
+ end
+
+ private
+
+ def parse
+ save = nil
+
+ begin
+ parse_init
+ do_parse
+ rescue SyntaxError
+ if not save and mime_encoded? @body
+ save = @body
+ @body = Decoder.decode(save)
+ retry
+ elsif save
+ @body = save
+ end
+
+ @illegal = true
+ raise if @config.strict_parse?
+ end
+ end
+
+ def parse_init
+ @comments = []
+ init
+ end
+
+ def do_parse
+ obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
+ set obj if obj
+ end
+
+ end
+
+
+ class DateTimeHeader < StructuredHeader
+
+ PARSE_TYPE = :DATETIME
+
+ def date
+ ensure_parsed
+ @date
+ end
+
+ def date=(arg)
+ ensure_parsed
+ @date = arg
+ end
+
+ private
+
+ def init
+ @date = nil
+ end
+
+ def set(t)
+ @date = t
+ end
+
+ def isempty?
+ not @date
+ end
+
+ def do_accept(strategy)
+ strategy.meta time2str(@date)
+ end
+
+ end
+
+
+ class AddressHeader < StructuredHeader
+
+ PARSE_TYPE = :MADDRESS
+
+ def addrs
+ ensure_parsed
+ @addrs
+ end
+
+ private
+
+ def init
+ @addrs = []
+ end
+
+ def set(a)
+ @addrs = a
+ end
+
+ def isempty?
+ @addrs.empty?
+ end
+
+ def do_accept(strategy)
+ first = true
+ @addrs.each do |a|
+ if first
+ first = false
+ else
+ strategy.meta ','
+ strategy.space
+ end
+ a.accept strategy
+ end
+
+ @comments.each do |c|
+ strategy.space
+ strategy.meta '('
+ strategy.text c
+ strategy.meta ')'
+ end
+ end
+
+ end
+
+
+ class ReturnPathHeader < AddressHeader
+
+ PARSE_TYPE = :RETPATH
+
+ def addr
+ addrs()[0]
+ end
+
+ def spec
+ a = addr() or return nil
+ a.spec
+ end
+
+ def routes
+ a = addr() or return nil
+ a.routes
+ end
+
+ private
+
+ def do_accept(strategy)
+ a = addr()
+
+ strategy.meta '<'
+ unless a.routes.empty?
+ strategy.meta a.routes.map {|i| '@' + i }.join(',')
+ strategy.meta ':'
+ end
+ spec = a.spec
+ strategy.meta spec if spec
+ strategy.meta '>'
+ end
+
+ end
+
+
+ class SingleAddressHeader < AddressHeader
+
+ def addr
+ addrs()[0]
+ end
+
+ private
+