Codebase list feed2imap / upstream/1.0
Imported Upstream version 1.0 Lucas Nussbaum 13 years ago
32 changed file(s) with 8195 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 GNU GENERAL PUBLIC LICENSE
1 Version 2, June 1991
2
3 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
4 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
5 Everyone is permitted to copy and distribute verbatim copies
6 of this license document, but changing it is not allowed.
7
8 Preamble
9
10 The licenses for most software are designed to take away your
11 freedom to share and change it. By contrast, the GNU General Public
12 License is intended to guarantee your freedom to share and change free
13 software--to make sure the software is free for all its users. This
14 General Public License applies to most of the Free Software
15 Foundation's software and to any other program whose authors commit to
16 using it. (Some other Free Software Foundation software is covered by
17 the GNU Library General Public License instead.) You can apply it to
18 your programs, too.
19
20 When we speak of free software, we are referring to freedom, not
21 price. Our General Public Licenses are designed to make sure that you
22 have the freedom to distribute copies of free software (and charge for
23 this service if you wish), that you receive source code or can get it
24 if you want it, that you can change the software or use pieces of it
25 in new free programs; and that you know you can do these things.
26
27 To protect your rights, we need to make restrictions that forbid
28 anyone to deny you these rights or to ask you to surrender the rights.
29 These restrictions translate to certain responsibilities for you if you
30 distribute copies of the software, or if you modify it.
31
32 For example, if you distribute copies of such a program, whether
33 gratis or for a fee, you must give the recipients all the rights that
34 you have. You must make sure that they, too, receive or can get the
35 source code. And you must show them these terms so they know their
36 rights.
37
38 We protect your rights with two steps: (1) copyright the software, and
39 (2) offer you this license which gives you legal permission to copy,
40 distribute and/or modify the software.
41
42 Also, for each author's protection and ours, we want to make certain
43 that everyone understands that there is no warranty for this free
44 software. If the software is modified by someone else and passed on, we
45 want its recipients to know that what they have is not the original, so
46 that any problems introduced by others will not reflect on the original
47 authors' reputations.
48
49 Finally, any free program is threatened constantly by software
50 patents. We wish to avoid the danger that redistributors of a free
51 program will individually obtain patent licenses, in effect making the
52 program proprietary. To prevent this, we have made it clear that any
53 patent must be licensed for everyone's free use or not licensed at all.
54
55 The precise terms and conditions for copying, distribution and
56 modification follow.
57
58 GNU GENERAL PUBLIC LICENSE
59 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
60
61 0. This License applies to any program or other work which contains
62 a notice placed by the copyright holder saying it may be distributed
63 under the terms of this General Public License. The "Program", below,
64 refers to any such program or work, and a "work based on the Program"
65 means either the Program or any derivative work under copyright law:
66 that is to say, a work containing the Program or a portion of it,
67 either verbatim or with modifications and/or translated into another
68 language. (Hereinafter, translation is included without limitation in
69 the term "modification".) Each licensee is addressed as "you".
70
71 Activities other than copying, distribution and modification are not
72 covered by this License; they are outside its scope. The act of
73 running the Program is not restricted, and the output from the Program
74 is covered only if its contents constitute a work based on the
75 Program (independent of having been made by running the Program).
76 Whether that is true depends on what the Program does.
77
78 1. You may copy and distribute verbatim copies of the Program's
79 source code as you receive it, in any medium, provided that you
80 conspicuously and appropriately publish on each copy an appropriate
81 copyright notice and disclaimer of warranty; keep intact all the
82 notices that refer to this License and to the absence of any warranty;
83 and give any other recipients of the Program a copy of this License
84 along with the Program.
85
86 You may charge a fee for the physical act of transferring a copy, and
87 you may at your option offer warranty protection in exchange for a fee.
88
89 2. You may modify your copy or copies of the Program or any portion
90 of it, thus forming a work based on the Program, and copy and
91 distribute such modifications or work under the terms of Section 1
92 above, provided that you also meet all of these conditions:
93
94 a) You must cause the modified files to carry prominent notices
95 stating that you changed the files and the date of any change.
96
97 b) You must cause any work that you distribute or publish, that in
98 whole or in part contains or is derived from the Program or any
99 part thereof, to be licensed as a whole at no charge to all third
100 parties under the terms of this License.
101
102 c) If the modified program normally reads commands interactively
103 when run, you must cause it, when started running for such
104 interactive use in the most ordinary way, to print or display an
105 announcement including an appropriate copyright notice and a
106 notice that there is no warranty (or else, saying that you provide
107 a warranty) and that users may redistribute the program under
108 these conditions, and telling the user how to view a copy of this
109 License. (Exception: if the Program itself is interactive but
110 does not normally print such an announcement, your work based on
111 the Program is not required to print an announcement.)
112
113 These requirements apply to the modified work as a whole. If
114 identifiable sections of that work are not derived from the Program,
115 and can be reasonably considered independent and separate works in
116 themselves, then this License, and its terms, do not apply to those
117 sections when you distribute them as separate works. But when you
118 distribute the same sections as part of a whole which is a work based
119 on the Program, the distribution of the whole must be on the terms of
120 this License, whose permissions for other licensees extend to the
121 entire whole, and thus to each and every part regardless of who wrote it.
122
123 Thus, it is not the intent of this section to claim rights or contest
124 your rights to work written entirely by you; rather, the intent is to
125 exercise the right to control the distribution of derivative or
126 collective works based on the Program.
127
128 In addition, mere aggregation of another work not based on the Program
129 with the Program (or with a work based on the Program) on a volume of
130 a storage or distribution medium does not bring the other work under
131 the scope of this License.
132
133 3. You may copy and distribute the Program (or a work based on it,
134 under Section 2) in object code or executable form under the terms of
135 Sections 1 and 2 above provided that you also do one of the following:
136
137 a) Accompany it with the complete corresponding machine-readable
138 source code, which must be distributed under the terms of Sections
139 1 and 2 above on a medium customarily used for software interchange; or,
140
141 b) Accompany it with a written offer, valid for at least three
142 years, to give any third party, for a charge no more than your
143 cost of physically performing source distribution, a complete
144 machine-readable copy of the corresponding source code, to be
145 distributed under the terms of Sections 1 and 2 above on a medium
146 customarily used for software interchange; or,
147
148 c) Accompany it with the information you received as to the offer
149 to distribute corresponding source code. (This alternative is
150 allowed only for noncommercial distribution and only if you
151 received the program in object code or executable form with such
152 an offer, in accord with Subsection b above.)
153
154 The source code for a work means the preferred form of the work for
155 making modifications to it. For an executable work, complete source
156 code means all the source code for all modules it contains, plus any
157 associated interface definition files, plus the scripts used to
158 control compilation and installation of the executable. However, as a
159 special exception, the source code distributed need not include
160 anything that is normally distributed (in either source or binary
161 form) with the major components (compiler, kernel, and so on) of the
162 operating system on which the executable runs, unless that component
163 itself accompanies the executable.
164
165 If distribution of executable or object code is made by offering
166 access to copy from a designated place, then offering equivalent
167 access to copy the source code from the same place counts as
168 distribution of the source code, even though third parties are not
169 compelled to copy the source along with the object code.
170
171 4. You may not copy, modify, sublicense, or distribute the Program
172 except as expressly provided under this License. Any attempt
173 otherwise to copy, modify, sublicense or distribute the Program is
174 void, and will automatically terminate your rights under this License.
175 However, parties who have received copies, or rights, from you under
176 this License will not have their licenses terminated so long as such
177 parties remain in full compliance.
178
179 5. You are not required to accept this License, since you have not
180 signed it. However, nothing else grants you permission to modify or
181 distribute the Program or its derivative works. These actions are
182 prohibited by law if you do not accept this License. Therefore, by
183 modifying or distributing the Program (or any work based on the
184 Program), you indicate your acceptance of this License to do so, and
185 all its terms and conditions for copying, distributing or modifying
186 the Program or works based on it.
187
188 6. Each time you redistribute the Program (or any work based on the
189 Program), the recipient automatically receives a license from the
190 original licensor to copy, distribute or modify the Program subject to
191 these terms and conditions. You may not impose any further
192 restrictions on the recipients' exercise of the rights granted herein.
193 You are not responsible for enforcing compliance by third parties to
194 this License.
195
196 7. If, as a consequence of a court judgment or allegation of patent
197 infringement or for any other reason (not limited to patent issues),
198 conditions are imposed on you (whether by court order, agreement or
199 otherwise) that contradict the conditions of this License, they do not
200 excuse you from the conditions of this License. If you cannot
201 distribute so as to satisfy simultaneously your obligations under this
202 License and any other pertinent obligations, then as a consequence you
203 may not distribute the Program at all. For example, if a patent
204 license would not permit royalty-free redistribution of the Program by
205 all those who receive copies directly or indirectly through you, then
206 the only way you could satisfy both it and this License would be to
207 refrain entirely from distribution of the Program.
208
209 If any portion of this section is held invalid or unenforceable under
210 any particular circumstance, the balance of the section is intended to
211 apply and the section as a whole is intended to apply in other
212 circumstances.
213
214 It is not the purpose of this section to induce you to infringe any
215 patents or other property right claims or to contest validity of any
216 such claims; this section has the sole purpose of protecting the
217 integrity of the free software distribution system, which is
218 implemented by public license practices. Many people have made
219 generous contributions to the wide range of software distributed
220 through that system in reliance on consistent application of that
221 system; it is up to the author/donor to decide if he or she is willing
222 to distribute software through any other system and a licensee cannot
223 impose that choice.
224
225 This section is intended to make thoroughly clear what is believed to
226 be a consequence of the rest of this License.
227
228 8. If the distribution and/or use of the Program is restricted in
229 certain countries either by patents or by copyrighted interfaces, the
230 original copyright holder who places the Program under this License
231 may add an explicit geographical distribution limitation excluding
232 those countries, so that distribution is permitted only in or among
233 countries not thus excluded. In such case, this License incorporates
234 the limitation as if written in the body of this License.
235
236 9. The Free Software Foundation may publish revised and/or new versions
237 of the General Public License from time to time. Such new versions will
238 be similar in spirit to the present version, but may differ in detail to
239 address new problems or concerns.
240
241 Each version is given a distinguishing version number. If the Program
242 specifies a version number of this License which applies to it and "any
243 later version", you have the option of following the terms and conditions
244 either of that version or of any later version published by the Free
245 Software Foundation. If the Program does not specify a version number of
246 this License, you may choose any version ever published by the Free Software
247 Foundation.
248
249 10. If you wish to incorporate parts of the Program into other free
250 programs whose distribution conditions are different, write to the author
251 to ask for permission. For software which is copyrighted by the Free
252 Software Foundation, write to the Free Software Foundation; we sometimes
253 make exceptions for this. Our decision will be guided by the two goals
254 of preserving the free status of all derivatives of our free software and
255 of promoting the sharing and reuse of software generally.
256
257 NO WARRANTY
258
259 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
260 FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
261 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
262 PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
263 OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
264 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
265 TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
266 PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
267 REPAIR OR CORRECTION.
268
269 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
270 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
271 REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
272 INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
273 OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
274 TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
275 YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
276 PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
277 POSSIBILITY OF SUCH DAMAGES.
278
279 END OF TERMS AND CONDITIONS
280
281 How to Apply These Terms to Your New Programs
282
283 If you develop a new program, and you want it to be of the greatest
284 possible use to the public, the best way to achieve this is to make it
285 free software which everyone can redistribute and change under these terms.
286
287 To do so, attach the following notices to the program. It is safest
288 to attach them to the start of each source file to most effectively
289 convey the exclusion of warranty; and each file should have at least
290 the "copyright" line and a pointer to where the full notice is found.
291
292 <one line to give the program's name and a brief idea of what it does.>
293 Copyright (C) <year> <name of author>
294
295 This program is free software; you can redistribute it and/or modify
296 it under the terms of the GNU General Public License as published by
297 the Free Software Foundation; either version 2 of the License, or
298 (at your option) any later version.
299
300 This program is distributed in the hope that it will be useful,
301 but WITHOUT ANY WARRANTY; without even the implied warranty of
302 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
303 GNU General Public License for more details.
304
305 You should have received a copy of the GNU General Public License
306 along with this program; if not, write to the Free Software
307 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
308
309
310 Also add information on how to contact you by electronic and paper mail.
311
312 If the program is interactive, make it output a short notice like this
313 when it starts in an interactive mode:
314
315 Gnomovision version 69, Copyright (C) year name of author
316 Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 This is free software, and you are welcome to redistribute it
318 under certain conditions; type `show c' for details.
319
320 The hypothetical commands `show w' and `show c' should show the appropriate
321 parts of the General Public License. Of course, the commands you use may
322 be called something other than `show w' and `show c'; they could even be
323 mouse-clicks or menu items--whatever suits your program.
324
325 You should also get your employer (if you work as a programmer) or your
326 school, if any, to sign a "copyright disclaimer" for the program, if
327 necessary. Here is a sample; alter the names:
328
329 Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 `Gnomovision' (which makes passes at compilers) written by James Hacker.
331
332 <signature of Ty Coon>, 1 April 1989
333 Ty Coon, President of Vice
334
335 This General Public License does not permit incorporating your program into
336 proprietary programs. If your program is a subroutine library, you may
337 consider it more useful to permit linking proprietary applications with the
338 library. If this is what you want to do, use the GNU Library General
339 Public License instead of this License.
0 Feed2Imap 1.0 (18/04/2010)
1 ==========================
2 * Removed patch from Haegar as it's no longer needed with the new
3 rubyimap.rb (see discussion in https://gna.org/bugs/?13977)
4 * Support writing to maildirs instead of through IMAP
5 * Use Message-ID instead of X-CacheIndex
6 * Do not use acme.com
7 * Update rubyimap.rb
8 * Provide a way to disable SSL certification verification when
9 connecting to IMAPS accounts
10
11 Feed2Imap 0.9.4 (27/07/2009)
12 ============================
13 * Warn (INFO level, so only displayed with feed2imap -v) if fetching a
14 feed takes more than 10s, as this might cause massive delays
15 in feed2imap run times.
16 * Fix encoding of email headers
17 * Only include images once when used several times in the same item
18 * New version of Net::Imap
19 * Use Message-Id instead of X-CacheIndex
20 * Fix MIME formatting when including images
21 * Require ruby-feedparser 0.7, better email formatting
22 * Made it possible to re-use substrings in targets
23 * Fix buffering problem with filters
24 * Added a patch from Haegar to fix problem with dovecot 1.2.1
25
26 Feed2Imap 0.9.3 (23/07/2008)
27 ============================
28 * Check the return code from external commands, and warn if != 0. Fixes
29 Gna bug #10516.
30 * Support for including images in the mails (see include-images config
31 option). Based on a patch by Pavel Avgustinov, and with help from
32 Arnt Gulbrandsen.
33 * Dropped rubymail_patch.rb
34 * Added option to wrap text output. Thanks to Maxime Petazzoni for
35 the patch.
36 * When updating an email, remove the Recent flag.
37 * You need to use ruby-feedparser 0.6 or greater with that release.
38
39 Feed2Imap 0.9.2 (28/10/2007)
40 ============================
41 * resynchronized rubyimap.rb with stdlib, and fixed send! -> send.
42 * upload items in reverse order, to upload the older first
43 Closes Gna bug #8986. Thanks go do Rial Juan for the patch.
44 * Don't allow more than 16 fetchers to run at the same time.
45 16 should be a reasonable default for everybody.
46 Closes Gna #9032.
47 * Reduce the default HTTP timeout to 30s.
48 * Don't update the cache if uploading items failed (should avoid
49 missing some items).
50 * Safer cache writing. Should avoid some cache corruption problems.
51 * Now exits when we receive an IMAP error, instead of trying to recover. We
52 shouldn't receive IMAP errors anyway. Closes Debian #405070.
53 * Fixed content-type-encoding in HTML emails. Reported by Arnt Gulbrandse.
54
55 Feed2Imap 0.9.1 (15/05/2007)
56 ============================
57 * Fixed bug with folder creation.
58
59 Feed2Imap 0.9 (15/05/2007)
60 ============================
61 * Folder creation moved to upload. This should make feed2imap run
62 slightly faster.
63 * Per-feed dumpdir option (helps debugging)
64 * Now uses Content-Transfer-Encoding: 8bit (thanks Arnt Gulbrandsen
65 <arnt@gulbrandsen.priv.no>)
66 * Now supports Snowscripts, using the 'execurl' and 'filter' config
67 keywords. For more information, see the example configuration file.
68 * Slightly better option parsing. Thanks to Paul van Tilburg for the
69 patch.
70 * A debug mode was added, and the normal mode was improved, so it is
71 no longer necessary to redirect feed2imap output to /dev/null:
72 transient errors are only reported after they have happened a
73 certain number of times (default 10).
74 * An ignore-hash option was added for feeds whose content change all
75 the time.
76
77 Feed2Imap 0.8 (28/06/2006)
78 ============================
79 * Uses the http_proxy environment variable to determine the proxy server
80 if available. (fixes gna bug #5820, all credits go to Boyd Adamson
81 <boyd-adamson@usa.net>)
82 * Fixes flocking on Solaris (fixes gna bug #5819). Again, all credits go to
83 Boyd Adamson <boyd-adamson@usa.net>.
84 * Rewrite of the "find updated and new items" code. It should work much better
85 now. Also, a debug-updated configuration variable was added to make it
86 easier to debug those issues.
87 * New always-new flag in the config file to consider all items as new (for
88 feeds where items are wrongly marked as updated, e.g mediawiki feeds).
89 See example configuration file for more information (fixes Debian bug
90 #366878).
91 * When disconnecting from the IMAP server, don't display an exception in
92 non-verbose mode if the "connection is reset by peer" (fixes Debian bug
93 #367282).
94 * Handling of exceptions in needMIME (fixes gna bug #5872).
95
96 Feed2Imap 0.7 (17/02/2006)
97 ============================
98 * Fixes the IMAPS disconnection problem (patch provided by Gael Utard
99 <gael.utard@laposte.net>) (fixes gna bug #2178).
100 * Fixes some issues regarding parallel fetching of feeds.
101 * Now displays the feed creator as sender of emails. (fixes gna bug #5043).
102 * Don't display the password in error messages (fixes debian bug #350370).
103 * Upload mail with the Item's time, not the upload's time (fixes debian
104 bug #350371).
105
106 Feed2Imap 0.6 (11/01/2006)
107 ============================
108 * Moved the RSS/Atom parser to a separate library (ruby-feedparser).
109 * Locks the Cache file to avoid concurrent instances of feed2imap.
110 * Issue a warning if the config file is world readable.
111 * Fixed a small bug in Atom feeds parsing.
112 * Fix another bug related to escaped HTML.
113 * Minor fixes.
114
115 Feed2Imap 0.5 (19/09/2005)
116 ============================
117 * Fixed a parser problem with items with multiple children in the description.
118 * Mails were upload with \n only, but \r\n are needed.
119 * Feed2Imap can now work without libopenssl.
120 * Fixed a bug in the HTML2Text converter with <a> tags without href.
121 * Reserved characters (eg @) can now be included in the login, password or
122 folder. You just need to escape them.
123 * Feed2Imap is now included in Debian (package name: feed2imap).
124 * Much better handling of feeds with escaped HTML (LinuxFR for example).
125
126 Feed2Imap 0.4 (25/07/2005)
127 ============================
128 * now available as a Debian package.
129 * added manpages for everything.
130 * added min-frequency and disable config options. Added doc in example config.
131 * You can now use WordPress's feed:http://something urls in feed2imaprc.
132 * Switched to a real SGML parser for the text version.
133 * Much better output for the text version of emails.
134 * New feed2imap-cleaner to remove old mails seen but not flagged
135 * Feed2Imap version number wasn't displayed in the User-Agent
136 * Better exception handling when parsing errors occur
137 * added feed2imap's RSS feed to the default feeds in the config file
138
139 Feed2Imap 0.3 (04/06/2005)
140 ============================
141 * New releases are now advertised using a RSS feed
142 * Cleaner way to manage duplicate IDs (#1773)
143 * Fixed a problem with pseudo-duplicate entries from Mediawiki
144 * Fixed a problem with updated items being seen as updated at each update.
145 * Fixed a problem when the disconnection from the IMAP server failed.
146 reported by Ludovic Gomez <ludogomez@chez.com>
147
148 Feed2Imap 0.2 (30/04/2005)
149 ============================
150 * Fixed a problem with feeds with strange caching bugs (old items going away
151 and coming back)
152 * The text version is now encoded in iso-8859-1 instead of utf-8.
153 * The subject is now MIME-encoded in utf-8. It works with mutt & evo.
154 * No longer overwrite mail flags (Read, Important,..) when updating an item.
155 * HTTP fetching is now multithreaded and is much faster (about 300%).
156 * Fetching over HTTPS works.
157 * HTTP fetching is fully unit-tested.
158
159 Feed2Imap 0.1 (02/04/2005)
160 ==========================
161 * first public release.
0 Feed2Imap
1 -------------
2 by Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 Currently, all the information is provided on
5
6 http://home.gna.org/feed2imap
7
8 Copyright (c) 2005-2010 Lucas Nussbaum <lucas@lucas-nussbaum.net>
9
10 This program is free software; you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation; either version 2 of the License, or
13 (at your option) any later version.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
0 require 'rake/testtask'
1 require 'rake/rdoctask'
2 require 'rake/packagetask'
3 require 'rake'
4 require 'find'
5
6 task :default => [:package]
7
8 PKG_NAME = 'feed2imap'
9 PKG_VERSION = '1.0'
10 PKG_FILES = [ 'ChangeLog', 'README', 'COPYING', 'setup.rb', 'Rakefile']
11 Find.find('bin/', 'lib/', 'test/', 'data/') do |f|
12 if FileTest.directory?(f) and f =~ /\.svn/
13 Find.prune
14 else
15 PKG_FILES << f
16 end
17 end
18 Rake::TestTask.new do |t|
19 t.libs << "libs/feed2imap"
20 t.libs << "test"
21 t.test_files = FileList['test/tc_*.rb']
22 end
23
24 Rake::RDocTask.new do |rd|
25 rd.main = 'README'
26 rd.rdoc_files.include('lib/*.rb', 'lib/feed2imap/*.rb')
27 rd.options << '--all'
28 rd.options << '--diagram'
29 rd.options << '--fileboxes'
30 rd.options << '--inline-source'
31 rd.options << '--line-numbers'
32 rd.rdoc_dir = 'rdoc'
33 end
34
35 Rake::PackageTask.new(PKG_NAME, PKG_VERSION) do |p|
36 p.need_tar = true
37 p.need_zip = true
38 p.package_files = PKG_FILES
39 end
40
41 # "Gem" part of the Rakefile
42 begin
43 require 'rake/gempackagetask'
44
45 spec = Gem::Specification.new do |s|
46 s.platform = Gem::Platform::RUBY
47 s.summary = "RSS/Atom feed aggregator"
48 s.name = PKG_NAME
49 s.version = PKG_VERSION
50 s.requirements << 'feedparser'
51 s.require_path = 'lib'
52 s.files = PKG_FILES
53 s.description = "RSS/Atom feed aggregator"
54 end
55
56 Rake::GemPackageTask.new(spec) do |pkg|
57 pkg.need_zip = true
58 pkg.need_tar = true
59 end
60 rescue LoadError
61 puts "Will not generate gem."
62 end
0 #!/usr/bin/ruby
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
4 require 'feed2imap/feed2imap'
5 require 'optparse'
6
7 verbose = false
8 version = false
9 cacherebuild = false
10 configf = ENV['HOME'] + '/.feed2imaprc'
11 progname = File::basename($PROGRAM_NAME)
12 opts = OptionParser::new do |opts|
13 opts.program_name = progname
14 opts.banner = "Usage: #{progname} [options]"
15 opts.separator ""
16 opts.separator "Options:"
17
18 opts.on("-v", "--verbose", "Verbose mode") do |v|
19 verbose = true
20 end
21
22 opts.on("-d", "--debug", "Debug mode") do |v|
23 verbose = :debug
24 end
25
26 opts.on("-V", "--version", "Display Feed2Imap version") do |v|
27 version = true
28 end
29 opts.on("-c", "--rebuild-cache", "Cache rebuilding run : will fetch everything and add to cache, without uploading to the IMAP server. Useful if your cache file was lost, and you don't want to re-read all the items.") do |c|
30 cacherebuild = true
31 end
32 opts.on("-f", "--config <file>", "Select alternate config file") do |f|
33 configf = f
34 end
35 end
36 begin
37 opts.parse!(ARGV)
38 rescue OptionParser::ParseError => pe
39 opts.warn pe
40 puts opts
41 exit 1
42 end
43
44 if version
45 puts "Feed2Imap v.#{F2I_VERSION}"
46 else
47 Feed2Imap::new(verbose, cacherebuild, configf)
48 end
0 #!/usr/bin/ruby
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
4 require 'feed2imap/feed2imap'
5 require 'optparse'
6
7 configf = ENV['HOME'] + '/.feed2imaprc'
8 dryrun = false
9
10 opts = OptionParser::new do |opts|
11 opts.banner = "Usage: feed2imap-cleaner [options]"
12 opts.separator ""
13 opts.separator "Options:"
14 opts.on("-d", "--dry-run", "Dont really remove messages") do |v|
15 dryrun = true
16 end
17 opts.on("-f", "--config <file>", "Select alternate config file") do |f|
18 configf = f
19 end
20 end
21 opts.parse!(ARGV)
22
23 config = nil
24 File::open(configf) { |f| config = F2IConfig::new(f) }
25 config.imap_accounts.each_value do |ac|
26 ac.connect
27 end
28 config.feeds.each do |f|
29 f.imapaccount.cleanup(f.folder, dryrun)
30 end
31
0 #!/usr/bin/ruby
1
2 =begin
3 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
4 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 =end
20
21 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
22
23 require 'feed2imap/config'
24 require 'optparse'
25
26 configf = ENV['HOME'] + '/.feed2imaprc'
27 opts = OptionParser::new do |opts|
28 opts.banner = "Usage: ./dumpconfig.rb [options]"
29 opts.separator ""
30 opts.separator "Options:"
31 opts.on("-f", "--config <file>", "Select alternate config file") do |f|
32 configf = f
33 end
34 end
35 opts.parse!(ARGV)
36
37 if not File::exist?(configf)
38 puts "Configuration file #{configfile} not found."
39 exit(1)
40 end
41 File::open(configf) { |f| puts F2IConfig::new(f).to_s }
0 #!/usr/bin/ruby
1
2 =begin
3 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
4 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 =end
20
21 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
22
23 require 'rexml/document'
24 require 'yaml'
25
26 DEFAULTIMAPFOLDER = 'imap://login:password@imapserver/folder.folder2'
27
28 opml = ARGV[0]
29 doc = nil
30 doc = REXML::Document::new(IO.read(opml))
31 feeds = []
32 doc.root.each_element('//outline') do |e|
33 if u = e.attribute('xmlUrl') || e.attribute('htmlUrl')
34 # dirty liferea hack
35 next if u.value == 'vfolder'
36 # get title
37 t = e.attribute('text') || e.attribute('Title') || nil
38 if t.nil?
39 title = '*** FEED TITLE (must be unique) ***'
40 else
41 title = t.value
42 end
43 url = u.value
44 feeds.push({'name' => title, 'url' => url, 'target' => DEFAULTIMAPFOLDER})
45 end
46 end
47 YAML::dump({'feeds' => feeds}, $stdout)
0 # Global options:
1 # max-failures: maximum number of failures allowed before they are reported in
2 # normal mode (default 10). By default, failures are only visible in verbose
3 # mode. Most feeds tend to suffer from temporary failures.
4 # dumpdir: (for debugging purposes) directory where all fetched feeds will be
5 # dumped.
6 # debug-updated: (for debugging purposes) if true, display a lot of information
7 # about the "updated-items" algorithm.
8 # include-images: download images and include them in the mail? (true/false)
9 # default-email: default email address in the format foo@example.com
10 # disable-ssl-verification: disable SSL certification when connecting
11 # to IMAPS accounts (true/false)
12 #
13 # Per-feed options:
14 # name: name of the feed (must be unique)
15 # url: HTTP[S] address where the feed has to be fetched
16 # target: the IMAP URI where to put emails. Should start with imap:// for IMAP,
17 # imaps:// for IMAPS and maildir:// for a path to a local maildir.
18 # min-frequency: (in HOURS) is the minimum frequency with which this particular
19 # feed will be fetched
20 # disable: if set to something, the feed will be ignored
21 # include-images: download images and include them in the mail? (true/false)
22 # always-new: feed2imap tries to use a clever algorithm to determine whether
23 # an item is new or has been updated. It doesn't work well with some web apps
24 # like mediawiki. When this flag is enabled, all items which don't match
25 # exactly a previously downloaded item are considered as new items.
26 # ignore-hash: Some feeds change the content of their items all the time, so
27 # feed2imap detects that they have been updated at each run. When this flag
28 # is enabled, feed2imap ignores the content of an item when determining
29 # whether the item is already known.
30 # dumpdir: (for debugging purposes) directory where all fetched feeds will be
31 # dumped.
32 # Snownews/Liferea scripts support :
33 # execurl: Command to execute that will display the RSS/Atom feed on stdout
34 # filter: Command to execute which will receive the RSS/Atom feed on stdin,
35 # modify it, and output it on stdout.
36 # For more information: http://kiza.kcore.de/software/snownews/snowscripts/
37 #
38 #
39 # If your login contains an @ character, replace it with %40. Other reserved
40 # characters can be escaped in the same way (see man ascii to get their code)
41 feeds:
42 - name: feed2imap
43 url: http://home.gna.org/feed2imap/feed2imap.rss
44 target: imap://luser:password@imap.apinc.org/INBOX.Feeds.Feed2Imap
45 - name: lucas
46 url: http://www.lucas-nussbaum.net/blog/?feed=rss2
47 target: imap://luser:password@imap.apinc.org/INBOX.Feeds.Lucas
48 - name: JabberFrWiki
49 url: http://wiki.jabberfr.org/index.php?title=Special:Recentchanges&feed=rss
50 target: imaps://luser:password@imap.apinc.org/INBOX.Feeds.JabberFR
51 always-new: true
52 - name: LeMonde
53 execurl: "wget -q -O /dev/stdout http://www.lemonde.fr/rss/sequence/0,2-3208,1-0,0.xml"
54 filter: "/home/lucas/lemonde_getbody"
55 target: imap://luser:password@imap.apinc.org/INBOX.Feeds.LeMonde
56 # It is also possible to reuse the same string in the target parameter:
57 # target-refix: &target "imap://user:pass@host/rss."
58 # feeds:
59 # - name: test1
60 # target: [ *target, 'test1' ]
61 # ...
62 # - name: test2
63 # target: [ *target, 'test2' ]
64 # ...
0 .TH feed2imap\-cleaner 1 "Jul 25, 2005"
1 .SH NAME
2 feed2imap\-cleaner \- Removes old items from IMAP folders
3 .SH SYNOPSIS
4 \fBfeed2imap\-cleaner\fR [OPTIONS]
5 .SH DESCRIPTION
6 feed2imap\-cleaner deletes old items from IMAP folders specified in the configuration file. The actual query string used to determine whether an item is old is :
7 "SEEN NOT FLAGGED BEFORE (3 days ago)". Which means that an item WON'T be deleted if it satisfies one of the following conditions :
8 .TP 0.2i
9 \(bu
10 It isn't 3 days old ;
11 .TP 0.2i
12 \(bu
13 It hasn't been read yet ;
14 .TP 0.2i
15 \(bu
16 It is flagged (marked as Important, for example).
17 .TP
18 \fB\-d\fR, \fB\-\-dry\-run\fR
19 Don't remove anything, but show what would be removed if run without this option.
20 .TP
21 \fB\-f\fR, \fB\-\-config \fIfile\fB\fR
22 Use another config file (~/.feed2imaprc is the default).
23 .SH BUGS
24 Deletion criterias should probably be more configurable.
25 .SH "SEE ALSO"
26 Homepage :
27 http://home.gna.org/feed2imap/
28 .PP
29 \fBfeed2imaprc\fR(5),
30 \fBfeed2imap\fR(1)
31 .SH AUTHOR
32 Copyright (C) 2005 Lucas Nussbaum lucas@lucas\-nussbaum.net
33 .PP
34 This program is free software; you can redistribute it and/or modify
35 it under the terms of the GNU General Public License as published by the
36 Free Software Foundation; either version 2 of the License, or (at your
37 option) any later version.
38 .PP
39 This program is distributed in the hope that it will be useful, but
40 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
41 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
42 more details.
0 .TH feed2imap\-dumpconfig 1 "Jul 25, 2005"
1 .SH NAME
2 feed2imap\-dumpconfig \- Dump feed2imap config
3 .SH SYNOPSIS
4 \fBfeed2imap\-dumpconfig\fR [OPTIONS]
5 .SH DESCRIPTION
6 feed2imap\-dumpconfig dumps the content of your feed2imaprc to screen.
7 .TP
8 \fB\-f\fR, \fB\-\-config \fIfile\fB\fR
9 Use another config file (~/.feed2imaprc is the default).
10 .SH "SEE ALSO"
11 Homepage :
12 http://home.gna.org/feed2imap/
13 .PP
14 \fBfeed2imaprc\fR(5),
15 \fBfeed2imap\fR(1)
16 .SH AUTHOR
17 Copyright (C) 2005 Lucas Nussbaum lucas@lucas\-nussbaum.net
18 .PP
19 This program is free software; you can redistribute it and/or modify
20 it under the terms of the GNU General Public License as published by the
21 Free Software Foundation; either version 2 of the License, or (at your
22 option) any later version.
23 .PP
24 This program is distributed in the hope that it will be useful, but
25 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
26 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
27 more details.
0 .TH feed2imap\-opmlimport 1 "Jul 25, 2005"
1 .SH NAME
2 feed2imap\-opmlimport \- Convert an OPML subscription list to a feed2imap config file
3 .SH SYNOPSIS
4 \fBfeed2imap\-opmlimport\fR
5 .SH DESCRIPTION
6 feed2imap\-opmlimport reads an OPML subscription list on standard input and outputs a feed2imap configuration file on standard output. The resulting configuration file will require some tweaking.
7 .SH BUGS
8 Should probably accept parameters to be able to change default values.
9 .SH "SEE ALSO"
10 Homepage :
11 http://home.gna.org/feed2imap/
12 .PP
13 \fBfeed2imaprc\fR(5),
14 \fBfeed2imap\fR(1)
15 .SH AUTHOR
16 Copyright (C) 2005 Lucas Nussbaum lucas@lucas\-nussbaum.net
17 .PP
18 This program is free software; you can redistribute it and/or modify
19 it under the terms of the GNU General Public License as published by the
20 Free Software Foundation; either version 2 of the License, or (at your
21 option) any later version.
22 .PP
23 This program is distributed in the hope that it will be useful, but
24 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
25 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
26 more details.
0 .TH feed2imap 1 "Jul 25, 2005"
1 .SH NAME
2 feed2imap \- clever RSS/ATOM feed aggregator
3 .SH SYNOPSIS
4 \fBfeed2imap\fR [OPTIONS]
5 .SH DESCRIPTION
6 feed2imap is an RSS/Atom feed aggregator. After
7 Downloading feeds (over HTTP or HTTPS), it uploads them to a specified
8 folder of an IMAP mail server. The user can then access the feeds using
9 Mutt, Evolution, Mozilla Thunderbird or even a webmail.
10 .TP
11 \fB\-V\fR, \fB\-\-version\fR
12 Show version information.
13 .TP
14 \fB\-v\fR, \fB\-\-verbose\fR
15 Run in verbose mode.
16 .TP
17 \fB\-c\fR, \fB\-\-rebuild\-cache\fR
18 Rebuilds the cache. Fetches all items and mark them as already seen. Useful if you lose your .feed2imap.cache file.
19 .TP
20 \fB\-f\fR, \fB\-\-config \fIfile\fB\fR
21 Use another config file (~/.feed2imaprc is the default).
22 .SH "SEE ALSO"
23 Homepage :
24 http://home.gna.org/feed2imap/
25 .PP
26 \fBfeed2imaprc\fR(5),
27 \fBfeed2imap\-cleaner\fR(1),
28 \fBfeed2imap\-dumpconfig\fR(1),
29 \fBfeed2imap\-opmlimport\fR(1)
30 .SH AUTHOR
31 Copyright (C) 2005 Lucas Nussbaum lucas@lucas\-nussbaum.net
32 .PP
33 This program is free software; you can redistribute it and/or modify
34 it under the terms of the GNU General Public License as published by the
35 Free Software Foundation; either version 2 of the License, or (at your
36 option) any later version.
37 .PP
38 This program is distributed in the hope that it will be useful, but
39 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
40 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
41 more details.
0 .TH feed2imaprc 5 "Jul 25, 2005"
1 .SH NAME
2 feed2imaprc \- feed2imap configuration file
3 .SH SYNOPSIS
4 \fBfeed2imaprc\fR is feed2imap's configuration file. It is usually located in \fB~/.feed2imaprc\fR.
5 .SH EXAMPLE
6 See \fB/usr/share/doc/feed2imap/examples/feed2imaprc\fR.
7 .SH "RESERVED CHARACTERS"
8 Some characters are reserved in RFC2396 (URI). If you need to include a reserved character in the login/password part of your target URI, replace it with its hex code. For example, @ can be replaced by %40.
9 .SH BUGS
10 This manpage should probably give more details. However, the example configuration file is
11 very well documented.
12 .SH "SEE ALSO"
13 Homepage :
14 http://home.gna.org/feed2imap/
15 .PP
16 \fBfeed2imap\fR(1)
17 .SH AUTHOR
18 Copyright (C) 2005 Lucas Nussbaum lucas@lucas\-nussbaum.net
19 .PP
20 This program is free software; you can redistribute it and/or modify
21 it under the terms of the GNU General Public License as published by the
22 Free Software Foundation; either version 2 of the License, or (at your
23 option) any later version.
24 .PP
25 This program is distributed in the hope that it will be useful, but
26 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
27 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
28 more details.
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 =end
18
19 # debug mode
20 $updateddebug = false
21
22 # This class manages a cache of items
23 # (items which have already been seen)
24
25 require 'digest/md5'
26
27 class ItemCache
28 def initialize(debug = false)
29 @channels = {}
30 @@cacheidx = 0
31 $updateddebug = debug
32 self
33 end
34
35 # Returns the really new items amongst items
36 def get_new_items(id, items, always_new = false, ignore_hash = false)
37 if $updateddebug
38 puts "======================================================="
39 puts "GET_NEW_ITEMS FOR #{id}... (#{Time::now})"
40 end
41 @channels[id] ||= CachedChannel::new
42 @channels[id].parsefailures = 0
43 return @channels[id].get_new_items(items, always_new, ignore_hash)
44 end
45
46 # Commit changes to the cache
47 def commit_cache(id)
48 @channels[id] ||= CachedChannel::new
49 @channels[id].commit
50 end
51
52 # Get the last time the cache was updated
53 def get_last_check(id)
54 @channels[id] ||= CachedChannel::new
55 @channels[id].lastcheck
56 end
57
58 # Get the last time the cache was updated
59 def set_last_check(id, time)
60 @channels[id] ||= CachedChannel::new
61 @channels[id].lastcheck = time
62 @channels[id].failures = 0
63 self
64 end
65
66 # Fetching failure.
67 # returns number of failures
68 def fetch_failed(id)
69 @channels[id].fetch_failed
70 end
71
72 # Parsing failure.
73 # returns number of failures
74 def parse_failed(id)
75 @channels[id].parse_failed
76 end
77
78 # Load the cache from an IO stream
79 def load(io)
80 begin
81 @@cacheidx, @channels = Marshal.load(io)
82 rescue
83 @channels = Marshal.load(io)
84 @@cacheidx = 0
85 end
86 end
87
88 # Save the cache to an IO stream
89 def save(io)
90 Marshal.dump([@@cacheidx, @channels], io)
91 end
92
93 # Return the number of channels in the cache
94 def nbchannels
95 @channels.length
96 end
97
98 # Return the number of items in the cache
99 def nbitems
100 nb = 0
101 @channels.each_value { |c|
102 nb += c.nbitems
103 }
104 nb
105 end
106
107 def ItemCache.getindex
108 i = @@cacheidx
109 @@cacheidx += 1
110 i
111 end
112 end
113
114 class CachedChannel
115 # Size of the cache for each feed
116 # 100 items should be enough for everybody, even quite busy feeds
117 CACHESIZE = 100
118
119 attr_accessor :lastcheck, :items, :failures, :parsefailures
120
121 def initialize
122 @lastcheck = Time::at(0)
123 @items = []
124 @itemstemp = [] # see below
125 @nbnewitems = 0
126 @failures = 0
127 @parsefailures = 0
128 end
129
130 # Let's explain @items and @itemstemp.
131 # @items contains the CachedItems serialized to the disk cache.
132 # The - quite complicated - get_new_items method fills in @itemstemp
133 # but leaves @items unchanged.
134 # Later, the commit() method replaces @items with @itemstemp and
135 # empties @itemstemp. This way, if something wrong happens during the
136 # upload to the IMAP server, items aren't lost.
137 # @nbnewitems is set by get_new_items, and is used to limit the number
138 # of (old) items serialized.
139
140 # Returns the really new items amongst items
141 def get_new_items(items, always_new = false, ignore_hash = false)
142 # save number of new items
143 @nbnewitems = items.length
144 # set items' cached version if not set yet
145 newitems = []
146 updateditems = []
147 @itemstemp = @items
148 items.each { |i| i.cacheditem ||= CachedItem::new(i) }
149 if $updateddebug
150 puts "-------Items downloaded before dups removal (#{items.length}) :----------"
151 items.each { |i| puts "#{i.cacheditem.to_s}" }
152 end
153 # remove dups
154 dups = true
155 while dups
156 dups = false
157 for i in 0...items.length do
158 for j in i+1...items.length do
159 if items[i].cacheditem == items[j].cacheditem
160 if $updateddebug
161 puts "## Removed duplicate #{items[j].cacheditem.to_s}"
162 end
163 items.delete_at(j)
164 dups = true
165 break
166 end
167 end
168 break if dups
169 end
170 end
171 # debug : dump interesting info to stdout.
172 if $updateddebug
173 puts "-------Items downloaded after dups removal (#{items.length}) :----------"
174 items.each { |i| puts "#{i.cacheditem.to_s}" }
175 puts "-------Items already there (#{@items.length}) :----------"
176 @items.each { |i| puts "#{i.to_s}" }
177 puts "Items always considered as new: #{always_new.to_s}"
178 puts "Items compared ignoring the hash: #{ignore_hash.to_s}"
179 end
180 items.each do |i|
181 found = false
182 # Try to find a perfect match
183 @items.each do |j|
184 # note that simple_compare only CachedItem, not RSSItem, so we have to use
185 # j.simple_compare(i) and not i.simple_compare(j)
186 if (i.cacheditem == j and not ignore_hash) or
187 (j.simple_compare(i) and ignore_hash)
188 i.cacheditem.index = j.index
189 found = true
190 # let's put j in front of itemstemp
191 @itemstemp.delete(j)
192 @itemstemp.unshift(j)
193 break
194 end
195 end
196 next if found
197 if not always_new
198 # Try to find an updated item
199 @items.each do |j|
200 # Do we need a better heuristic ?
201 if j.is_ancestor_of(i)
202 i.cacheditem.index = j.index
203 i.cacheditem.updated = true
204 updateditems.push(i)
205 found = true
206 # let's put j in front of itemstemp
207 @itemstemp.delete(j)
208 @itemstemp.unshift(i.cacheditem)
209 break
210 end
211 end
212 end
213 next if found
214 # add as new
215 i.cacheditem.create_index
216 newitems.push(i)
217 # add i.cacheditem to @itemstemp
218 @itemstemp.unshift(i.cacheditem)
219 end
220 if $updateddebug
221 puts "-------New items :----------"
222 newitems.each { |i| puts "#{i.cacheditem.to_s}" }
223 puts "-------Updated items :----------"
224 updateditems.each { |i| puts "#{i.cacheditem.to_s}" }
225 end
226 return [newitems, updateditems]
227 end
228
229 def commit
230 # too old items must be dropped
231 n = @nbnewitems > CACHESIZE ? @nbnewitems : CACHESIZE
232 @items = @itemstemp[0..n]
233 if $updateddebug
234 puts "Committing: new items: #{@nbnewitems} / items kept: #{@items.length}"
235 end
236 @itemstemp = []
237 self
238 end
239
240 # returns the number of items
241 def nbitems
242 @items.length
243 end
244
245 def parse_failed
246 @parsefailures = 0 if @parsefailures.nil?
247 @parsefailures += 1
248 return @parsefailures
249 end
250
251 def fetch_failed
252 @failures = 0 if @failures.nil?
253 @failures += 1
254 return @failures
255 end
256 end
257
258 # This class is the only thing kept in the cache
259 class CachedItem
260 attr_reader :title, :link, :creator, :date, :hash
261 attr_accessor :index
262 attr_accessor :updated
263
264 def initialize(item)
265 @title = item.title
266 @link = item.link
267 @date = item.date
268 @creator = item.creator
269 if item.content.nil?
270 @hash = nil
271 else
272 @hash = Digest::MD5.hexdigest(item.content.to_s)
273 end
274 end
275
276 def ==(other)
277 if $updateddebug
278 puts "Comparing #{self.to_s} and #{other.to_s}:"
279 puts "Title: #{@title == other.title}"
280 puts "Link: #{@link == other.link}"
281 puts "Creator: #{@creator == other.creator}"
282 puts "Date: #{@date == other.date}"
283 puts "Hash: #{@hash == other.hash}"
284 end
285 @title == other.title and @link == other.link and
286 (@creator.nil? or other.creator.nil? or @creator == other.creator) and
287 (@date.nil? or other.date.nil? or @date == other.date) and @hash == other.hash
288 end
289
290 def simple_compare(other)
291 @title == other.title and @link == other.link and
292 (@creator.nil? or other.creator.nil? or @creator == other.creator)
293 end
294
295 def create_index
296 @index = ItemCache.getindex
297 end
298
299 def is_ancestor_of(other)
300 (@link and other.link and @link == other.link) and
301 ((@creator and other.creator and @creator == other.creator) or (@creator.nil?))
302 end
303
304 def to_s
305 "\"#{@title}\" #{@creator}/#{@date} #{@link} #{@hash}"
306 end
307 end
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 =end
18
19 require 'yaml'
20 require 'uri'
21 require 'feed2imap/imap'
22 require 'feed2imap/maildir'
23 require 'etc'
24 require 'socket'
25
26 # Default cache file
27 DEFCACHE = ENV['HOME'] + '/.feed2imap.cache'
28
29 # Hostname and login name of the current user
30 HOSTNAME = Socket.gethostname
31 LOGNAME = Etc.getlogin
32
33 # Feed2imap configuration
34 class F2IConfig
35 attr_reader :imap_accounts, :cache, :feeds, :dumpdir, :updateddebug, :max_failures, :include_images, :default_email, :hostname
36
37 # Load the configuration from the IO stream
38 # TODO should do some sanity check on the data read.
39 def initialize(io)
40 @conf = YAML::load(io)
41 @cache = @conf['cache'] || DEFCACHE
42 @dumpdir = @conf['dumpdir'] || nil
43 @conf['feeds'] ||= []
44 @feeds = []
45 @max_failures = (@conf['max-failures'] || 10).to_i
46 @updateddebug = (@conf['debug-updated'] and @conf['debug-updated'] != 'false')
47 @include_images = (@conf['include-images'] and @conf['include-images'] != 'false')
48 @default_email = (@conf['default-email'] || "#{LOGNAME}@#{HOSTNAME}")
49 ImapAccount.no_ssl_verify = (@conf['disable-ssl-verification'] and @conf['disable-ssl-verification'] != 'false')
50 @hostname = HOSTNAME # FIXME: should this be configurable as well?
51 @imap_accounts = ImapAccounts::new
52 maildir_account = MaildirAccount::new
53 @conf['feeds'].each do |f|
54 if f['disable'].nil?
55 uri = URI::parse(f['target'].to_s)
56 path = URI::unescape(uri.path)
57 path = path[1..-1] if path[0,1] == '/'
58 if uri.scheme == 'maildir'
59 @feeds.push(ConfigFeed::new(f, maildir_account, path, self))
60 else
61 @feeds.push(ConfigFeed::new(f, @imap_accounts.add_account(uri), path, self))
62 end
63 end
64 end
65 end
66
67 def to_s
68 s = "Your Feed2Imap config :\n"
69 s += "=======================\n"
70 s += "Cache file: #{@cache}\n\n"
71 s += "Imap accounts I'll have to connect to :\n"
72 s += "---------------------------------------\n"
73 @imap_accounts.each_value { |i| s += i.to_s + "\n" }
74 s += "\nFeeds :\n"
75 s += "-------\n"
76 i = 1
77 @feeds.each do |f|
78 s += "#{i}. #{f.name}\n"
79 s += " URL: #{f.url}\n"
80 s += " IMAP Account: #{f.imapaccount}\n"
81 s += " Folder: #{f.folder}\n"
82
83 if not f.wrapto
84 s += " Not wrapped.\n"
85 end
86
87 s += "\n"
88 i += 1
89 end
90 s
91 end
92 end
93
94 # A configured feed. simple data container.
95 class ConfigFeed
96 attr_reader :name, :url, :imapaccount, :folder, :always_new, :execurl, :filter, :ignore_hash, :dumpdir, :wrapto, :include_images
97 attr_accessor :body
98
99 def initialize(f, imapaccount, folder, f2iconfig)
100 @name = f['name']
101 @url = f['url']
102 @url.sub!(/^feed:/, '') if @url =~ /^feed:/
103 @imapaccount, @folder = imapaccount, folder
104 @freq = f['min-frequency']
105 @always_new = (f['always-new'] and f['always-new'] != 'false')
106 @execurl = f['execurl']
107 @filter = f['filter']
108 @ignore_hash = f['ignore-hash'] || false
109 @freq = @freq.to_i if @freq
110 @dumpdir = f['dumpdir'] || nil
111 @wrapto = if f['wrapto'] == nil then 72 else f['wrapto'].to_i end
112 @include_images = f2iconfig.include_images
113 if f['include-images']
114 @include_images = (f['include-images'] != 'false')
115 end
116 end
117
118 def needfetch(lastcheck)
119 return true if @freq.nil?
120 return (lastcheck + @freq * 3600) < Time::now
121 end
122 end
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 =end
18
19 # Feed2Imap version
20 F2I_VERSION = '1.0'
21 F2I_WARNFETCHTIME = 10
22
23 require 'feed2imap/config'
24 require 'feed2imap/cache'
25 require 'feed2imap/httpfetcher'
26 require 'logger'
27 require 'thread'
28 require 'feedparser'
29 require 'feed2imap/itemtomail'
30 require 'open3'
31
32 class Feed2Imap
33 def Feed2Imap.version
34 return F2I_VERSION
35 end
36
37 def initialize(verbose, cacherebuild, configfile)
38 @logger = Logger::new(STDOUT)
39 if verbose == :debug
40 @logger.level = Logger::DEBUG
41 require 'pp'
42 elsif verbose == true
43 @logger.level = Logger::INFO
44 else
45 @logger.level = Logger::WARN
46 end
47 @logger.info("Feed2Imap V.#{F2I_VERSION} started")
48 # reading config
49 @logger.info('Reading configuration file ...')
50 if not File::exist?(configfile)
51 @logger.fatal("Configuration file #{configfile} not found.")
52 exit(1)
53 end
54 if (File::stat(configfile).mode & 044) != 0
55 @logger.warn("Configuration file is readable by other users. It " +
56 "probably contains your password.")
57 end
58 begin
59 File::open(configfile) {
60 |f| @config = F2IConfig::new(f)
61 }
62 rescue
63 @logger.fatal("Error while reading configuration file, exiting: #{$!}")
64 exit(1)
65 end
66 if @logger.level == Logger::DEBUG
67 @logger.debug("Configuration read:")
68 pp(@config)
69 end
70
71 # init cache
72 @logger.info('Initializing cache ...')
73 @cache = ItemCache::new(@config.updateddebug)
74 if not File::exist?(@config.cache + '.lock')
75 f = File::new(@config.cache + '.lock', 'w')
76 f.close
77 end
78 if File::new(@config.cache + '.lock', 'w').flock(File::LOCK_EX | File::LOCK_NB) == false
79 @logger.fatal("Another instance of feed2imap is already locking the cache file")
80 exit(1)
81 end
82 if not File::exist?(@config.cache)
83 @logger.warn("Cache file #{@config.cache} not found, using a new one")
84 else
85 File::open(@config.cache) do |f|
86 @cache.load(f)
87 end
88 end
89
90 # connecting all IMAP accounts
91 @logger.info('Connecting to IMAP accounts ...')
92 @config.imap_accounts.each_value do |ac|
93 begin
94 ac.connect
95 rescue
96 @logger.fatal("Error while connecting to #{ac}, exiting: #{$!}")
97 exit(1)
98 end
99 end
100
101 # for each feed, fetch, upload to IMAP and cache
102 @logger.info("Fetching and filtering feeds ...")
103 ths = []
104 mutex = Mutex::new
105 sparefetchers = 16 # max number of fetchers running at the same time.
106 sparefetchers_mutex = Mutex::new
107 sparefetchers_cond = ConditionVariable::new
108 @config.feeds.each do |f|
109 ths << Thread::new(f) do |feed|
110 begin
111 mutex.lock
112 lastcheck = @cache.get_last_check(feed.name)
113 if feed.needfetch(lastcheck)
114 mutex.unlock
115 sparefetchers_mutex.synchronize do
116 while sparefetchers <= 0
117 sparefetchers_cond.wait(sparefetchers_mutex)
118 end
119 sparefetchers -= 1
120 end
121 fetch_start = Time::now
122 if feed.url
123 s = HTTPFetcher::fetch(feed.url, @cache.get_last_check(feed.name))
124 elsif feed.execurl
125 # avoid running more than one command at the same time.
126 # We need it because the called command might not be
127 # thread-safe, and we need to get the right exitcode
128 mutex.lock
129 s = %x{#{feed.execurl}}
130 if $?.exitstatus != 0
131 @logger.warn("Command for #{feed.name} exited with status #{$?.exitstatus} !")
132 end
133 mutex.unlock
134 else
135 @logger.warn("No way to fetch feed #{feed.name} !")
136 end
137 if feed.filter and s != nil
138 # avoid running more than one command at the same time.
139 # We need it because the called command might not be
140 # thread-safe, and we need to get the right exitcode.
141 mutex.lock
142 # hack hack hack, avoid buffering problems
143 stdin, stdout, stderr = Open3::popen3(feed.filter)
144 inth = Thread::new do
145 stdin.puts s
146 stdin.close
147 end
148 output = nil
149 outh = Thread::new do
150 output = stdout.read
151 end
152 inth.join
153 outh.join
154 s = output
155 if $?.exitstatus != 0
156 @logger.warn("Filter command for #{feed.name} exited with status #{$?.exitstatus}. Output might be corrupted !")
157 end
158 mutex.unlock
159 end
160 if Time::now - fetch_start > F2I_WARNFETCHTIME
161 @logger.info("Fetching feed #{feed.name} took #{(Time::now - fetch_start).to_i}s")
162 end
163 sparefetchers_mutex.synchronize do
164 sparefetchers += 1
165 sparefetchers_cond.signal
166 end
167 mutex.lock
168 feed.body = s
169 @cache.set_last_check(feed.name, Time::now)
170 else
171 @logger.debug("Feed #{feed.name} doesn't need to be checked again for now.")
172 end
173 mutex.unlock
174 # dump if requested
175 if @config.dumpdir
176 mutex.synchronize do
177 if feed.body
178 fname = @config.dumpdir + '/' + feed.name + '-' + Time::now.xmlschema
179 File::open(fname, 'w') { |file| file.puts feed.body }
180 end
181 end
182 end
183 # dump this feed if requested
184 if feed.dumpdir
185 mutex.synchronize do
186 if feed.body
187 fname = feed.dumpdir + '/' + feed.name + '-' + Time::now.xmlschema
188 File::open(fname, 'w') { |file| file.puts feed.body }
189 end
190 end
191 end
192 rescue Timeout::Error
193 mutex.synchronize do
194 n = @cache.fetch_failed(feed.name)
195 m = "Timeout::Error while fetching #{feed.url}: #{$!} (failed #{n} times)"
196 if n > @config.max_failures
197 @logger.fatal(m)
198 else
199 @logger.info(m)
200 end
201 end
202 rescue
203 mutex.synchronize do
204 n = @cache.fetch_failed(feed.name)
205 m = "Error while fetching #{feed.url}: #{$!} (failed #{n} times)"
206 if n > @config.max_failures
207 @logger.fatal(m)
208 else
209 @logger.info(m)
210 end
211 end
212 end
213 end
214 end
215 ths.each { |t| t.join }
216 @logger.info("Parsing and uploading ...")
217 @config.feeds.each do |f|
218 if f.body.nil? # means 304
219 @logger.debug("Feed #{f.name} did not change.")
220 next
221 end
222 begin
223 feed = FeedParser::Feed::new(f.body)
224 rescue Exception
225 n = @cache.parse_failed(f.name)
226 m = "Error while parsing #{f.name}: #{$!} (failed #{n} times)"
227 if n > @config.max_failures
228 @logger.fatal(m)
229 else
230 @logger.info(m)
231 end
232 next
233 end
234 begin
235 newitems, updateditems = @cache.get_new_items(f.name, feed.items, f.always_new, f.ignore_hash)
236 rescue
237 @logger.fatal("Exception caught when selecting new items for #{f.name}: #{$!}")
238 puts $!.backtrace
239 next
240 end
241 @logger.info("#{f.name}: #{newitems.length} new items, #{updateditems.length} updated items.") if newitems.length > 0 or updateditems.length > 0 or @logger.level == Logger::DEBUG
242 begin
243 if !cacherebuild
244 fn = f.name.gsub(/[^0-9A-Za-z]/,'')
245 updateditems.each do |i|
246 id = "<#{fn}-#{i.cacheditem.index}@#{@config.hostname}>"
247 email = item_to_mail(@config, i, id, true, f.name, f.include_images, f.wrapto)
248 f.imapaccount.updatemail(f.folder, email,
249 id, i.date || Time::new)
250 end
251 # reverse is needed to upload older items first (fixes gna#8986)
252 newitems.reverse.each do |i|
253 id = "<#{fn}-#{i.cacheditem.index}@#{@config.hostname}>"
254 email = item_to_mail(@config, i, id, false, f.name, f.include_images, f.wrapto)
255 f.imapaccount.putmail(f.folder, email, i.date || Time::new)
256 end
257 end
258 rescue
259 @logger.fatal("Exception caught while uploading mail to #{f.folder}: #{$!}")
260 puts $!.backtrace
261 @logger.fatal("We can't recover from IMAP errors, so we are exiting.")
262 exit(1)
263 end
264 begin
265 @cache.commit_cache(f.name)
266 rescue
267 @logger.fatal("Exception caught while updating cache for #{f.name}: #{$!}")
268 next
269 end
270 end
271 @logger.info("Finished. Saving cache ...")
272 begin
273 File::open("#{@config.cache}.new", 'w') { |f| @cache.save(f) }
274 rescue
275 @logger.fatal("Exception caught while writing new cache to #{@config.cache}.new: #{$!}")
276 end
277 begin
278 File::rename("#{@config.cache}.new", @config.cache)
279 rescue
280 @logger.fatal("Exception caught while renaming #{@config.cache}.new to #{@config.cache}: #{$!}")
281 end
282 @logger.info("Closing IMAP connections ...")
283 @config.imap_accounts.each_value do |ac|
284 begin
285 ac.disconnect
286 rescue
287 # servers tend to cause an exception to be raised here, hence the INFO level.
288 @logger.info("Exception caught while closing connection to #{ac.to_s}: #{$!}")
289 end
290 end
291 end
292 end
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 =end
18
19 require 'feed2imap/sgml-parser'
20
21 # this class provides a simple SGML parser that removes HTML tags
22 class HTML2TextParser < SGMLParser
23
24 attr_reader :savedata
25
26 def initialize(verbose = false)
27 @savedata = ''
28 @pre = false
29 @href = nil
30 @links = []
31 super(verbose)
32 end
33
34 def handle_data(data)
35 # let's remove all CR
36 data.gsub!(/\n/, '') if not @pre
37
38 @savedata << data
39 end
40
41 def unknown_starttag(tag, attrs)
42 case tag
43 when 'p'
44 @savedata << "\n\n"
45 when 'br'
46 @savedata << "\n"
47 when 'b'
48 @savedata << '*'
49 when 'u'
50 @savedata << '_'
51 when 'i'
52 @savedata << '/'
53 when 'pre'
54 @savedata << "\n\n"
55 @pre = true
56 when 'a'
57 # find href in args
58 @href = nil
59 attrs.each do |a|
60 if a[0] == 'href'
61 @href = a[1]
62 end
63 end
64 if @href
65 @links << @href.gsub(/^("|'|)(.*)("|')$/,'\2')
66 end
67 end
68 end
69
70 def close
71 super
72 if @links.length > 0
73 @savedata << "\n\n"
74 @links.each_index do |i|
75 @savedata << "[#{i+1}] #{@links[i]}\n"
76 end
77 end
78 end
79
80 def unknown_endtag(tag)
81 case tag
82 when 'b'
83 @savedata << '*'
84 when 'u'
85 @savedata << '_'
86 when 'i'
87 @savedata << '/'
88 when 'pre'
89 @savedata << "\n\n"
90 @pre = false
91 when 'a'
92 if @href
93 @savedata << "[#{@links.length}]"
94 @href = nil
95 end
96 end
97 end
98 end
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 =end
18
19 require 'net/http'
20 # get openssl if available
21 begin
22 require 'net/https'
23 rescue LoadError
24 end
25 require 'uri'
26
27
28 # max number of redirections
29 MAXREDIR = 5
30
31 HTTPDEBUG = false
32
33 # Class used to retrieve the feed over HTTP
34 class HTTPFetcher
35 def HTTPFetcher::fetcher(baseuri, uri, lastcheck, recursion)
36 proxy_host = nil
37 proxy_port = nil
38 proxy_user = nil
39 proxy_pass = nil
40 if ENV['http_proxy']
41 proxy_uri = URI.parse(ENV['http_proxy'])
42 proxy_host = proxy_uri.host
43 proxy_port = proxy_uri.port
44 proxy_user, proxy_pass = proxy_uri.userinfo.split(/:/) if proxy_uri.userinfo
45 end
46
47 http = Net::HTTP::Proxy(proxy_host,
48 proxy_port,
49 proxy_user,
50 proxy_pass ).new(uri.host, uri.port)
51 http.read_timeout = 30 # should be enough for everybody...
52 http.open_timeout = 30
53 if uri.scheme == 'https'
54 http.use_ssl = true
55 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
56 end
57 if defined?(Feed2Imap)
58 useragent = "Feed2Imap v#{Feed2Imap.version} http://home.gna.org/feed2imap/"
59 else
60 useragent = 'Feed2Imap http://home.gna.org/feed2imap/'
61 end
62
63 if lastcheck == Time::at(0)
64 req = Net::HTTP::Get::new(uri.request_uri, {'User-Agent' => useragent })
65 else
66 req = Net::HTTP::Get::new(uri.request_uri, {'User-Agent' => useragent, 'If-Modified-Since' => lastcheck.httpdate})
67 end
68 if uri.userinfo
69 login, pw = uri.userinfo.split(':')
70 req.basic_auth(login, pw)
71 # workaround. eg. wikini redirects and loses auth info.
72 elsif uri.host == baseuri.host and baseuri.userinfo
73 login, pw = baseuri.userinfo.split(':')
74 req.basic_auth(login, pw)
75 end
76 begin
77 response = http.request(req)
78 rescue Timeout::Error
79 raise "Timeout while fetching #{baseuri.to_s}"
80 end
81 case response
82 when Net::HTTPSuccess
83 return response.body
84 when Net::HTTPRedirection
85 # if not modified
86 if Net::HTTPNotModified === response
87 puts "HTTPNotModified on #{uri}" if HTTPDEBUG
88 return nil
89 end
90 if recursion > 0
91 redir = URI::join(uri.to_s, response['location'])
92 return fetcher(baseuri, redir, lastcheck, recursion - 1)
93 else
94 raise "Too many redirections while fetching #{baseuri.to_s}"
95 end
96 else
97 raise "#{response.code}: #{response.message} while fetching #{baseuri.to_s}"
98 end
99 end
100
101 def HTTPFetcher::fetch(url, lastcheck)
102 uri = URI::parse(url)
103 return HTTPFetcher::fetcher(uri, uri, lastcheck, MAXREDIR)
104 end
105 end
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 =end
18
19 # Imap connection handling
20 require 'feed2imap/rubyimap'
21 begin
22 require 'openssl'
23 rescue LoadError
24 end
25 require 'uri'
26
27 # This class is a container of IMAP accounts.
28 # Thanks to it, accounts are re-used : several feeds
29 # using the same IMAP account will create only one
30 # IMAP connection.
31 class ImapAccounts < Hash
32
33 def add_account(uri)
34 u = URI::Generic::build({ :scheme => uri.scheme,
35 :userinfo => uri.userinfo,
36 :host => uri.host,
37 :port => uri.port })
38 if not include?(u)
39 ac = ImapAccount::new(u)
40 self[u] = ac
41 end
42 return self[u]
43 end
44 end
45
46 # This class is an IMAP account, with the given fd
47 # once the connection has been established
48 class ImapAccount
49 attr_reader :uri
50
51 @@no_ssl_verify = false
52 def ImapAccount::no_ssl_verify=(v)
53 @@no_ssl_verify = v
54 end
55
56 def initialize(uri)
57 @uri = uri
58 @existing_folders = []
59 self
60 end
61
62 # connects to the IMAP server
63 # raises an exception if it fails
64 def connect
65 port = 143
66 usessl = false
67 if uri.scheme == 'imap'
68 port = 143
69 usessl = false
70 elsif uri.scheme == 'imaps'
71 port = 993
72 usessl = true
73 else
74 raise "Unknown scheme: #{uri.scheme}"
75 end
76 # use given port if port given
77 port = uri.port if uri.port
78 @connection = Net::IMAP::new(uri.host, port, usessl, nil, !@@no_ssl_verify)
79 user, password = URI::unescape(uri.userinfo).split(':',2)
80 @connection.login(user, password)
81 self
82 end
83
84 # disconnect from the IMAP server
85 def disconnect
86 if @connection
87 @connection.logout
88 @connection.disconnect
89 end
90 end
91
92 # tests if the folder exists and create it if not
93 def create_folder_if_not_exists(folder)
94 return if @existing_folders.include?(folder)
95 if @connection.list('', folder).nil?
96 @connection.create(folder)
97 @connection.subscribe(folder)
98 end
99 @existing_folders << folder
100 end
101
102 # Put the mail in the given folder
103 # You should check whether the folder exist first.
104 def putmail(folder, mail, date = Time::now)
105 create_folder_if_not_exists(folder)
106 @connection.append(folder, mail.gsub(/\n/, "\r\n"), nil, date)
107 end
108
109 # update a mail
110 def updatemail(folder, mail, id, date = Time::now)
111 create_folder_if_not_exists(folder)
112 @connection.select(folder)
113 searchres = @connection.search(['HEADER', 'Message-Id', id])
114 flags = nil
115 if searchres.length > 0
116 # we get the flags from the first result and delete everything
117 flags = @connection.fetch(searchres[0], 'FLAGS')[0].attr['FLAGS']
118 searchres.each { |m| @connection.store(m, "+FLAGS", [:Deleted]) }
119 @connection.expunge
120 flags -= [ :Recent ] # avoids errors with dovecot
121 end
122 @connection.append(folder, mail.gsub(/\n/, "\r\n"), flags, date)
123 end
124
125 # convert to string
126 def to_s
127 u2 = uri.clone
128 u2.password = 'PASSWORD'
129 u2.to_s
130 end
131
132 # remove mails in a folder according to a criteria
133 def cleanup(folder, dryrun = false)
134 puts "-- Considering #{folder}:"
135 @connection.select(folder)
136 a = ['SEEN', 'NOT', 'FLAGGED', 'BEFORE', (Date::today - 3).strftime('%d-%b-%Y')]
137 todel = @connection.search(a)
138 todel.each do |m|
139 f = @connection.fetch(m, "FULL")
140 d = f[0].attr['INTERNALDATE']
141 s = f[0].attr['ENVELOPE'].subject
142 if s =~ /^=\?utf-8\?b\?/
143 s = Base64::decode64(s.gsub(/^=\?utf-8\?b\?(.*)\?=$/, '\1')).toISO_8859_1('utf-8')
144 end
145 if dryrun
146 puts "To remove: #{s} (#{d})"
147 else
148 puts "Removing: #{s} (#{d})"
149 @connection.store(m, "+FLAGS", [:Deleted])
150 end
151 end
152 puts "-- Deleted #{todel.length} messages."
153 if not dryrun
154 @connection.expunge
155 end
156 return todel.length
157 end
158 end
159
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This file contains classes to parse a feed and store it as a Channel object.
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 =end
20
21 require 'rexml/document'
22 require 'time'
23 require 'rmail'
24 require 'feedparser'
25 require 'feedparser/text-output'
26 require 'feedparser/html-output'
27 require 'base64'
28 require 'rmail'
29 require 'digest/md5'
30
31 class String
32 def needMIME
33 utf8 = false
34 begin
35 self.unpack('U*').each do |c|
36 if c > 127
37 utf8 = true
38 break
39 end
40 end
41 rescue
42 # safe fallback in case of problems
43 utf8 = true
44 end
45 utf8
46 end
47 end
48
49 def item_to_mail(config, item, id, updated, from = 'Feed2Imap', inline_images = false, wrapto = false)
50 message = RMail::Message::new
51 if item.creator and item.creator != ''
52 if item.creator.include?('@')
53 message.header['From'] = item.creator.chomp
54 else
55 message.header['From'] = "=?utf-8?b?#{Base64::encode64(item.creator.chomp).gsub("\n",'')}?= <#{config.default_email}>"
56 end
57 else
58 message.header['From'] = "=?utf-8?b?#{Base64::encode64(from).gsub("\n",'')}?= <#{config.default_email}>"
59 end
60 message.header['To'] = "=?utf-8?b?#{Base64::encode64(from).gsub("\n",'')}?= <#{config.default_email}>"
61
62 if item.date.nil?
63 message.header['Date'] = Time::new.rfc2822
64 else
65 message.header['Date'] = item.date.rfc2822
66 end
67 message.header['X-Feed2Imap-Version'] = F2I_VERSION if defined?(F2I_VERSION)
68 message.header['Message-Id'] = id
69 message.header['X-F2IStatus'] = "Updated" if updated
70 # treat subject. Might need MIME encoding.
71 subj = item.title or (item.date and item.date.to_s) or item.link
72 if subj
73 if subj.needMIME
74 message.header['Subject'] = "=?utf-8?b?#{Base64::encode64(subj).gsub("\n",'')}?="
75 else
76 message.header['Subject'] = subj
77 end
78 end
79 textpart = RMail::Message::new
80 textpart.header['Content-Type'] = 'text/plain; charset=utf-8; format=flowed'
81 textpart.header['Content-Transfer-Encoding'] = '8bit'
82 textpart.body = item.to_text(true, wrapto, false)
83 htmlpart = RMail::Message::new
84 htmlpart.header['Content-Type'] = 'text/html; charset=utf-8'
85 htmlpart.header['Content-Transfer-Encoding'] = '8bit'
86 htmlpart.body = item.to_html
87
88 # inline images as attachments
89 imgs = []
90 if inline_images
91 cids = []
92 htmlpart.body.gsub!(/(<img[^>]+)src="(\S+?\/([^\/]+?\.(png|gif|jpe?g)))"([^>]*>)/i) do |match|
93 # $2 contains url, $3 the image name, $4 the image extension
94 begin
95 image = Base64.encode64(HTTPFetcher::fetch($2, Time.at(0)).chomp) + "\n"
96 cid = "#{Digest::MD5.hexdigest($2)}@#{config.hostname}"
97 if not cids.include?(cid)
98 cids << cid
99 imgpart = RMail::Message.new
100 imgpart.header.set('Content-ID', "<#{cid}>")
101 type = $4
102 type = 'jpeg' if type.downcase == 'jpg' # hack hack hack
103 imgpart.header.set('Content-Type', "image/#{type}", 'name' => $3)
104 imgpart.header.set('Content-Disposition', 'attachment', 'filename' => $3)
105 imgpart.header.set('Content-Transfer-Encoding', 'base64')
106 imgpart.body = image
107 imgs << imgpart
108 end
109 # now to specify what to replace with
110 newtag = "#{$1}src=\"cid:#{cid}\"#{$5}"
111 #print "#{cid}: Replacing '#{$&}' with '#{newtag}'...\n"
112 newtag
113 rescue
114 #print "Error while fetching image #{$2}: #{$!}...\n"
115 $& # don't modify on exception
116 end
117 end
118 end
119 if imgs.length > 0
120 message.header.set('Content-Type', 'multipart/related', 'type'=> 'multipart/alternative')
121 texthtml = RMail::Message::new
122 texthtml.header.set('Content-Type', 'multipart/alternative')
123 texthtml.add_part(textpart)
124 texthtml.add_part(htmlpart)
125 message.add_part(texthtml)
126 imgs.each do |i|
127 message.add_part(i)
128 end
129 else
130 message.header['Content-Type'] = 'multipart/alternative'
131 message.add_part(textpart)
132 message.add_part(htmlpart)
133 end
134 return message.to_s
135 end
136
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server, or local Maildir
2 Copyright (c) 2009 Andreas Rottmann
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
16 =end
17
18 require 'uri'
19 require 'fileutils'
20 require 'fcntl'
21
22 class MaildirAccount
23 MYHOSTNAME = Socket.gethostname
24
25 attr_reader :uri
26
27 def putmail(folder, mail, date = Time::now)
28 store_message(folder_dir(folder), date, nil) do |f|
29 f.puts(mail)
30 end
31 end
32
33 def updatemail(folder, mail, idx, date = Time::now)
34 dir = folder_dir(folder)
35 guarantee_maildir(dir)
36 mail_files = find_mails(dir, idx)
37 flags = nil
38 if mail_files.length > 0
39 # get the info from the first result and delete everything
40 info = maildir_file_info(mail_files[0])
41 mail_files.each { |f| File.delete(File.join(dir, f)) }
42 end
43 store_message(dir, date, info) { |f| f.puts(mail) }
44 end
45
46 def to_s
47 uri.to_s
48 end
49
50 def cleanup(folder, dryrun = false)
51 dir = folder_dir(folder)
52 puts "-- Considering #{dir}:"
53 guarantee_maildir(dir)
54
55 del_count = 0
56 recent_time = Time.now() -- (3 * 24 * 60 * 60) # 3 days
57 Dir[File.join(dir, 'cur', '*')].each do |fn|
58 flags = maildir_file_info_flags(fn)
59 # don't consider not-seen, flagged, or recent messages
60 mtime = File.mtime(fn)
61 next if (not flags.index('S') or
62 flags.index('F') or
63 mtime > recent_time)
64 File.open(fn) do |f|
65 mail = RMail::Parser.read(f)
66 end
67 if dryrun
68 puts "To remove: #{subject} #{mtime}"
69 else
70 puts "Removing: #{subject} #{mtime}"
71 File.delete(fn)
72 end
73 del_count += 1
74 end
75 puts "-- Deleted #{del_count} messages"
76 return del_count
77 end
78
79 private
80
81 def folder_dir(folder)
82 return File.join('/', folder)
83 end
84
85 def store_message(dir, date, info, &block)
86 # TODO: handle `date'
87
88 guarantee_maildir(dir)
89
90 stored = false
91 Dir.chdir(dir) do |d|
92 timer = 30
93 fd = nil
94 while timer >= 0
95 new_fn = new_maildir_basefn
96 tmp_path = File.join(dir, 'tmp', new_fn)
97 new_path = File.join(dir, 'new', new_fn)
98 begin
99 fd = IO::sysopen(tmp_path,
100 Fcntl::O_WRONLY | Fcntl::O_EXCL | Fcntl::O_CREAT)
101 break
102 rescue Errno::EEXIST
103 sleep 2
104 timer -= 2
105 next
106 end
107 end
108
109 if fd
110 begin
111 f = IO.open(fd)
112 # provide a writable interface for the caller
113 yield f
114 f.fsync
115 File.link tmp_path, new_path
116 stored = true
117 ensure
118 File.unlink tmp_path if File.exists? tmp_path
119 end
120 end
121
122 if stored and info
123 cur_path = File.join(dir, 'cur', new_fn + ':' + info)
124 File.rename(new_path, cur_path)
125 end
126 end # Dir.chdir
127
128 return stored
129 end
130
131 def find_mails(dir, idx)
132 dir_paths = []
133 ['cur', 'new'].each do |d|
134 subdir = File.join(dir, d)
135 raise "#{subdir} not a directory" unless File.directory? subdir
136 Dir[File.join(subdir, '*')].each do |fn|
137 File.open(fn) do |f|
138 mail = RMail::Parser.read(f)
139 cache_index = mail.header['Message-Id']
140 next if not (cache_index and cache_index == idx)
141 dir_paths.push(File.join(d, File.basename(fn)))
142 end
143 end
144 end
145 return dir_paths
146 end
147
148 def guarantee_maildir(dir)
149 # Ensure maildir-folderness
150 ['new', 'cur', 'tmp'].each do |d|
151 FileUtils.mkdir_p(File.join(dir, d))
152 end
153 end
154
155 def maildir_file_info(file)
156 basename = File.basename(file)
157 colon = basename.rindex(':')
158
159 return (colon and basename.slice(colon + 1, -1))
160 end
161
162 # Shamelessly taken from
163 # http://gitorious.org/sup/mainline/blobs/master/lib/sup/maildir.rb
164 def new_maildir_basefn
165 Kernel::srand()
166 "#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
167 end
168 end
169
0 =begin
1 Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server
2 Copyright (c) 2005 Lucas Nussbaum <lucas@lucas-nussbaum.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 =end
18
19 require 'feedparser'
20
21 # Patch for REXML
22 # Very ugly patch to make REXML error-proof.
23 # The problem is REXML uses IConv, which isn't error-proof at all.
24 # With those changes, it uses unpack/pack with some error handling
25 module REXML
26 module Encoding
27 def decode(str)
28 return str.toUTF8(@encoding)
29 end
30
31 def encode(str)
32 return str
33 end
34
35 def encoding=(enc)
36 return if defined? @encoding and enc == @encoding
37 @encoding = enc || 'utf-8'
38 end
39 end
40
41 class Element
42 def children
43 @children
44 end
45 end
46 end
0 # File fetched from
1 # http://svn.ruby-lang.org/cgi-bin/viewvc.cgi/trunk/lib/net/imap.rb?view=log
2 # Current rev: 27336
3 ############################################################################
4 #
5 # = net/imap.rb
6 #
7 # Copyright (C) 2000 Shugo Maeda <shugo@ruby-lang.org>
8 #
9 # This library is distributed under the terms of the Ruby license.
10 # You can freely distribute/modify this library.
11 #
12 # Documentation: Shugo Maeda, with RDoc conversion and overview by William
13 # Webber.
14 #
15 # See Net::IMAP for documentation.
16 #
17
18
19 require "socket"
20 require "monitor"
21 require "digest/md5"
22 require "strscan"
23 begin
24 require "openssl"
25 rescue LoadError
26 end
27
28 module Net
29
30 #
31 # Net::IMAP implements Internet Message Access Protocol (IMAP) client
32 # functionality. The protocol is described in [IMAP].
33 #
34 # == IMAP Overview
35 #
36 # An IMAP client connects to a server, and then authenticates
37 # itself using either #authenticate() or #login(). Having
38 # authenticated itself, there is a range of commands
39 # available to it. Most work with mailboxes, which may be
40 # arranged in an hierarchical namespace, and each of which
41 # contains zero or more messages. How this is implemented on
42 # the server is implementation-dependent; on a UNIX server, it
43 # will frequently be implemented as a files in mailbox format
44 # within a hierarchy of directories.
45 #
46 # To work on the messages within a mailbox, the client must
47 # first select that mailbox, using either #select() or (for
48 # read-only access) #examine(). Once the client has successfully
49 # selected a mailbox, they enter _selected_ state, and that
50 # mailbox becomes the _current_ mailbox, on which mail-item
51 # related commands implicitly operate.
52 #
53 # Messages have two sorts of identifiers: message sequence
54 # numbers, and UIDs.
55 #
56 # Message sequence numbers number messages within a mail box
57 # from 1 up to the number of items in the mail box. If new
58 # message arrives during a session, it receives a sequence
59 # number equal to the new size of the mail box. If messages
60 # are expunged from the mailbox, remaining messages have their
61 # sequence numbers "shuffled down" to fill the gaps.
62 #
63 # UIDs, on the other hand, are permanently guaranteed not to
64 # identify another message within the same mailbox, even if
65 # the existing message is deleted. UIDs are required to
66 # be assigned in ascending (but not necessarily sequential)
67 # order within a mailbox; this means that if a non-IMAP client
68 # rearranges the order of mailitems within a mailbox, the
69 # UIDs have to be reassigned. An IMAP client cannot thus
70 # rearrange message orders.
71 #
72 # == Examples of Usage
73 #
74 # === List sender and subject of all recent messages in the default mailbox
75 #
76 # imap = Net::IMAP.new('mail.example.com')
77 # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
78 # imap.examine('INBOX')
79 # imap.search(["RECENT"]).each do |message_id|
80 # envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
81 # puts "#{envelope.from[0].name}: \t#{envelope.subject}"
82 # end
83 #
84 # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03"
85 #
86 # imap = Net::IMAP.new('mail.example.com')
87 # imap.authenticate('LOGIN', 'joe_user', 'joes_password')
88 # imap.select('Mail/sent-mail')
89 # if not imap.list('Mail/', 'sent-apr03')
90 # imap.create('Mail/sent-apr03')
91 # end
92 # imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
93 # imap.copy(message_id, "Mail/sent-apr03")
94 # imap.store(message_id, "+FLAGS", [:Deleted])
95 # end
96 # imap.expunge
97 #
98 # == Thread Safety
99 #
100 # Net::IMAP supports concurrent threads. For example,
101 #
102 # imap = Net::IMAP.new("imap.foo.net", "imap2")
103 # imap.authenticate("cram-md5", "bar", "password")
104 # imap.select("inbox")
105 # fetch_thread = Thread.start { imap.fetch(1..-1, "UID") }
106 # search_result = imap.search(["BODY", "hello"])
107 # fetch_result = fetch_thread.value
108 # imap.disconnect
109 #
110 # This script invokes the FETCH command and the SEARCH command concurrently.
111 #
112 # == Errors
113 #
114 # An IMAP server can send three different types of responses to indicate
115 # failure:
116 #
117 # NO:: the attempted command could not be successfully completed. For
118 # instance, the username/password used for logging in are incorrect;
119 # the selected mailbox does not exists; etc.
120 #
121 # BAD:: the request from the client does not follow the server's
122 # understanding of the IMAP protocol. This includes attempting
123 # commands from the wrong client state; for instance, attempting
124 # to perform a SEARCH command without having SELECTed a current
125 # mailbox. It can also signal an internal server
126 # failure (such as a disk crash) has occurred.
127 #
128 # BYE:: the server is saying goodbye. This can be part of a normal
129 # logout sequence, and can be used as part of a login sequence
130 # to indicate that the server is (for some reason) unwilling
131 # to accept our connection. As a response to any other command,
132 # it indicates either that the server is shutting down, or that
133 # the server is timing out the client connection due to inactivity.
134 #
135 # These three error response are represented by the errors
136 # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and
137 # Net::IMAP::ByeResponseError, all of which are subclasses of
138 # Net::IMAP::ResponseError. Essentially, all methods that involve
139 # sending a request to the server can generate one of these errors.
140 # Only the most pertinent instances have been documented below.
141 #
142 # Because the IMAP class uses Sockets for communication, its methods
143 # are also susceptible to the various errors that can occur when
144 # working with sockets. These are generally represented as
145 # Errno errors. For instance, any method that involves sending a
146 # request to the server and/or receiving a response from it could
147 # raise an Errno::EPIPE error if the network connection unexpectedly
148 # goes down. See the socket(7), ip(7), tcp(7), socket(2), connect(2),
149 # and associated man pages.
150 #
151 # Finally, a Net::IMAP::DataFormatError is thrown if low-level data
152 # is found to be in an incorrect format (for instance, when converting
153 # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is
154 # thrown if a server response is non-parseable.
155 #
156 #
157 # == References
158 #
159 # [[IMAP]]
160 # M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1",
161 # RFC 2060, December 1996. (Note: since obsoleted by RFC 3501)
162 #
163 # [[LANGUAGE-TAGS]]
164 # Alvestrand, H., "Tags for the Identification of
165 # Languages", RFC 1766, March 1995.
166 #
167 # [[MD5]]
168 # Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC
169 # 1864, October 1995.
170 #
171 # [[MIME-IMB]]
172 # Freed, N., and N. Borenstein, "MIME (Multipurpose Internet
173 # Mail Extensions) Part One: Format of Internet Message Bodies", RFC
174 # 2045, November 1996.
175 #
176 # [[RFC-822]]
177 # Crocker, D., "Standard for the Format of ARPA Internet Text
178 # Messages", STD 11, RFC 822, University of Delaware, August 1982.
179 #
180 # [[RFC-2087]]
181 # Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997.
182 #
183 # [[RFC-2086]]
184 # Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997.
185 #
186 # [[RFC-2195]]
187 # Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension
188 # for Simple Challenge/Response", RFC 2195, September 1997.
189 #
190 # [[SORT-THREAD-EXT]]
191 # Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD
192 # Extensions", draft-ietf-imapext-sort, May 2003.
193 #
194 # [[OSSL]]
195 # http://www.openssl.org
196 #
197 # [[RSSL]]
198 # http://savannah.gnu.org/projects/rubypki
199 #
200 # [[UTF7]]
201 # Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of
202 # Unicode", RFC 2152, May 1997.
203 #
204 class IMAP
205 include MonitorMixin
206 if defined?(OpenSSL)
207 include OpenSSL
208 include SSL
209 end
210
211 # Returns an initial greeting response from the server.
212 attr_reader :greeting
213
214 # Returns recorded untagged responses. For example:
215 #
216 # imap.select("inbox")
217 # p imap.responses["EXISTS"][-1]
218 # #=> 2
219 # p imap.responses["UIDVALIDITY"][-1]
220 # #=> 968263756
221 attr_reader :responses
222
223 # Returns all response handlers.
224 attr_reader :response_handlers
225
226 # The thread to receive exceptions.
227 attr_accessor :client_thread
228
229 # Flag indicating a message has been seen
230 SEEN = :Seen
231
232 # Flag indicating a message has been answered
233 ANSWERED = :Answered
234
235 # Flag indicating a message has been flagged for special or urgent
236 # attention
237 FLAGGED = :Flagged
238
239 # Flag indicating a message has been marked for deletion. This
240 # will occur when the mailbox is closed or expunged.
241 DELETED = :Deleted
242
243 # Flag indicating a message is only a draft or work-in-progress version.
244 DRAFT = :Draft
245
246 # Flag indicating that the message is "recent", meaning that this
247 # session is the first session in which the client has been notified
248 # of this message.
249 RECENT = :Recent
250
251 # Flag indicating that a mailbox context name cannot contain
252 # children.
253 NOINFERIORS = :Noinferiors
254
255 # Flag indicating that a mailbox is not selected.
256 NOSELECT = :Noselect
257
258 # Flag indicating that a mailbox has been marked "interesting" by
259 # the server; this commonly indicates that the mailbox contains
260 # new messages.
261 MARKED = :Marked
262
263 # Flag indicating that the mailbox does not contains new messages.
264 UNMARKED = :Unmarked
265
266 # Returns the debug mode.
267 def self.debug
268 return @@debug
269 end
270
271 # Sets the debug mode.
272 def self.debug=(val)
273 return @@debug = val
274 end
275
276 # Returns the max number of flags interned to symbols.
277 def self.max_flag_count
278 return @@max_flag_count
279 end
280
281 # Sets the max number of flags interned to symbols.
282 def self.max_flag_count=(count)
283 @@max_flag_count = count
284 end
285
286 # Adds an authenticator for Net::IMAP#authenticate. +auth_type+
287 # is the type of authentication this authenticator supports
288 # (for instance, "LOGIN"). The +authenticator+ is an object
289 # which defines a process() method to handle authentication with
290 # the server. See Net::IMAP::LoginAuthenticator,
291 # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator
292 # for examples.
293 #
294 #
295 # If +auth_type+ refers to an existing authenticator, it will be
296 # replaced by the new one.
297 def self.add_authenticator(auth_type, authenticator)
298 @@authenticators[auth_type] = authenticator
299 end
300
301 # Disconnects from the server.
302 def disconnect
303 begin
304 begin
305 # try to call SSL::SSLSocket#io.
306 @sock.io.shutdown
307 rescue NoMethodError
308 # @sock is not an SSL::SSLSocket.
309 @sock.shutdown
310 end
311 rescue Errno::ENOTCONN
312 # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
313 end
314 @receiver_thread.join
315 @sock.close
316 end
317
318 # Returns true if disconnected from the server.
319 def disconnected?
320 return @sock.closed?
321 end
322
323 # Sends a CAPABILITY command, and returns an array of
324 # capabilities that the server supports. Each capability
325 # is a string. See [IMAP] for a list of possible
326 # capabilities.
327 #
328 # Note that the Net::IMAP class does not modify its
329 # behaviour according to the capabilities of the server;
330 # it is up to the user of the class to ensure that
331 # a certain capability is supported by a server before
332 # using it.
333 def capability
334 synchronize do
335 send_command("CAPABILITY")
336 return @responses.delete("CAPABILITY")[-1]
337 end
338 end
339
340 # Sends a NOOP command to the server. It does nothing.
341 def noop
342 send_command("NOOP")
343 end
344
345 # Sends a LOGOUT command to inform the server that the client is
346 # done with the connection.
347 def logout
348 send_command("LOGOUT")
349 end
350
351 # Sends a STARTTLS command to start TLS session.
352 def starttls(options = {}, verify = true)
353 send_command("STARTTLS") do |resp|
354 if resp.kind_of?(TaggedResponse) && resp.name == "OK"
355 begin
356 # for backward compatibility
357 certs = options.to_str
358 options = create_ssl_params(certs, verify)
359 rescue NoMethodError
360 end
361 start_tls_session(options)
362 end
363 end
364 end
365
366 # Sends an AUTHENTICATE command to authenticate the client.
367 # The +auth_type+ parameter is a string that represents
368 # the authentication mechanism to be used. Currently Net::IMAP
369 # supports authentication mechanisms:
370 #
371 # LOGIN:: login using cleartext user and password.
372 # CRAM-MD5:: login with cleartext user and encrypted password
373 # (see [RFC-2195] for a full description). This
374 # mechanism requires that the server have the user's
375 # password stored in clear-text password.
376 #
377 # For both these mechanisms, there should be two +args+: username
378 # and (cleartext) password. A server may not support one or other
379 # of these mechanisms; check #capability() for a capability of
380 # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
381 #
382 # Authentication is done using the appropriate authenticator object:
383 # see @@authenticators for more information on plugging in your own
384 # authenticator.
385 #
386 # For example:
387 #
388 # imap.authenticate('LOGIN', user, password)
389 #
390 # A Net::IMAP::NoResponseError is raised if authentication fails.
391 def authenticate(auth_type, *args)
392 auth_type = auth_type.upcase
393 unless @@authenticators.has_key?(auth_type)
394 raise ArgumentError,
395 format('unknown auth type - "%s"', auth_type)
396 end
397 authenticator = @@authenticators[auth_type].new(*args)
398 send_command("AUTHENTICATE", auth_type) do |resp|
399 if resp.instance_of?(ContinuationRequest)
400 data = authenticator.process(resp.data.text.unpack("m")[0])
401 s = [data].pack("m").gsub(/\n/, "")
402 send_string_data(s)
403 put_string(CRLF)
404 end
405 end
406 end
407
408 # Sends a LOGIN command to identify the client and carries
409 # the plaintext +password+ authenticating this +user+. Note
410 # that, unlike calling #authenticate() with an +auth_type+
411 # of "LOGIN", #login() does *not* use the login authenticator.
412 #
413 # A Net::IMAP::NoResponseError is raised if authentication fails.
414 def login(user, password)
415 send_command("LOGIN", user, password)
416 end
417
418 # Sends a SELECT command to select a +mailbox+ so that messages
419 # in the +mailbox+ can be accessed.
420 #
421 # After you have selected a mailbox, you may retrieve the
422 # number of items in that mailbox from @responses["EXISTS"][-1],
423 # and the number of recent messages from @responses["RECENT"][-1].
424 # Note that these values can change if new messages arrive
425 # during a session; see #add_response_handler() for a way of
426 # detecting this event.
427 #
428 # A Net::IMAP::NoResponseError is raised if the mailbox does not
429 # exist or is for some reason non-selectable.
430 def select(mailbox)
431 synchronize do
432 @responses.clear
433 send_command("SELECT", mailbox)
434 end
435 end
436
437 # Sends a EXAMINE command to select a +mailbox+ so that messages
438 # in the +mailbox+ can be accessed. Behaves the same as #select(),
439 # except that the selected +mailbox+ is identified as read-only.
440 #
441 # A Net::IMAP::NoResponseError is raised if the mailbox does not
442 # exist or is for some reason non-examinable.
443 def examine(mailbox)
444 synchronize do
445 @responses.clear
446 send_command("EXAMINE", mailbox)
447 end
448 end
449
450 # Sends a CREATE command to create a new +mailbox+.
451 #
452 # A Net::IMAP::NoResponseError is raised if a mailbox with that name
453 # cannot be created.
454 def create(mailbox)
455 send_command("CREATE", mailbox)
456 end
457
458 # Sends a DELETE command to remove the +mailbox+.
459 #
460 # A Net::IMAP::NoResponseError is raised if a mailbox with that name
461 # cannot be deleted, either because it does not exist or because the
462 # client does not have permission to delete it.
463 def delete(mailbox)
464 send_command("DELETE", mailbox)
465 end
466
467 # Sends a RENAME command to change the name of the +mailbox+ to
468 # +newname+.
469 #
470 # A Net::IMAP::NoResponseError is raised if a mailbox with the
471 # name +mailbox+ cannot be renamed to +newname+ for whatever
472 # reason; for instance, because +mailbox+ does not exist, or
473 # because there is already a mailbox with the name +newname+.
474 def rename(mailbox, newname)
475 send_command("RENAME", mailbox, newname)
476 end
477
478 # Sends a SUBSCRIBE command to add the specified +mailbox+ name to
479 # the server's set of "active" or "subscribed" mailboxes as returned
480 # by #lsub().
481 #
482 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
483 # subscribed to, for instance because it does not exist.
484 def subscribe(mailbox)
485 send_command("SUBSCRIBE", mailbox)
486 end
487
488 # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name
489 # from the server's set of "active" or "subscribed" mailboxes.
490 #
491 # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be
492 # unsubscribed from, for instance because the client is not currently
493 # subscribed to it.
494 def unsubscribe(mailbox)
495 send_command("UNSUBSCRIBE", mailbox)
496 end
497
498 # Sends a LIST command, and returns a subset of names from
499 # the complete set of all names available to the client.
500 # +refname+ provides a context (for instance, a base directory
501 # in a directory-based mailbox hierarchy). +mailbox+ specifies
502 # a mailbox or (via wildcards) mailboxes under that context.
503 # Two wildcards may be used in +mailbox+: '*', which matches
504 # all characters *including* the hierarchy delimiter (for instance,
505 # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%',
506 # which matches all characters *except* the hierarchy delimiter.
507 #
508 # If +refname+ is empty, +mailbox+ is used directly to determine
509 # which mailboxes to match. If +mailbox+ is empty, the root
510 # name of +refname+ and the hierarchy delimiter are returned.
511 #
512 # The return value is an array of +Net::IMAP::MailboxList+. For example:
513 #
514 # imap.create("foo/bar")
515 # imap.create("foo/baz")
516 # p imap.list("", "foo/%")
517 # #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\
518 # #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\
519 # #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">]
520 def list(refname, mailbox)
521 synchronize do
522 send_command("LIST", refname, mailbox)
523 return @responses.delete("LIST")
524 end
525 end
526
527 # Sends the GETQUOTAROOT command along with specified +mailbox+.
528 # This command is generally available to both admin and user.
529 # If mailbox exists, returns an array containing objects of
530 # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota.
531 def getquotaroot(mailbox)
532 synchronize do
533 send_command("GETQUOTAROOT", mailbox)
534 result = []
535 result.concat(@responses.delete("QUOTAROOT"))
536 result.concat(@responses.delete("QUOTA"))
537 return result
538 end
539 end
540
541 # Sends the GETQUOTA command along with specified +mailbox+.
542 # If this mailbox exists, then an array containing a
543 # Net::IMAP::MailboxQuota object is returned. This
544 # command generally is only available to server admin.
545 def getquota(mailbox)
546 synchronize do
547 send_command("GETQUOTA", mailbox)
548 return @responses.delete("QUOTA")
549 end
550 end
551
552 # Sends a SETQUOTA command along with the specified +mailbox+ and
553 # +quota+. If +quota+ is nil, then quota will be unset for that
554 # mailbox. Typically one needs to be logged in as server admin
555 # for this to work. The IMAP quota commands are described in
556 # [RFC-2087].
557 def setquota(mailbox, quota)
558 if quota.nil?
559 data = '()'
560 else
561 data = '(STORAGE ' + quota.to_s + ')'
562 end
563 send_command("SETQUOTA", mailbox, RawData.new(data))
564 end
565
566 # Sends the SETACL command along with +mailbox+, +user+ and the
567 # +rights+ that user is to have on that mailbox. If +rights+ is nil,
568 # then that user will be stripped of any rights to that mailbox.
569 # The IMAP ACL commands are described in [RFC-2086].
570 def setacl(mailbox, user, rights)
571 if rights.nil?
572 send_command("SETACL", mailbox, user, "")
573 else
574 send_command("SETACL", mailbox, user, rights)
575 end
576 end
577
578 # Send the GETACL command along with specified +mailbox+.
579 # If this mailbox exists, an array containing objects of
580 # Net::IMAP::MailboxACLItem will be returned.
581 def getacl(mailbox)
582 synchronize do
583 send_command("GETACL", mailbox)
584 return @responses.delete("ACL")[-1]
585 end
586 end
587
588 # Sends a LSUB command, and returns a subset of names from the set
589 # of names that the user has declared as being "active" or
590 # "subscribed". +refname+ and +mailbox+ are interpreted as
591 # for #list().
592 # The return value is an array of +Net::IMAP::MailboxList+.
593 def lsub(refname, mailbox)
594 synchronize do
595 send_command("LSUB", refname, mailbox)
596 return @responses.delete("LSUB")
597 end
598 end
599
600 # Sends a STATUS command, and returns the status of the indicated
601 # +mailbox+. +attr+ is a list of one or more attributes that
602 # we are request the status of. Supported attributes include:
603 #
604 # MESSAGES:: the number of messages in the mailbox.
605 # RECENT:: the number of recent messages in the mailbox.
606 # UNSEEN:: the number of unseen messages in the mailbox.
607 #
608 # The return value is a hash of attributes. For example:
609 #
610 # p imap.status("inbox", ["MESSAGES", "RECENT"])
611 # #=> {"RECENT"=>0, "MESSAGES"=>44}
612 #
613 # A Net::IMAP::NoResponseError is raised if status values
614 # for +mailbox+ cannot be returned, for instance because it
615 # does not exist.
616 def status(mailbox, attr)
617 synchronize do
618 send_command("STATUS", mailbox, attr)
619 return @responses.delete("STATUS")[-1].attr
620 end
621 end
622
623 # Sends a APPEND command to append the +message+ to the end of
624 # the +mailbox+. The optional +flags+ argument is an array of
625 # flags to initially passing to the new message. The optional
626 # +date_time+ argument specifies the creation time to assign to the
627 # new message; it defaults to the current time.
628 # For example:
629 #
630 # imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now)
631 # Subject: hello
632 # From: shugo@ruby-lang.org
633 # To: shugo@ruby-lang.org
634 #
635 # hello world
636 # EOF
637 #
638 # A Net::IMAP::NoResponseError is raised if the mailbox does
639 # not exist (it is not created automatically), or if the flags,
640 # date_time, or message arguments contain errors.
641 def append(mailbox, message, flags = nil, date_time = nil)
642 args = []
643 if flags
644 args.push(flags)
645 end
646 args.push(date_time) if date_time
647 args.push(Literal.new(message))
648 send_command("APPEND", mailbox, *args)
649 end
650
651 # Sends a CHECK command to request a checkpoint of the currently
652 # selected mailbox. This performs implementation-specific
653 # housekeeping, for instance, reconciling the mailbox's
654 # in-memory and on-disk state.
655 def check
656 send_command("CHECK")
657 end
658
659 # Sends a CLOSE command to close the currently selected mailbox.
660 # The CLOSE command permanently removes from the mailbox all
661 # messages that have the \Deleted flag set.
662 def close
663 send_command("CLOSE")
664 end
665
666 # Sends a EXPUNGE command to permanently remove from the currently
667 # selected mailbox all messages that have the \Deleted flag set.
668 def expunge
669 synchronize do
670 send_command("EXPUNGE")
671 return @responses.delete("EXPUNGE")
672 end
673 end
674
675 # Sends a SEARCH command to search the mailbox for messages that
676 # match the given searching criteria, and returns message sequence
677 # numbers. +keys+ can either be a string holding the entire
678 # search string, or a single-dimension array of search keywords and
679 # arguments. The following are some common search criteria;
680 # see [IMAP] section 6.4.4 for a full list.
681 #
682 # <message set>:: a set of message sequence numbers. ',' indicates
683 # an interval, ':' indicates a range. For instance,
684 # '2,10:12,15' means "2,10,11,12,15".
685 #
686 # BEFORE <date>:: messages with an internal date strictly before
687 # <date>. The date argument has a format similar
688 # to 8-Aug-2002.
689 #
690 # BODY <string>:: messages that contain <string> within their body.
691 #
692 # CC <string>:: messages containing <string> in their CC field.
693 #
694 # FROM <string>:: messages that contain <string> in their FROM field.
695 #
696 # NEW:: messages with the \Recent, but not the \Seen, flag set.
697 #
698 # NOT <search-key>:: negate the following search key.
699 #
700 # OR <search-key> <search-key>:: "or" two search keys together.
701 #
702 # ON <date>:: messages with an internal date exactly equal to <date>,
703 # which has a format similar to 8-Aug-2002.
704 #
705 # SINCE <date>:: messages with an internal date on or after <date>.
706 #
707 # SUBJECT <string>:: messages with <string> in their subject.
708 #
709 # TO <string>:: messages with <string> in their TO field.
710 #
711 # For example:
712 #
713 # p imap.search(["SUBJECT", "hello", "NOT", "NEW"])
714 # #=> [1, 6, 7, 8]
715 def search(keys, charset = nil)
716 return search_internal("SEARCH", keys, charset)
717 end
718
719 # As for #search(), but returns unique identifiers.
720 def uid_search(keys, charset = nil)
721 return search_internal("UID SEARCH", keys, charset)
722 end
723
724 # Sends a FETCH command to retrieve data associated with a message
725 # in the mailbox. The +set+ parameter is a number or an array of
726 # numbers or a Range object. The number is a message sequence
727 # number. +attr+ is a list of attributes to fetch; see the
728 # documentation for Net::IMAP::FetchData for a list of valid
729 # attributes.
730 # The return value is an array of Net::IMAP::FetchData. For example:
731 #
732 # p imap.fetch(6..8, "UID")
733 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\
734 # #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\
735 # #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>]
736 # p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]")
737 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>]
738 # data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0]
739 # p data.seqno
740 # #=> 6
741 # p data.attr["RFC822.SIZE"]
742 # #=> 611
743 # p data.attr["INTERNALDATE"]
744 # #=> "12-Oct-2000 22:40:59 +0900"
745 # p data.attr["UID"]
746 # #=> 98
747 def fetch(set, attr)
748 return fetch_internal("FETCH", set, attr)
749 end
750
751 # As for #fetch(), but +set+ contains unique identifiers.
752 def uid_fetch(set, attr)
753 return fetch_internal("UID FETCH", set, attr)
754 end
755
756 # Sends a STORE command to alter data associated with messages
757 # in the mailbox, in particular their flags. The +set+ parameter
758 # is a number or an array of numbers or a Range object. Each number
759 # is a message sequence number. +attr+ is the name of a data item
760 # to store: 'FLAGS' means to replace the message's flag list
761 # with the provided one; '+FLAGS' means to add the provided flags;
762 # and '-FLAGS' means to remove them. +flags+ is a list of flags.
763 #
764 # The return value is an array of Net::IMAP::FetchData. For example:
765 #
766 # p imap.store(6..8, "+FLAGS", [:Deleted])
767 # #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
768 # #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\
769 # #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>]
770 def store(set, attr, flags)
771 return store_internal("STORE", set, attr, flags)
772 end
773
774 # As for #store(), but +set+ contains unique identifiers.
775 def uid_store(set, attr, flags)
776 return store_internal("UID STORE", set, attr, flags)
777 end
778
779 # Sends a COPY command to copy the specified message(s) to the end
780 # of the specified destination +mailbox+. The +set+ parameter is
781 # a number or an array of numbers or a Range object. The number is
782 # a message sequence number.
783 def copy(set, mailbox)
784 copy_internal("COPY", set, mailbox)
785 end
786
787 # As for #copy(), but +set+ contains unique identifiers.
788 def uid_copy(set, mailbox)
789 copy_internal("UID COPY", set, mailbox)
790 end
791
792 # Sends a SORT command to sort messages in the mailbox.
793 # Returns an array of message sequence numbers. For example:
794 #
795 # p imap.sort(["FROM"], ["ALL"], "US-ASCII")
796 # #=> [1, 2, 3, 5, 6, 7, 8, 4, 9]
797 # p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII")
798 # #=> [6, 7, 8, 1]
799 #
800 # See [SORT-THREAD-EXT] for more details.
801 def sort(sort_keys, search_keys, charset)
802 return sort_internal("SORT", sort_keys, search_keys, charset)
803 end
804
805 # As for #sort(), but returns an array of unique identifiers.
806 def uid_sort(sort_keys, search_keys, charset)
807 return sort_internal("UID SORT", sort_keys, search_keys, charset)
808 end
809
810 # Adds a response handler. For example, to detect when
811 # the server sends us a new EXISTS response (which normally
812 # indicates new messages being added to the mail box),
813 # you could add the following handler after selecting the
814 # mailbox.
815 #
816 # imap.add_response_handler { |resp|
817 # if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
818 # puts "Mailbox now has #{resp.data} messages"
819 # end
820 # }
821 #
822 def add_response_handler(handler = Proc.new)
823 @response_handlers.push(handler)
824 end
825
826 # Removes the response handler.
827 def remove_response_handler(handler)
828 @response_handlers.delete(handler)
829 end
830
831 # As for #search(), but returns message sequence numbers in threaded
832 # format, as a Net::IMAP::ThreadMember tree. The supported algorithms
833 # are:
834 #
835 # ORDEREDSUBJECT:: split into single-level threads according to subject,
836 # ordered by date.
837 # REFERENCES:: split into threads by parent/child relationships determined
838 # by which message is a reply to which.
839 #
840 # Unlike #search(), +charset+ is a required argument. US-ASCII
841 # and UTF-8 are sample values.
842 #
843 # See [SORT-THREAD-EXT] for more details.
844 def thread(algorithm, search_keys, charset)
845 return thread_internal("THREAD", algorithm, search_keys, charset)
846 end
847
848 # As for #thread(), but returns unique identifiers instead of
849 # message sequence numbers.
850 def uid_thread(algorithm, search_keys, charset)
851 return thread_internal("UID THREAD", algorithm, search_keys, charset)
852 end
853
854 # Sends an IDLE command that waits for notifications of new or expunged
855 # messages. Yields responses from the server during the IDLE.
856 #
857 # Use #idle_done() to leave IDLE.
858 def idle(&response_handler)
859 raise LocalJumpError, "no block given" unless response_handler
860
861 response = nil
862
863 synchronize do
864 tag = Thread.current[:net_imap_tag] = generate_tag
865 put_string("#{tag} IDLE#{CRLF}")
866
867 begin
868 add_response_handler(response_handler)
869 @idle_done_cond = new_cond
870 @idle_done_cond.wait
871 @idle_done_cond = nil
872 ensure
873 remove_response_handler(response_handler)
874 put_string("DONE#{CRLF}")
875 response = get_tagged_response(tag, "IDLE")
876 end
877 end
878
879 return response
880 end
881
882 # Leaves IDLE.
883 def idle_done
884 synchronize do
885 if @idle_done_cond.nil?
886 raise Net::IMAP::Error, "not during IDLE"
887 end
888 @idle_done_cond.signal
889 end
890 end
891
892 # Decode a string from modified UTF-7 format to UTF-8.
893 #
894 # UTF-7 is a 7-bit encoding of Unicode [UTF7]. IMAP uses a
895 # slightly modified version of this to encode mailbox names
896 # containing non-ASCII characters; see [IMAP] section 5.1.3.
897 #
898 # Net::IMAP does _not_ automatically encode and decode
899 # mailbox names to and from utf7.
900 def self.decode_utf7(s)
901 return s.gsub(/&(.*?)-/n) {
902 if $1.empty?
903 "&"
904 else
905 base64 = $1.tr(",", "/")
906 x = base64.length % 4
907 if x > 0
908 base64.concat("=" * (4 - x))
909 end
910 base64.unpack("m")[0].unpack("n*").pack("U*")
911 end
912 }.force_encoding("UTF-8")
913 end
914
915 # Encode a string from UTF-8 format to modified UTF-7.
916 def self.encode_utf7(s)
917 return s.gsub(/(&)|([^\x20-\x7e]+)/u) {
918 if $1
919 "&-"
920 else
921 base64 = [$&.unpack("U*").pack("n*")].pack("m")
922 "&" + base64.delete("=\n").tr("/", ",") + "-"
923 end
924 }.force_encoding("ASCII-8BIT")
925 end
926
927 # Formats +time+ as an IMAP-style date.
928 def self.format_date(time)
929 return time.strftime('%d-%b-%Y')
930 end
931
932 # Formats +time+ as an IMAP-style date-time.
933 def self.format_datetime(time)
934 return time.strftime('%d-%b-%Y %H:%M %z')
935 end
936
937 private
938
939 CRLF = "\r\n" # :nodoc:
940 PORT = 143 # :nodoc:
941 SSL_PORT = 993 # :nodoc:
942
943 @@debug = false
944 @@authenticators = {}
945 @@max_flag_count = 10000
946
947 # call-seq:
948 # Net::IMAP.new(host, options = {})
949 #
950 # Creates a new Net::IMAP object and connects it to the specified
951 # +host+.
952 #
953 # +options+ is an option hash, each key of which is a symbol.
954 #
955 # The available options are:
956 #
957 # port:: port number (default value is 143 for imap, or 993 for imaps)
958 # ssl:: if options[:ssl] is true, then an attempt will be made
959 # to use SSL (now TLS) to connect to the server. For this to work
960 # OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to
961 # be installed.
962 # if options[:ssl] is a hash, it's passed to
963 # OpenSSL::SSL::SSLContext#set_params as parameters.
964 #
965 # The most common errors are:
966 #
967 # Errno::ECONNREFUSED:: connection refused by +host+ or an intervening
968 # firewall.
969 # Errno::ETIMEDOUT:: connection timed out (possibly due to packets
970 # being dropped by an intervening firewall).
971 # Errno::ENETUNREACH:: there is no route to that network.
972 # SocketError:: hostname not known or other socket error.
973 # Net::IMAP::ByeResponseError:: we connected to the host, but they
974 # immediately said goodbye to us.
975 def initialize(host, port_or_options = {},
976 usessl = false, certs = nil, verify = true)
977 super()
978 @host = host
979 begin
980 options = port_or_options.to_hash
981 rescue NoMethodError
982 # for backward compatibility
983 options = {}
984 options[:port] = port_or_options
985 if usessl
986 options[:ssl] = create_ssl_params(certs, verify)
987 end
988 end
989 @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT)
990 @tag_prefix = "RUBY"
991 @tagno = 0
992 @parser = ResponseParser.new
993 @sock = TCPSocket.open(@host, @port)
994 if options[:ssl]
995 start_tls_session(options[:ssl])
996 @usessl = true
997 else
998 @usessl = false
999 end
1000 @responses = Hash.new([].freeze)
1001 @tagged_responses = {}
1002 @response_handlers = []
1003 @tagged_response_arrival = new_cond
1004 @continuation_request_arrival = new_cond
1005 @idle_done_cond = nil
1006 @logout_command_tag = nil
1007 @debug_output_bol = true
1008 @exception = nil
1009
1010 @greeting = get_response
1011 if @greeting.name == "BYE"
1012 @sock.close
1013 raise ByeResponseError, @greeting
1014 end
1015
1016 @client_thread = Thread.current
1017 @receiver_thread = Thread.start {
1018 receive_responses
1019 }
1020 end
1021
1022 def receive_responses
1023 connection_closed = false
1024 until connection_closed
1025 synchronize do
1026 @exception = nil
1027 end
1028 begin
1029 resp = get_response
1030 rescue Exception => e
1031 synchronize do
1032 @sock.close
1033 @exception = e
1034 end
1035 break
1036 end
1037 unless resp
1038 synchronize do
1039 @exception = EOFError.new("end of file reached")
1040 end
1041 break
1042 end
1043 begin
1044 synchronize do
1045 case resp
1046 when TaggedResponse
1047 @tagged_responses[resp.tag] = resp
1048 @tagged_response_arrival.broadcast
1049 if resp.tag == @logout_command_tag
1050 return
1051 end
1052 when UntaggedResponse
1053 record_response(resp.name, resp.data)
1054 if resp.data.instance_of?(ResponseText) &&
1055 (code = resp.data.code)
1056 record_response(code.name, code.data)
1057 end
1058 if resp.name == "BYE" && @logout_command_tag.nil?
1059 @sock.close
1060 @exception = ByeResponseError.new(resp)
1061 connection_closed = true
1062 end
1063 when ContinuationRequest
1064 @continuation_request_arrival.signal
1065 end
1066 @response_handlers.each do |handler|
1067 handler.call(resp)
1068 end
1069 end
1070 rescue Exception => e
1071 @exception = e
1072 synchronize do
1073 @tagged_response_arrival.broadcast
1074 @continuation_request_arrival.broadcast
1075 end
1076 end
1077 end
1078 synchronize do
1079 @tagged_response_arrival.broadcast
1080 @continuation_request_arrival.broadcast
1081 end
1082 end
1083
1084 def get_tagged_response(tag, cmd)
1085 until @tagged_responses.key?(tag)
1086 raise @exception if @exception
1087 @tagged_response_arrival.wait
1088 end
1089 resp = @tagged_responses.delete(tag)
1090 case resp.name
1091 when /\A(?:NO)\z/ni
1092 raise NoResponseError, resp
1093 when /\A(?:BAD)\z/ni
1094 raise BadResponseError, resp
1095 else
1096 return resp
1097 end
1098 end
1099
1100 def get_response
1101 buff = ""
1102 while true
1103 s = @sock.gets(CRLF)
1104 break unless s
1105 buff.concat(s)
1106 if /\{(\d+)\}\r\n/n =~ s
1107 s = @sock.read($1.to_i)
1108 buff.concat(s)
1109 else
1110 break
1111 end
1112 end
1113 return nil if buff.length == 0
1114 if @@debug
1115 $stderr.print(buff.gsub(/^/n, "S: "))
1116 end
1117 return @parser.parse(buff)
1118 end
1119
1120 def record_response(name, data)
1121 unless @responses.has_key?(name)
1122 @responses[name] = []
1123 end
1124 @responses[name].push(data)
1125 end
1126
1127 def send_command(cmd, *args, &block)
1128 synchronize do
1129 args.each do |i|
1130 validate_data(i)
1131 end
1132 tag = generate_tag
1133 put_string(tag + " " + cmd)
1134 args.each do |i|
1135 put_string(" ")
1136 send_data(i)
1137 end
1138 put_string(CRLF)
1139 if cmd == "LOGOUT"
1140 @logout_command_tag = tag
1141 end
1142 if block
1143 add_response_handler(block)
1144 end
1145 begin
1146 return get_tagged_response(tag, cmd)
1147 ensure
1148 if block
1149 remove_response_handler(block)
1150 end
1151 end
1152 end
1153 end
1154
1155 def generate_tag
1156 @tagno += 1
1157 return format("%s%04d", @tag_prefix, @tagno)
1158 end
1159
1160 def put_string(str)
1161 @sock.print(str)
1162 if @@debug
1163 if @debug_output_bol
1164 $stderr.print("C: ")
1165 end
1166 $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: "))
1167 if /\r\n\z/n.match(str)
1168 @debug_output_bol = true
1169 else
1170 @debug_output_bol = false
1171 end
1172 end
1173 end
1174
1175 def validate_data(data)
1176 case data
1177 when nil
1178 when String
1179 when Integer
1180 if data < 0 || data >= 4294967296
1181 raise DataFormatError, num.to_s
1182 end
1183 when Array
1184 data.each do |i|
1185 validate_data(i)
1186 end
1187 when Time
1188 when Symbol
1189 else
1190 data.validate
1191 end
1192 end
1193
1194 def send_data(data)
1195 case data
1196 when nil
1197 put_string("NIL")
1198 when String
1199 send_string_data(data)
1200 when Integer
1201 send_number_data(data)
1202 when Array
1203 send_list_data(data)
1204 when Time
1205 send_time_data(data)
1206 when Symbol
1207 send_symbol_data(data)
1208 else
1209 data.send_data(self)
1210 end
1211 end
1212
1213 def send_string_data(str)
1214 case str
1215 when ""
1216 put_string('""')
1217 when /[\x80-\xff\r\n]/n
1218 # literal
1219 send_literal(str)
1220 when /[(){ \x00-\x1f\x7f%*"\\]/n
1221 # quoted string
1222 send_quoted_string(str)
1223 else
1224 put_string(str)
1225 end
1226 end
1227
1228 def send_quoted_string(str)
1229 put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"')
1230 end
1231
1232 def send_literal(str)
1233 put_string("{" + str.length.to_s + "}" + CRLF)
1234 @continuation_request_arrival.wait
1235 raise @exception if @exception
1236 put_string(str)
1237 end
1238
1239 def send_number_data(num)
1240 put_string(num.to_s)
1241 end
1242
1243 def send_list_data(list)
1244 put_string("(")
1245 first = true
1246 list.each do |i|
1247 if first
1248 first = false
1249 else
1250 put_string(" ")
1251 end
1252 send_data(i)
1253 end
1254 put_string(")")
1255 end
1256
1257 DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
1258
1259 def send_time_data(time)
1260 t = time.dup.gmtime
1261 s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"',
1262 t.day, DATE_MONTH[t.month - 1], t.year,
1263 t.hour, t.min, t.sec)
1264 put_string(s)
1265 end
1266
1267 def send_symbol_data(symbol)
1268 put_string("\\" + symbol.to_s)
1269 end
1270
1271 def search_internal(cmd, keys, charset)
1272 if keys.instance_of?(String)
1273 keys = [RawData.new(keys)]
1274 else
1275 normalize_searching_criteria(keys)
1276 end
1277 synchronize do
1278 if charset
1279 send_command(cmd, "CHARSET", charset, *keys)
1280 else
1281 send_command(cmd, *keys)
1282 end
1283 return @responses.delete("SEARCH")[-1]
1284 end
1285 end
1286
1287 def fetch_internal(cmd, set, attr)
1288 if attr.instance_of?(String)
1289 attr = RawData.new(attr)
1290 end
1291 synchronize do
1292 @responses.delete("FETCH")
1293 send_command(cmd, MessageSet.new(set), attr)
1294 return @responses.delete("FETCH")
1295 end
1296 end
1297
1298 def store_internal(cmd, set, attr, flags)
1299 if attr.instance_of?(String)
1300 attr = RawData.new(attr)
1301 end
1302 synchronize do
1303 @responses.delete("FETCH")
1304 send_command(cmd, MessageSet.new(set), attr, flags)
1305 return @responses.delete("FETCH")
1306 end
1307 end
1308
1309 def copy_internal(cmd, set, mailbox)
1310 send_command(cmd, MessageSet.new(set), mailbox)
1311 end
1312
1313 def sort_internal(cmd, sort_keys, search_keys, charset)
1314 if search_keys.instance_of?(String)
1315 search_keys = [RawData.new(search_keys)]
1316 else
1317 normalize_searching_criteria(search_keys)
1318 end
1319 normalize_searching_criteria(search_keys)
1320 synchronize do
1321 send_command(cmd, sort_keys, charset, *search_keys)
1322 return @responses.delete("SORT")[-1]
1323 end
1324 end
1325
1326 def thread_internal(cmd, algorithm, search_keys, charset)
1327 if search_keys.instance_of?(String)
1328 search_keys = [RawData.new(search_keys)]
1329 else
1330 normalize_searching_criteria(search_keys)
1331 end
1332 normalize_searching_criteria(search_keys)
1333 send_command(cmd, algorithm, charset, *search_keys)
1334 return @responses.delete("THREAD")[-1]
1335 end
1336
1337 def normalize_searching_criteria(keys)
1338 keys.collect! do |i|
1339 case i
1340 when -1, Range, Array
1341 MessageSet.new(i)
1342 else
1343 i
1344 end
1345 end
1346 end
1347
1348 def create_ssl_params(certs = nil, verify = true)
1349 params = {}
1350 if certs
1351 if File.file?(certs)
1352 params[:ca_file] = certs
1353 elsif File.directory?(certs)
1354 params[:ca_path] = certs
1355 end
1356 end
1357 if verify
1358 params[:verify_mode] = VERIFY_PEER
1359 else
1360 params[:verify_mode] = VERIFY_NONE
1361 end
1362 return params
1363 end
1364
1365 def start_tls_session(params = {})
1366 unless defined?(OpenSSL)
1367 raise "SSL extension not installed"
1368 end
1369 if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
1370 raise RuntimeError, "already using SSL"
1371 end
1372 begin
1373 params = params.to_hash
1374 rescue NoMethodError
1375 params = {}
1376 end
1377 context = SSLContext.new
1378 context.set_params(params)
1379 if defined?(VerifyCallbackProc)
1380 context.verify_callback = VerifyCallbackProc
1381 end
1382 @sock = SSLSocket.new(@sock, context)
1383 @sock.sync_close = true
1384 @sock.connect
1385 if context.verify_mode != VERIFY_NONE
1386 @sock.post_connection_check(@host)
1387 end
1388 end
1389
1390 class RawData # :nodoc:
1391 def send_data(imap)
1392 imap.send(:put_string, @data)
1393 end
1394
1395 def validate
1396 end
1397
1398 private
1399
1400 def initialize(data)
1401 @data = data
1402 end
1403 end
1404
1405 class Atom # :nodoc:
1406 def send_data(imap)
1407 imap.send(:put_string, @data)
1408 end
1409
1410 def validate
1411 end
1412
1413 private
1414
1415 def initialize(data)
1416 @data = data
1417 end
1418 end
1419
1420 class QuotedString # :nodoc:
1421 def send_data(imap)
1422 imap.send(:send_quoted_string, @data)
1423 end
1424
1425 def validate
1426 end
1427
1428 private
1429
1430 def initialize(data)
1431 @data = data
1432 end
1433 end
1434
1435 class Literal # :nodoc:
1436 def send_data(imap)
1437 imap.send(:send_literal, @data)
1438 end
1439
1440 def validate
1441 end
1442
1443 private
1444
1445 def initialize(data)
1446 @data = data
1447 end
1448 end
1449
1450 class MessageSet # :nodoc:
1451 def send_data(imap)
1452 imap.send(:put_string, format_internal(@data))
1453 end
1454
1455 def validate
1456 validate_internal(@data)
1457 end
1458
1459 private
1460
1461 def initialize(data)
1462 @data = data
1463 end
1464
1465 def format_internal(data)
1466 case data
1467 when "*"
1468 return data
1469 when Integer
1470 if data == -1
1471 return "*"
1472 else
1473 return data.to_s
1474 end
1475 when Range
1476 return format_internal(data.first) +
1477 ":" + format_internal(data.last)
1478 when Array
1479 return data.collect {|i| format_internal(i)}.join(",")
1480 when ThreadMember
1481 return data.seqno.to_s +
1482 ":" + data.children.collect {|i| format_internal(i).join(",")}
1483 end
1484 end
1485
1486 def validate_internal(data)
1487 case data
1488 when "*"
1489 when Integer
1490 ensure_nz_number(data)
1491 when Range
1492 when Array
1493 data.each do |i|
1494 validate_internal(i)
1495 end
1496 when ThreadMember
1497 data.children.each do |i|
1498 validate_internal(i)
1499 end
1500 else
1501 raise DataFormatError, data.inspect
1502 end
1503 end
1504
1505 def ensure_nz_number(num)
1506 if num < -1 || num == 0 || num >= 4294967296
1507 msg = "nz_number must be non-zero unsigned 32-bit integer: " +
1508 num.inspect
1509 raise DataFormatError, msg
1510 end
1511 end
1512 end
1513
1514 # Net::IMAP::ContinuationRequest represents command continuation requests.
1515 #
1516 # The command continuation request response is indicated by a "+" token
1517 # instead of a tag. This form of response indicates that the server is
1518 # ready to accept the continuation of a command from the client. The
1519 # remainder of this response is a line of text.
1520 #
1521 # continue_req ::= "+" SPACE (resp_text / base64)
1522 #
1523 # ==== Fields:
1524 #
1525 # data:: Returns the data (Net::IMAP::ResponseText).
1526 #
1527 # raw_data:: Returns the raw data string.
1528 ContinuationRequest = Struct.new(:data, :raw_data)
1529
1530 # Net::IMAP::UntaggedResponse represents untagged responses.
1531 #
1532 # Data transmitted by the server to the client and status responses
1533 # that do not indicate command completion are prefixed with the token
1534 # "*", and are called untagged responses.
1535 #
1536 # response_data ::= "*" SPACE (resp_cond_state / resp_cond_bye /
1537 # mailbox_data / message_data / capability_data)
1538 #
1539 # ==== Fields:
1540 #
1541 # name:: Returns the name such as "FLAGS", "LIST", "FETCH"....
1542 #
1543 # data:: Returns the data such as an array of flag symbols,
1544 # a ((<Net::IMAP::MailboxList>)) object....
1545 #
1546 # raw_data:: Returns the raw data string.
1547 UntaggedResponse = Struct.new(:name, :data, :raw_data)
1548
1549 # Net::IMAP::TaggedResponse represents tagged responses.
1550 #
1551 # The server completion result response indicates the success or
1552 # failure of the operation. It is tagged with the same tag as the
1553 # client command which began the operation.
1554 #
1555 # response_tagged ::= tag SPACE resp_cond_state CRLF
1556 #
1557 # tag ::= 1*<any ATOM_CHAR except "+">
1558 #
1559 # resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text
1560 #
1561 # ==== Fields:
1562 #
1563 # tag:: Returns the tag.
1564 #
1565 # name:: Returns the name. the name is one of "OK", "NO", "BAD".
1566 #
1567 # data:: Returns the data. See ((<Net::IMAP::ResponseText>)).
1568 #
1569 # raw_data:: Returns the raw data string.
1570 #
1571 TaggedResponse = Struct.new(:tag, :name, :data, :raw_data)
1572
1573 # Net::IMAP::ResponseText represents texts of responses.
1574 # The text may be prefixed by the response code.
1575 #
1576 # resp_text ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text)
1577 # ;; text SHOULD NOT begin with "[" or "="
1578 #
1579 # ==== Fields:
1580 #
1581 # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)).
1582 #
1583 # text:: Returns the text.
1584 #
1585 ResponseText = Struct.new(:code, :text)
1586
1587 #
1588 # Net::IMAP::ResponseCode represents response codes.
1589 #
1590 # resp_text_code ::= "ALERT" / "PARSE" /
1591 # "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" /
1592 # "READ-ONLY" / "READ-WRITE" / "TRYCREATE" /
1593 # "UIDVALIDITY" SPACE nz_number /
1594 # "UNSEEN" SPACE nz_number /
1595 # atom [SPACE 1*<any TEXT_CHAR except "]">]
1596 #
1597 # ==== Fields:
1598 #
1599 # name:: Returns the name such as "ALERT", "PERMANENTFLAGS", "UIDVALIDITY"....
1600 #
1601 # data:: Returns the data if it exists.
1602 #
1603 ResponseCode = Struct.new(:name, :data)
1604
1605 # Net::IMAP::MailboxList represents contents of the LIST response.
1606 #
1607 # mailbox_list ::= "(" #("\Marked" / "\Noinferiors" /
1608 # "\Noselect" / "\Unmarked" / flag_extension) ")"
1609 # SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox
1610 #
1611 # ==== Fields:
1612 #
1613 # attr:: Returns the name attributes. Each name attribute is a symbol
1614 # capitalized by String#capitalize, such as :Noselect (not :NoSelect).
1615 #
1616 # delim:: Returns the hierarchy delimiter
1617 #
1618 # name:: Returns the mailbox name.
1619 #
1620 MailboxList = Struct.new(:attr, :delim, :name)
1621
1622 # Net::IMAP::MailboxQuota represents contents of GETQUOTA response.
1623 # This object can also be a response to GETQUOTAROOT. In the syntax
1624 # specification below, the delimiter used with the "#" construct is a
1625 # single space (SPACE).
1626 #
1627 # quota_list ::= "(" #quota_resource ")"
1628 #
1629 # quota_resource ::= atom SPACE number SPACE number
1630 #
1631 # quota_response ::= "QUOTA" SPACE astring SPACE quota_list
1632 #
1633 # ==== Fields:
1634 #
1635 # mailbox:: The mailbox with the associated quota.
1636 #
1637 # usage:: Current storage usage of mailbox.
1638 #
1639 # quota:: Quota limit imposed on mailbox.
1640 #
1641 MailboxQuota = Struct.new(:mailbox, :usage, :quota)
1642
1643 # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT
1644 # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.)
1645 #
1646 # quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring)
1647 #
1648 # ==== Fields:
1649 #
1650 # mailbox:: The mailbox with the associated quota.
1651 #
1652 # quotaroots:: Zero or more quotaroots that effect the quota on the
1653 # specified mailbox.
1654 #
1655 MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots)
1656
1657 # Net::IMAP::MailboxACLItem represents response from GETACL.
1658 #
1659 # acl_data ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights)
1660 #
1661 # identifier ::= astring
1662 #
1663 # rights ::= astring
1664 #
1665 # ==== Fields:
1666 #
1667 # user:: Login name that has certain rights to the mailbox
1668 # that was specified with the getacl command.
1669 #
1670 # rights:: The access rights the indicated user has to the
1671 # mailbox.
1672 #
1673 MailboxACLItem = Struct.new(:user, :rights)
1674
1675 # Net::IMAP::StatusData represents contents of the STATUS response.
1676 #
1677 # ==== Fields:
1678 #
1679 # mailbox:: Returns the mailbox name.
1680 #
1681 # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT",
1682 # "UIDVALIDITY", "UNSEEN". Each value is a number.
1683 #
1684 StatusData = Struct.new(:mailbox, :attr)
1685
1686 # Net::IMAP::FetchData represents contents of the FETCH response.
1687 #
1688 # ==== Fields:
1689 #
1690 # seqno:: Returns the message sequence number.
1691 # (Note: not the unique identifier, even for the UID command response.)
1692 #
1693 # attr:: Returns a hash. Each key is a data item name, and each value is
1694 # its value.
1695 #
1696 # The current data items are:
1697 #
1698 # [BODY]
1699 # A form of BODYSTRUCTURE without extension data.
1700 # [BODY[<section>]<<origin_octet>>]
1701 # A string expressing the body contents of the specified section.
1702 # [BODYSTRUCTURE]
1703 # An object that describes the [MIME-IMB] body structure of a message.
1704 # See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText,
1705 # Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart.
1706 # [ENVELOPE]
1707 # A Net::IMAP::Envelope object that describes the envelope
1708 # structure of a message.
1709 # [FLAGS]
1710 # A array of flag symbols that are set for this message. flag symbols
1711 # are capitalized by String#capitalize.
1712 # [INTERNALDATE]
1713 # A string representing the internal date of the message.
1714 # [RFC822]
1715 # Equivalent to BODY[].
1716 # [RFC822.HEADER]
1717 # Equivalent to BODY.PEEK[HEADER].
1718 # [RFC822.SIZE]
1719 # A number expressing the [RFC-822] size of the message.
1720 # [RFC822.TEXT]
1721 # Equivalent to BODY[TEXT].
1722 # [UID]
1723 # A number expressing the unique identifier of the message.
1724 #
1725 FetchData = Struct.new(:seqno, :attr)
1726
1727 # Net::IMAP::Envelope represents envelope structures of messages.
1728 #
1729 # ==== Fields:
1730 #
1731 # date:: Returns a string that represents the date.
1732 #
1733 # subject:: Returns a string that represents the subject.
1734 #
1735 # from:: Returns an array of Net::IMAP::Address that represents the from.
1736 #
1737 # sender:: Returns an array of Net::IMAP::Address that represents the sender.
1738 #
1739 # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to.
1740 #
1741 # to:: Returns an array of Net::IMAP::Address that represents the to.
1742 #
1743 # cc:: Returns an array of Net::IMAP::Address that represents the cc.
1744 #
1745 # bcc:: Returns an array of Net::IMAP::Address that represents the bcc.
1746 #
1747 # in_reply_to:: Returns a string that represents the in-reply-to.
1748 #
1749 # message_id:: Returns a string that represents the message-id.
1750 #
1751 Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to,
1752 :to, :cc, :bcc, :in_reply_to, :message_id)
1753
1754 #
1755 # Net::IMAP::Address represents electronic mail addresses.
1756 #
1757 # ==== Fields:
1758 #
1759 # name:: Returns the phrase from [RFC-822] mailbox.
1760 #
1761 # route:: Returns the route from [RFC-822] route-addr.
1762 #
1763 # mailbox:: nil indicates end of [RFC-822] group.
1764 # If non-nil and host is nil, returns [RFC-822] group name.
1765 # Otherwise, returns [RFC-822] local-part
1766 #
1767 # host:: nil indicates [RFC-822] group syntax.
1768 # Otherwise, returns [RFC-822] domain name.
1769 #
1770 Address = Struct.new(:name, :route, :mailbox, :host)
1771
1772 #
1773 # Net::IMAP::ContentDisposition represents Content-Disposition fields.
1774 #
1775 # ==== Fields:
1776 #
1777 # dsp_type:: Returns the disposition type.
1778 #
1779 # param:: Returns a hash that represents parameters of the Content-Disposition
1780 # field.
1781 #
1782 ContentDisposition = Struct.new(:dsp_type, :param)
1783
1784 # Net::IMAP::ThreadMember represents a thread-node returned
1785 # by Net::IMAP#thread
1786 #
1787 # ==== Fields:
1788 #
1789 # seqno:: The sequence number of this message.
1790 #
1791 # children:: an array of Net::IMAP::ThreadMember objects for mail
1792 # items that are children of this in the thread.
1793 #
1794 ThreadMember = Struct.new(:seqno, :children)
1795
1796 # Net::IMAP::BodyTypeBasic represents basic body structures of messages.
1797 #
1798 # ==== Fields:
1799 #
1800 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1801 #
1802 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1803 #
1804 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1805 #
1806 # content_id:: Returns a string giving the content id as defined in [MIME-IMB].
1807 #
1808 # description:: Returns a string giving the content description as defined in
1809 # [MIME-IMB].
1810 #
1811 # encoding:: Returns a string giving the content transfer encoding as defined in
1812 # [MIME-IMB].
1813 #
1814 # size:: Returns a number giving the size of the body in octets.
1815 #
1816 # md5:: Returns a string giving the body MD5 value as defined in [MD5].
1817 #
1818 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1819 # the content disposition.
1820 #
1821 # language:: Returns a string or an array of strings giving the body
1822 # language value as defined in [LANGUAGE-TAGS].
1823 #
1824 # extension:: Returns extension data.
1825 #
1826 # multipart?:: Returns false.
1827 #
1828 class BodyTypeBasic < Struct.new(:media_type, :subtype,
1829 :param, :content_id,
1830 :description, :encoding, :size,
1831 :md5, :disposition, :language,
1832 :extension)
1833 def multipart?
1834 return false
1835 end
1836
1837 # Obsolete: use +subtype+ instead. Calling this will
1838 # generate a warning message to +stderr+, then return
1839 # the value of +subtype+.
1840 def media_subtype
1841 $stderr.printf("warning: media_subtype is obsolete.\n")
1842 $stderr.printf(" use subtype instead.\n")
1843 return subtype
1844 end
1845 end
1846
1847 # Net::IMAP::BodyTypeText represents TEXT body structures of messages.
1848 #
1849 # ==== Fields:
1850 #
1851 # lines:: Returns the size of the body in text lines.
1852 #
1853 # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic.
1854 #
1855 class BodyTypeText < Struct.new(:media_type, :subtype,
1856 :param, :content_id,
1857 :description, :encoding, :size,
1858 :lines,
1859 :md5, :disposition, :language,
1860 :extension)
1861 def multipart?
1862 return false
1863 end
1864
1865 # Obsolete: use +subtype+ instead. Calling this will
1866 # generate a warning message to +stderr+, then return
1867 # the value of +subtype+.
1868 def media_subtype
1869 $stderr.printf("warning: media_subtype is obsolete.\n")
1870 $stderr.printf(" use subtype instead.\n")
1871 return subtype
1872 end
1873 end
1874
1875 # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages.
1876 #
1877 # ==== Fields:
1878 #
1879 # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure.
1880 #
1881 # body:: Returns an object giving the body structure.
1882 #
1883 # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText.
1884 #
1885 class BodyTypeMessage < Struct.new(:media_type, :subtype,
1886 :param, :content_id,
1887 :description, :encoding, :size,
1888 :envelope, :body, :lines,
1889 :md5, :disposition, :language,
1890 :extension)
1891 def multipart?
1892 return false
1893 end
1894
1895 # Obsolete: use +subtype+ instead. Calling this will
1896 # generate a warning message to +stderr+, then return
1897 # the value of +subtype+.
1898 def media_subtype
1899 $stderr.printf("warning: media_subtype is obsolete.\n")
1900 $stderr.printf(" use subtype instead.\n")
1901 return subtype
1902 end
1903 end
1904
1905 # Net::IMAP::BodyTypeMultipart represents multipart body structures
1906 # of messages.
1907 #
1908 # ==== Fields:
1909 #
1910 # media_type:: Returns the content media type name as defined in [MIME-IMB].
1911 #
1912 # subtype:: Returns the content subtype name as defined in [MIME-IMB].
1913 #
1914 # parts:: Returns multiple parts.
1915 #
1916 # param:: Returns a hash that represents parameters as defined in [MIME-IMB].
1917 #
1918 # disposition:: Returns a Net::IMAP::ContentDisposition object giving
1919 # the content disposition.
1920 #
1921 # language:: Returns a string or an array of strings giving the body
1922 # language value as defined in [LANGUAGE-TAGS].
1923 #
1924 # extension:: Returns extension data.
1925 #
1926 # multipart?:: Returns true.
1927 #
1928 class BodyTypeMultipart < Struct.new(:media_type, :subtype,
1929 :parts,
1930 :param, :disposition, :language,
1931 :extension)
1932 def multipart?
1933 return true
1934 end
1935
1936 # Obsolete: use +subtype+ instead. Calling this will
1937 # generate a warning message to +stderr+, then return
1938 # the value of +subtype+.
1939 def media_subtype
1940 $stderr.printf("warning: media_subtype is obsolete.\n")
1941 $stderr.printf(" use subtype instead.\n")
1942 return subtype
1943 end
1944 end
1945
1946 class ResponseParser # :nodoc:
1947 def initialize
1948 @str = nil
1949 @pos = nil
1950 @lex_state = nil
1951 @token = nil
1952 @flag_symbols = {}
1953 end
1954
1955 def parse(str)
1956 @str = str
1957 @pos = 0
1958 @lex_state = EXPR_BEG
1959 @token = nil
1960 return response
1961 end
1962
1963 private
1964
1965 EXPR_BEG = :EXPR_BEG
1966 EXPR_DATA = :EXPR_DATA
1967 EXPR_TEXT = :EXPR_TEXT
1968 EXPR_RTEXT = :EXPR_RTEXT
1969 EXPR_CTEXT = :EXPR_CTEXT
1970
1971 T_SPACE = :SPACE
1972 T_NIL = :NIL
1973 T_NUMBER = :NUMBER
1974 T_ATOM = :ATOM
1975 T_QUOTED = :QUOTED
1976 T_LPAR = :LPAR
1977 T_RPAR = :RPAR
1978 T_BSLASH = :BSLASH
1979 T_STAR = :STAR
1980 T_LBRA = :LBRA
1981 T_RBRA = :RBRA
1982 T_LITERAL = :LITERAL
1983 T_PLUS = :PLUS
1984 T_PERCENT = :PERCENT
1985 T_CRLF = :CRLF
1986 T_EOF = :EOF
1987 T_TEXT = :TEXT
1988
1989 BEG_REGEXP = /\G(?:\
1990 (?# 1: SPACE )( +)|\
1991 (?# 2: NIL )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1992 (?# 3: NUMBER )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
1993 (?# 4: ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
1994 (?# 5: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
1995 (?# 6: LPAR )(\()|\
1996 (?# 7: RPAR )(\))|\
1997 (?# 8: BSLASH )(\\)|\
1998 (?# 9: STAR )(\*)|\
1999 (?# 10: LBRA )(\[)|\
2000 (?# 11: RBRA )(\])|\
2001 (?# 12: LITERAL )\{(\d+)\}\r\n|\
2002 (?# 13: PLUS )(\+)|\
2003 (?# 14: PERCENT )(%)|\
2004 (?# 15: CRLF )(\r\n)|\
2005 (?# 16: EOF )(\z))/ni
2006
2007 DATA_REGEXP = /\G(?:\
2008 (?# 1: SPACE )( )|\
2009 (?# 2: NIL )(NIL)|\
2010 (?# 3: NUMBER )(\d+)|\
2011 (?# 4: QUOTED )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
2012 (?# 5: LITERAL )\{(\d+)\}\r\n|\
2013 (?# 6: LPAR )(\()|\
2014 (?# 7: RPAR )(\)))/ni
2015
2016 TEXT_REGEXP = /\G(?:\
2017 (?# 1: TEXT )([^\x00\r\n]*))/ni
2018
2019 RTEXT_REGEXP = /\G(?:\
2020 (?# 1: LBRA )(\[)|\
2021 (?# 2: TEXT )([^\x00\r\n]*))/ni
2022
2023 CTEXT_REGEXP = /\G(?:\
2024 (?# 1: TEXT )([^\x00\r\n\]]*))/ni
2025
2026 Token = Struct.new(:symbol, :value)
2027
2028 def response
2029 token = lookahead
2030 case token.symbol
2031 when T_PLUS
2032 result = continue_req
2033 when T_STAR
2034 result = response_untagged
2035 else
2036 result = response_tagged
2037 end
2038 match(T_CRLF)
2039 match(T_EOF)
2040 return result
2041 end
2042
2043 def continue_req
2044 match(T_PLUS)
2045 match(T_SPACE)
2046 return ContinuationRequest.new(resp_text, @str)
2047 end
2048
2049 def response_untagged
2050 match(T_STAR)
2051 match(T_SPACE)
2052 token = lookahead
2053 if token.symbol == T_NUMBER
2054 return numeric_response
2055 elsif token.symbol == T_ATOM
2056 case token.value
2057 when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni
2058 return response_cond
2059 when /\A(?:FLAGS)\z/ni
2060 return flags_response
2061 when /\A(?:LIST|LSUB)\z/ni
2062 return list_response
2063 when /\A(?:QUOTA)\z/ni
2064 return getquota_response
2065 when /\A(?:QUOTAROOT)\z/ni
2066 return getquotaroot_response
2067 when /\A(?:ACL)\z/ni
2068 return getacl_response
2069 when /\A(?:SEARCH|SORT)\z/ni
2070 return search_response
2071 when /\A(?:THREAD)\z/ni
2072 return thread_response
2073 when /\A(?:STATUS)\z/ni
2074 return status_response
2075 when /\A(?:CAPABILITY)\z/ni
2076 return capability_response
2077 else
2078 return text_response
2079 end
2080 else
2081 parse_error("unexpected token %s", token.symbol)
2082 end
2083 end
2084
2085 def response_tagged
2086 tag = atom
2087 match(T_SPACE)
2088 token = match(T_ATOM)
2089 name = token.value.upcase
2090 match(T_SPACE)
2091 return TaggedResponse.new(tag, name, resp_text, @str)
2092 end
2093
2094 def response_cond
2095 token = match(T_ATOM)
2096 name = token.value.upcase
2097 match(T_SPACE)
2098 return UntaggedResponse.new(name, resp_text, @str)
2099 end
2100
2101 def numeric_response
2102 n = number
2103 match(T_SPACE)
2104 token = match(T_ATOM)
2105 name = token.value.upcase
2106 case name
2107 when "EXISTS", "RECENT", "EXPUNGE"
2108 return UntaggedResponse.new(name, n, @str)
2109 when "FETCH"
2110 shift_token
2111 match(T_SPACE)
2112 data = FetchData.new(n, msg_att)
2113 return UntaggedResponse.new(name, data, @str)
2114 end
2115 end
2116
2117 def msg_att
2118 match(T_LPAR)
2119 attr = {}
2120 while true
2121 token = lookahead
2122 case token.symbol
2123 when T_RPAR
2124 shift_token
2125 break
2126 when T_SPACE
2127 shift_token
2128 token = lookahead
2129 end
2130 case token.value
2131 when /\A(?:ENVELOPE)\z/ni
2132 name, val = envelope_data
2133 when /\A(?:FLAGS)\z/ni
2134 name, val = flags_data
2135 when /\A(?:INTERNALDATE)\z/ni
2136 name, val = internaldate_data
2137 when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
2138 name, val = rfc822_text
2139 when /\A(?:RFC822\.SIZE)\z/ni
2140 name, val = rfc822_size
2141 when /\A(?:BODY(?:STRUCTURE)?)\z/ni
2142 name, val = body_data
2143 when /\A(?:UID)\z/ni
2144 name, val = uid_data
2145 else
2146 parse_error("unknown attribute `%s'", token.value)
2147 end
2148 attr[name] = val
2149 end
2150 return attr
2151 end
2152
2153 def envelope_data
2154 token = match(T_ATOM)
2155 name = token.value.upcase
2156 match(T_SPACE)
2157 return name, envelope
2158 end
2159
2160 def envelope
2161 @lex_state = EXPR_DATA
2162 token = lookahead
2163 if token.symbol == T_NIL
2164 shift_token
2165 result = nil
2166 else
2167 match(T_LPAR)
2168 date = nstring
2169 match(T_SPACE)
2170 subject = nstring
2171 match(T_SPACE)
2172 from = address_list
2173 match(T_SPACE)
2174 sender = address_list
2175 match(T_SPACE)
2176 reply_to = address_list
2177 match(T_SPACE)
2178 to = address_list
2179 match(T_SPACE)
2180 cc = address_list
2181 match(T_SPACE)
2182 bcc = address_list
2183 match(T_SPACE)
2184 in_reply_to = nstring
2185 match(T_SPACE)
2186 message_id = nstring
2187 match(T_RPAR)
2188 result = Envelope.new(date, subject, from, sender, reply_to,
2189 to, cc, bcc, in_reply_to, message_id)
2190 end
2191 @lex_state = EXPR_BEG
2192 return result
2193 end
2194
2195 def flags_data
2196 token = match(T_ATOM)
2197 name = token.value.upcase
2198 match(T_SPACE)
2199 return name, flag_list
2200 end
2201
2202 def internaldate_data
2203 token = match(T_ATOM)
2204 name = token.value.upcase
2205 match(T_SPACE)
2206 token = match(T_QUOTED)
2207 return name, token.value
2208 end
2209
2210 def rfc822_text
2211 token = match(T_ATOM)
2212 name = token.value.upcase
2213 match(T_SPACE)
2214 return name, nstring
2215 end
2216
2217 def rfc822_size
2218 token = match(T_ATOM)
2219 name = token.value.upcase
2220 match(T_SPACE)
2221 return name, number
2222 end
2223
2224 def body_data
2225 token = match(T_ATOM)
2226 name = token.value.upcase
2227 token = lookahead
2228 if token.symbol == T_SPACE
2229 shift_token
2230 return name, body
2231 end
2232 name.concat(section)
2233 token = lookahead
2234 if token.symbol == T_ATOM
2235 name.concat(token.value)
2236 shift_token
2237 end
2238 match(T_SPACE)
2239 data = nstring
2240 return name, data
2241 end
2242
2243 def body
2244 @lex_state = EXPR_DATA
2245 token = lookahead
2246 if token.symbol == T_NIL
2247 shift_token
2248 result = nil
2249 else
2250 match(T_LPAR)
2251 token = lookahead
2252 if token.symbol == T_LPAR
2253 result = body_type_mpart
2254 else
2255 result = body_type_1part
2256 end
2257 match(T_RPAR)
2258 end
2259 @lex_state = EXPR_BEG
2260 return result
2261 end
2262
2263 def body_type_1part
2264 token = lookahead
2265 case token.value
2266 when /\A(?:TEXT)\z/ni
2267 return body_type_text
2268 when /\A(?:MESSAGE)\z/ni
2269 return body_type_msg
2270 else
2271 return body_type_basic
2272 end
2273 end
2274
2275 def body_type_basic
2276 mtype, msubtype = media_type
2277 token = lookahead
2278 if token.symbol == T_RPAR
2279 return BodyTypeBasic.new(mtype, msubtype)
2280 end
2281 match(T_SPACE)
2282 param, content_id, desc, enc, size = body_fields
2283 md5, disposition, language, extension = body_ext_1part
2284 return BodyTypeBasic.new(mtype, msubtype,
2285 param, content_id,
2286 desc, enc, size,
2287 md5, disposition, language, extension)
2288 end
2289
2290 def body_type_text
2291 mtype, msubtype = media_type
2292 match(T_SPACE)
2293 param, content_id, desc, enc, size = body_fields
2294 match(T_SPACE)
2295 lines = number
2296 md5, disposition, language, extension = body_ext_1part
2297 return BodyTypeText.new(mtype, msubtype,
2298 param, content_id,
2299 desc, enc, size,
2300 lines,
2301 md5, disposition, language, extension)
2302 end
2303
2304 def body_type_msg
2305 mtype, msubtype = media_type
2306 match(T_SPACE)
2307 param, content_id, desc, enc, size = body_fields
2308 match(T_SPACE)
2309 env = envelope
2310 match(T_SPACE)
2311 b = body
2312 match(T_SPACE)
2313 lines = number
2314 md5, disposition, language, extension = body_ext_1part
2315 return BodyTypeMessage.new(mtype, msubtype,
2316 param, content_id,
2317 desc, enc, size,
2318 env, b, lines,
2319 md5, disposition, language, extension)
2320 end
2321
2322 def body_type_mpart
2323 parts = []
2324 while true
2325 token = lookahead
2326 if token.symbol == T_SPACE
2327 shift_token
2328 break
2329 end
2330 parts.push(body)
2331 end
2332 mtype = "MULTIPART"
2333 msubtype = case_insensitive_string
2334 param, disposition, language, extension = body_ext_mpart
2335 return BodyTypeMultipart.new(mtype, msubtype, parts,
2336 param, disposition, language,
2337 extension)
2338 end
2339
2340 def media_type
2341 mtype = case_insensitive_string
2342 match(T_SPACE)
2343 msubtype = case_insensitive_string
2344 return mtype, msubtype
2345 end
2346
2347 def body_fields
2348 param = body_fld_param
2349 match(T_SPACE)
2350 content_id = nstring
2351 match(T_SPACE)
2352 desc = nstring
2353 match(T_SPACE)
2354 enc = case_insensitive_string
2355 match(T_SPACE)
2356 size = number
2357 return param, content_id, desc, enc, size
2358 end
2359
2360 def body_fld_param
2361 token = lookahead
2362 if token.symbol == T_NIL
2363 shift_token
2364 return nil
2365 end
2366 match(T_LPAR)
2367 param = {}
2368 while true
2369 token = lookahead
2370 case token.symbol
2371 when T_RPAR
2372 shift_token
2373 break
2374 when T_SPACE
2375 shift_token
2376 end
2377 name = case_insensitive_string
2378 match(T_SPACE)
2379 val = string
2380 param[name] = val
2381 end
2382 return param
2383 end
2384
2385 def body_ext_1part
2386 token = lookahead
2387 if token.symbol == T_SPACE
2388 shift_token
2389 else
2390 return nil
2391 end
2392 md5 = nstring
2393
2394 token = lookahead
2395 if token.symbol == T_SPACE
2396 shift_token
2397 else
2398 return md5
2399 end
2400 disposition = body_fld_dsp
2401
2402 token = lookahead
2403 if token.symbol == T_SPACE
2404 shift_token
2405 else
2406 return md5, disposition
2407 end
2408 language = body_fld_lang
2409
2410 token = lookahead
2411 if token.symbol == T_SPACE
2412 shift_token
2413 else
2414 return md5, disposition, language
2415 end
2416
2417 extension = body_extensions
2418 return md5, disposition, language, extension
2419 end
2420
2421 def body_ext_mpart
2422 token = lookahead
2423 if token.symbol == T_SPACE
2424 shift_token
2425 else
2426 return nil
2427 end
2428 param = body_fld_param
2429
2430 token = lookahead
2431 if token.symbol == T_SPACE
2432 shift_token
2433 else
2434 return param
2435 end
2436 disposition = body_fld_dsp
2437 match(T_SPACE)
2438 language = body_fld_lang
2439
2440 token = lookahead
2441 if token.symbol == T_SPACE
2442 shift_token
2443 else
2444 return param, disposition, language
2445 end
2446
2447 extension = body_extensions
2448 return param, disposition, language, extension
2449 end
2450
2451 def body_fld_dsp
2452 token = lookahead
2453 if token.symbol == T_NIL
2454 shift_token
2455 return nil
2456 end
2457 match(T_LPAR)
2458 dsp_type = case_insensitive_string
2459 match(T_SPACE)
2460 param = body_fld_param
2461 match(T_RPAR)
2462 return ContentDisposition.new(dsp_type, param)
2463 end
2464
2465 def body_fld_lang
2466 token = lookahead
2467 if token.symbol == T_LPAR
2468 shift_token
2469 result = []
2470 while true
2471 token = lookahead
2472 case token.symbol
2473 when T_RPAR
2474 shift_token
2475 return result
2476 when T_SPACE
2477 shift_token
2478 end
2479 result.push(case_insensitive_string)
2480 end
2481 else
2482 lang = nstring
2483 if lang
2484 return lang.upcase
2485 else
2486 return lang
2487 end
2488 end
2489 end
2490
2491 def body_extensions
2492 result = []
2493 while true
2494 token = lookahead
2495 case token.symbol
2496 when T_RPAR
2497 return result
2498 when T_SPACE
2499 shift_token
2500 end
2501 result.push(body_extension)
2502 end
2503 end
2504
2505 def body_extension
2506 token = lookahead
2507 case token.symbol
2508 when T_LPAR
2509 shift_token
2510 result = body_extensions
2511 match(T_RPAR)
2512 return result
2513 when T_NUMBER
2514 return number
2515 else
2516 return nstring
2517 end
2518 end
2519
2520 def section
2521 str = ""
2522 token = match(T_LBRA)
2523 str.concat(token.value)
2524 token = match(T_ATOM, T_NUMBER, T_RBRA)
2525 if token.symbol == T_RBRA
2526 str.concat(token.value)
2527 return str
2528 end
2529 str.concat(token.value)
2530 token = lookahead
2531 if token.symbol == T_SPACE
2532 shift_token
2533 str.concat(token.value)
2534 token = match(T_LPAR)
2535 str.concat(token.value)
2536 while true
2537 token = lookahead
2538 case token.symbol
2539 when T_RPAR
2540 str.concat(token.value)
2541 shift_token
2542 break
2543 when T_SPACE
2544 shift_token
2545 str.concat(token.value)
2546 end
2547 str.concat(format_string(astring))
2548 end
2549 end
2550 token = match(T_RBRA)
2551 str.concat(token.value)
2552 return str
2553 end
2554
2555 def format_string(str)
2556 case str
2557 when ""
2558 return '""'
2559 when /[\x80-\xff\r\n]/n
2560 # literal
2561 return "{" + str.length.to_s + "}" + CRLF + str
2562 when /[(){ \x00-\x1f\x7f%*"\\]/n
2563 # quoted string
2564 return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"'
2565 else
2566 # atom
2567 return str
2568 end
2569 end
2570
2571 def uid_data
2572 token = match(T_ATOM)
2573 name = token.value.upcase
2574 match(T_SPACE)
2575 return name, number
2576 end
2577
2578 def text_response
2579 token = match(T_ATOM)
2580 name = token.value.upcase
2581 match(T_SPACE)
2582 @lex_state = EXPR_TEXT
2583 token = match(T_TEXT)
2584 @lex_state = EXPR_BEG
2585 return UntaggedResponse.new(name, token.value)
2586 end
2587
2588 def flags_response
2589 token = match(T_ATOM)
2590 name = token.value.upcase
2591 match(T_SPACE)
2592 return UntaggedResponse.new(name, flag_list, @str)
2593 end
2594
2595 def list_response
2596 token = match(T_ATOM)
2597 name = token.value.upcase
2598 match(T_SPACE)
2599 return UntaggedResponse.new(name, mailbox_list, @str)
2600 end
2601
2602 def mailbox_list
2603 attr = flag_list
2604 match(T_SPACE)
2605 token = match(T_QUOTED, T_NIL)
2606 if token.symbol == T_NIL
2607 delim = nil
2608 else
2609 delim = token.value
2610 end
2611 match(T_SPACE)
2612 name = astring
2613 return MailboxList.new(attr, delim, name)
2614 end
2615
2616 def getquota_response
2617 # If quota never established, get back
2618 # `NO Quota root does not exist'.
2619 # If quota removed, get `()' after the
2620 # folder spec with no mention of `STORAGE'.
2621 token = match(T_ATOM)
2622 name = token.value.upcase
2623 match(T_SPACE)
2624 mailbox = astring
2625 match(T_SPACE)
2626 match(T_LPAR)
2627 token = lookahead
2628 case token.symbol
2629 when T_RPAR
2630 shift_token
2631 data = MailboxQuota.new(mailbox, nil, nil)
2632 return UntaggedResponse.new(name, data, @str)
2633 when T_ATOM
2634 shift_token
2635 match(T_SPACE)
2636 token = match(T_NUMBER)
2637 usage = token.value
2638 match(T_SPACE)
2639 token = match(T_NUMBER)
2640 quota = token.value
2641 match(T_RPAR)
2642 data = MailboxQuota.new(mailbox, usage, quota)
2643 return UntaggedResponse.new(name, data, @str)
2644 else
2645 parse_error("unexpected token %s", token.symbol)
2646 end
2647 end
2648
2649 def getquotaroot_response
2650 # Similar to getquota, but only admin can use getquota.
2651 token = match(T_ATOM)
2652 name = token.value.upcase
2653 match(T_SPACE)
2654 mailbox = astring
2655 quotaroots = []
2656 while true
2657 token = lookahead
2658 break unless token.symbol == T_SPACE
2659 shift_token
2660 quotaroots.push(astring)
2661 end
2662 data = MailboxQuotaRoot.new(mailbox, quotaroots)
2663 return UntaggedResponse.new(name, data, @str)
2664 end
2665
2666 def getacl_response
2667 token = match(T_ATOM)
2668 name = token.value.upcase
2669 match(T_SPACE)
2670 mailbox = astring
2671 data = []
2672 token = lookahead
2673 if token.symbol == T_SPACE
2674 shift_token
2675 while true
2676 token = lookahead
2677 case token.symbol
2678 when T_CRLF
2679 break
2680 when T_SPACE
2681 shift_token
2682 end
2683 user = astring
2684 match(T_SPACE)
2685 rights = astring
2686 ##XXX data.push([user, rights])
2687 data.push(MailboxACLItem.new(user, rights))
2688 end
2689 end
2690 return UntaggedResponse.new(name, data, @str)
2691 end
2692
2693 def search_response
2694 token = match(T_ATOM)
2695 name = token.value.upcase
2696 token = lookahead
2697 if token.symbol == T_SPACE
2698 shift_token
2699 data = []
2700 while true
2701 token = lookahead
2702 case token.symbol
2703 when T_CRLF
2704 break
2705 when T_SPACE
2706 shift_token
2707 end
2708 data.push(number)
2709 end
2710 else
2711 data = []
2712 end
2713 return UntaggedResponse.new(name, data, @str)
2714 end
2715
2716 def thread_response
2717 token = match(T_ATOM)
2718 name = token.value.upcase
2719 token = lookahead
2720
2721 if token.symbol == T_SPACE
2722 threads = []
2723
2724 while true
2725 shift_token
2726 token = lookahead
2727
2728 case token.symbol
2729 when T_LPAR
2730 threads << thread_branch(token)
2731 when T_CRLF
2732 break
2733 end
2734 end
2735 else
2736 # no member
2737 threads = []
2738 end
2739
2740 return UntaggedResponse.new(name, threads, @str)
2741 end
2742
2743 def thread_branch(token)
2744 rootmember = nil
2745 lastmember = nil
2746
2747 while true
2748 shift_token # ignore first T_LPAR
2749 token = lookahead
2750
2751 case token.symbol
2752 when T_NUMBER
2753 # new member
2754 newmember = ThreadMember.new(number, [])
2755 if rootmember.nil?
2756 rootmember = newmember
2757 else
2758 lastmember.children << newmember
2759 end
2760 lastmember = newmember
2761 when T_SPACE
2762 # do nothing
2763 when T_LPAR
2764 if rootmember.nil?
2765 # dummy member
2766 lastmember = rootmember = ThreadMember.new(nil, [])
2767 end
2768
2769 lastmember.children << thread_branch(token)
2770 when T_RPAR
2771 break
2772 end
2773 end
2774
2775 return rootmember
2776 end
2777
2778 def status_response
2779 token = match(T_ATOM)
2780 name = token.value.upcase
2781 match(T_SPACE)
2782 mailbox = astring
2783 match(T_SPACE)
2784 match(T_LPAR)
2785 attr = {}
2786 while true
2787 token = lookahead
2788 case token.symbol
2789 when T_RPAR
2790 shift_token
2791 break
2792 when T_SPACE
2793 shift_token
2794 end
2795 token = match(T_ATOM)
2796 key = token.value.upcase
2797 match(T_SPACE)
2798 val = number
2799 attr[key] = val
2800 end
2801 data = StatusData.new(mailbox, attr)
2802 return UntaggedResponse.new(name, data, @str)
2803 end
2804
2805 def capability_response
2806 token = match(T_ATOM)
2807 name = token.value.upcase
2808 match(T_SPACE)
2809 data = []
2810 while true
2811 token = lookahead
2812 case token.symbol
2813 when T_CRLF
2814 break
2815 when T_SPACE
2816 shift_token
2817 end
2818 data.push(atom.upcase)
2819 end
2820 return UntaggedResponse.new(name, data, @str)
2821 end
2822
2823 def resp_text
2824 @lex_state = EXPR_RTEXT
2825 token = lookahead
2826 if token.symbol == T_LBRA
2827 code = resp_text_code
2828 else
2829 code = nil
2830 end
2831 token = match(T_TEXT)
2832 @lex_state = EXPR_BEG
2833 return ResponseText.new(code, token.value)
2834 end
2835
2836 def resp_text_code
2837 @lex_state = EXPR_BEG
2838 match(T_LBRA)
2839 token = match(T_ATOM)
2840 name = token.value.upcase
2841 case name
2842 when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n
2843 result = ResponseCode.new(name, nil)
2844 when /\A(?:PERMANENTFLAGS)\z/n
2845 match(T_SPACE)
2846 result = ResponseCode.new(name, flag_list)
2847 when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n
2848 match(T_SPACE)
2849 result = ResponseCode.new(name, number)
2850 else
2851 token = lookahead
2852 if token.symbol == T_SPACE
2853 shift_token
2854 @lex_state = EXPR_CTEXT
2855 token = match(T_TEXT)
2856 @lex_state = EXPR_BEG
2857 result = ResponseCode.new(name, token.value)
2858 else
2859 result = ResponseCode.new(name, nil)
2860 end
2861 end
2862 match(T_RBRA)
2863 @lex_state = EXPR_RTEXT
2864 return result
2865 end
2866
2867 def address_list
2868 token = lookahead
2869 if token.symbol == T_NIL
2870 shift_token
2871 return nil
2872 else
2873 result = []
2874 match(T_LPAR)
2875 while true
2876 token = lookahead
2877 case token.symbol
2878 when T_RPAR
2879 shift_token
2880 break
2881 when T_SPACE
2882 shift_token
2883 end
2884 result.push(address)
2885 end
2886 return result
2887 end
2888 end
2889
2890 ADDRESS_REGEXP = /\G\
2891 (?# 1: NAME )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2892 (?# 2: ROUTE )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2893 (?# 3: MAILBOX )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \
2894 (?# 4: HOST )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\
2895 \)/ni
2896
2897 def address
2898 match(T_LPAR)
2899 if @str.index(ADDRESS_REGEXP, @pos)
2900 # address does not include literal.
2901 @pos = $~.end(0)
2902 name = $1
2903 route = $2
2904 mailbox = $3
2905 host = $4
2906 for s in [name, route, mailbox, host]
2907 if s
2908 s.gsub!(/\\(["\\])/n, "\\1")
2909 end
2910 end
2911 else
2912 name = nstring
2913 match(T_SPACE)
2914 route = nstring
2915 match(T_SPACE)
2916 mailbox = nstring
2917 match(T_SPACE)
2918 host = nstring
2919 match(T_RPAR)
2920 end
2921 return Address.new(name, route, mailbox, host)
2922 end
2923
2924 # def flag_list
2925 # result = []
2926 # match(T_LPAR)
2927 # while true
2928 # token = lookahead
2929 # case token.symbol
2930 # when T_RPAR
2931 # shift_token
2932 # break
2933 # when T_SPACE
2934 # shift_token
2935 # end
2936 # result.push(flag)
2937 # end
2938 # return result
2939 # end
2940
2941 # def flag
2942 # token = lookahead
2943 # if token.symbol == T_BSLASH
2944 # shift_token
2945 # token = lookahead
2946 # if token.symbol == T_STAR
2947 # shift_token
2948 # return token.value.intern
2949 # else
2950 # return atom.intern
2951 # end
2952 # else
2953 # return atom
2954 # end
2955 # end
2956
2957 FLAG_REGEXP = /\
2958 (?# FLAG )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
2959 (?# ATOM )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n
2960
2961 def flag_list
2962 if @str.index(/\(([^)]*)\)/ni, @pos)
2963 @pos = $~.end(0)
2964 return $1.scan(FLAG_REGEXP).collect { |flag, atom|
2965 if atom
2966 atom
2967 else
2968 symbol = flag.capitalize.untaint.intern
2969 @flag_symbols[symbol] = true
2970 if @flag_symbols.length > IMAP.max_flag_count
2971 raise FlagCountError, "number of flag symbols exceeded"
2972 end
2973 symbol
2974 end
2975 }
2976 else
2977 parse_error("invalid flag list")
2978 end
2979 end
2980
2981 def nstring
2982 token = lookahead
2983 if token.symbol == T_NIL
2984 shift_token
2985 return nil
2986 else
2987 return string
2988 end
2989 end
2990
2991 def astring
2992 token = lookahead
2993 if string_token?(token)
2994 return string
2995 else
2996 return atom
2997 end
2998 end
2999
3000 def string
3001 token = lookahead
3002 if token.symbol == T_NIL
3003 shift_token
3004 return nil
3005 end
3006 token = match(T_QUOTED, T_LITERAL)
3007 return token.value
3008 end
3009
3010 STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]
3011
3012 def string_token?(token)
3013 return STRING_TOKENS.include?(token.symbol)
3014 end
3015
3016 def case_insensitive_string
3017 token = lookahead
3018 if token.symbol == T_NIL
3019 shift_token
3020 return nil
3021 end
3022 token = match(T_QUOTED, T_LITERAL)
3023 return token.value.upcase
3024 end
3025
3026 def atom
3027 result = ""
3028 while true
3029 token = lookahead
3030 if atom_token?(token)
3031 result.concat(token.value)
3032 shift_token
3033 else
3034 if result.empty?
3035 parse_error("unexpected token %s", token.symbol)
3036 else
3037 return result
3038 end
3039 end
3040 end
3041 end
3042
3043 ATOM_TOKENS = [
3044 T_ATOM,
3045 T_NUMBER,
3046 T_NIL,
3047 T_LBRA,
3048 T_RBRA,
3049 T_PLUS
3050 ]
3051
3052 def atom_token?(token)
3053 return ATOM_TOKENS.include?(token.symbol)
3054 end
3055
3056 def number
3057 token = lookahead
3058 if token.symbol == T_NIL
3059 shift_token
3060 return nil
3061 end
3062 token = match(T_NUMBER)
3063 return token.value.to_i
3064 end
3065
3066 def nil_atom
3067 match(T_NIL)
3068 return nil
3069 end
3070
3071 def match(*args)
3072 token = lookahead
3073 unless args.include?(token.symbol)
3074 parse_error('unexpected token %s (expected %s)',
3075 token.symbol.id2name,
3076 args.collect {|i| i.id2name}.join(" or "))
3077 end
3078 shift_token
3079 return token
3080 end
3081
3082 def lookahead
3083 unless @token
3084 @token = next_token
3085 end
3086 return @token
3087 end
3088
3089 def shift_token
3090 @token = nil
3091 end
3092
3093 def next_token
3094 case @lex_state
3095 when EXPR_BEG
3096 if @str.index(BEG_REGEXP, @pos)
3097 @pos = $~.end(0)
3098 if $1
3099 return Token.new(T_SPACE, $+)
3100 elsif $2
3101 return Token.new(T_NIL, $+)
3102 elsif $3
3103 return Token.new(T_NUMBER, $+)
3104 elsif $4
3105 return Token.new(T_ATOM, $+)
3106 elsif $5
3107 return Token.new(T_QUOTED,
3108 $+.gsub(/\\(["\\])/n, "\\1"))
3109 elsif $6
3110 return Token.new(T_LPAR, $+)
3111 elsif $7
3112 return Token.new(T_RPAR, $+)
3113 elsif $8
3114 return Token.new(T_BSLASH, $+)
3115 elsif $9
3116 return Token.new(T_STAR, $+)
3117 elsif $10
3118 return Token.new(T_LBRA, $+)
3119 elsif $11
3120 return Token.new(T_RBRA, $+)
3121 elsif $12
3122 len = $+.to_i
3123 val = @str[@pos, len]
3124 @pos += len
3125 return Token.new(T_LITERAL, val)
3126 elsif $13
3127 return Token.new(T_PLUS, $+)
3128 elsif $14
3129 return Token.new(T_PERCENT, $+)
3130 elsif $15
3131 return Token.new(T_CRLF, $+)
3132 elsif $16
3133 return Token.new(T_EOF, $+)
3134 else
3135 parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid")
3136 end
3137 else
3138 @str.index(/\S*/n, @pos)
3139 parse_error("unknown token - %s", $&.dump)
3140 end
3141 when EXPR_DATA
3142 if @str.index(DATA_REGEXP, @pos)
3143 @pos = $~.end(0)
3144 if $1
3145 return Token.new(T_SPACE, $+)
3146 elsif $2
3147 return Token.new(T_NIL, $+)
3148 elsif $3
3149 return Token.new(T_NUMBER, $+)
3150 elsif $4
3151 return Token.new(T_QUOTED,
3152 $+.gsub(/\\(["\\])/n, "\\1"))
3153 elsif $5
3154 len = $+.to_i
3155 val = @str[@pos, len]
3156 @pos += len
3157 return Token.new(T_LITERAL, val)
3158 elsif $6
3159 return Token.new(T_LPAR, $+)
3160 elsif $7
3161 return Token.new(T_RPAR, $+)
3162 else
3163 parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid")
3164 end
3165 else
3166 @str.index(/\S*/n, @pos)
3167 parse_error("unknown token - %s", $&.dump)
3168 end
3169 when EXPR_TEXT
3170 if @str.index(TEXT_REGEXP, @pos)
3171 @pos = $~.end(0)
3172 if $1
3173 return Token.new(T_TEXT, $+)
3174 else
3175 parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid")
3176 end
3177 else
3178 @str.index(/\S*/n, @pos)
3179 parse_error("unknown token - %s", $&.dump)
3180 end
3181 when EXPR_RTEXT
3182 if @str.index(RTEXT_REGEXP, @pos)
3183 @pos = $~.end(0)
3184 if $1
3185 return Token.new(T_LBRA, $+)
3186 elsif $2
3187 return Token.new(T_TEXT, $+)
3188 else
3189 parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid")
3190 end
3191 else
3192 @str.index(/\S*/n, @pos)
3193 parse_error("unknown token - %s", $&.dump)
3194 end
3195 when EXPR_CTEXT
3196 if @str.index(CTEXT_REGEXP, @pos)
3197 @pos = $~.end(0)
3198 if $1
3199 return Token.new(T_TEXT, $+)
3200 else
3201 parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid")
3202 end
3203 else
3204 @str.index(/\S*/n, @pos) #/
3205 parse_error("unknown token - %s", $&.dump)
3206 end
3207 else
3208 parse_error("invalid @lex_state - %s", @lex_state.inspect)
3209 end
3210 end
3211
3212 def parse_error(fmt, *args)
3213 if IMAP.debug
3214 $stderr.printf("@str: %s\n", @str.dump)
3215 $stderr.printf("@pos: %d\n", @pos)
3216 $stderr.printf("@lex_state: %s\n", @lex_state)
3217 if @token
3218 $stderr.printf("@token.symbol: %s\n", @token.symbol)
3219 $stderr.printf("@token.value: %s\n", @token.value.inspect)
3220 end
3221 end
3222 raise ResponseParseError, format(fmt, *args)
3223 end
3224 end
3225
3226 # Authenticator for the "LOGIN" authentication type. See
3227 # #authenticate().
3228 class LoginAuthenticator
3229 def process(data)
3230 case @state
3231 when STATE_USER
3232 @state = STATE_PASSWORD
3233 return @user
3234 when STATE_PASSWORD
3235 return @password
3236 end
3237 end
3238
3239 private
3240
3241 STATE_USER = :USER
3242 STATE_PASSWORD = :PASSWORD
3243
3244 def initialize(user, password)
3245 @user = user
3246 @password = password
3247 @state = STATE_USER
3248 end
3249 end
3250 add_authenticator "LOGIN", LoginAuthenticator
3251
3252 # Authenticator for the "PLAIN" authentication type. See
3253 # #authenticate().
3254 class PlainAuthenticator
3255 def process(data)
3256 return "\0#{@user}\0#{@password}"
3257 end
3258
3259 private
3260
3261 def initialize(user, password)
3262 @user = user
3263 @password = password
3264 end
3265 end
3266 add_authenticator "PLAIN", PlainAuthenticator
3267
3268 # Authenticator for the "CRAM-MD5" authentication type. See
3269 # #authenticate().
3270 class CramMD5Authenticator
3271 def process(challenge)
3272 digest = hmac_md5(challenge, @password)
3273 return @user + " " + digest
3274 end
3275
3276 private
3277
3278 def initialize(user, password)
3279 @user = user
3280 @password = password
3281 end
3282
3283 def hmac_md5(text, key)
3284 if key.length > 64
3285 key = Digest::MD5.digest(key)
3286 end
3287
3288 k_ipad = key + "\0" * (64 - key.length)
3289 k_opad = key + "\0" * (64 - key.length)
3290 for i in 0..63
3291 k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr
3292 k_opad[i] = (k_opad[i].ord ^ 0x5c).chr
3293 end
3294
3295 digest = Digest::MD5.digest(k_ipad + text)
3296
3297 return Digest::MD5.hexdigest(k_opad + digest)
3298 end
3299 end
3300 add_authenticator "CRAM-MD5", CramMD5Authenticator
3301
3302 # Authenticator for the "DIGEST-MD5" authentication type. See
3303 # #authenticate().
3304 class DigestMD5Authenticator
3305 def process(challenge)
3306 case @stage
3307 when STAGE_ONE
3308 @stage = STAGE_TWO
3309 sparams = {}
3310 c = StringScanner.new(challenge)
3311 while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/)
3312 k, v = c[1], c[2]
3313 if v =~ /^"(.*)"$/
3314 v = $1
3315 if v =~ /,/
3316 v = v.split(',')
3317 end
3318 end
3319 sparams[k] = v
3320 end
3321
3322 raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0
3323 raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth")
3324
3325 response = {
3326 :nonce => sparams['nonce'],
3327 :username => @user,
3328 :realm => sparams['realm'],
3329 :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
3330 :'digest-uri' => 'imap/' + sparams['realm'],
3331 :qop => 'auth',
3332 :maxbuf => 65535,
3333 :nc => "%08d" % nc(sparams['nonce']),
3334 :charset => sparams['charset'],
3335 }
3336
3337 response[:authzid] = @authname unless @authname.nil?
3338
3339 # now, the real thing
3340 a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
3341
3342 a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
3343 a1 << ':' + response[:authzid] unless response[:authzid].nil?
3344
3345 a2 = "AUTHENTICATE:" + response[:'digest-uri']
3346 a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
3347
3348 response[:response] = Digest::MD5.hexdigest(
3349 [
3350 Digest::MD5.hexdigest(a1),
3351 response.values_at(:nonce, :nc, :cnonce, :qop),
3352 Digest::MD5.hexdigest(a2)
3353 ].join(':')
3354 )
3355
3356 return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
3357 when STAGE_TWO
3358 @stage = nil
3359 # if at the second stage, return an empty string
3360 if challenge =~ /rspauth=/
3361 return ''
3362 else
3363 raise ResponseParseError, challenge
3364 end
3365 else
3366 raise ResponseParseError, challenge
3367 end
3368 end
3369
3370 def initialize(user, password, authname = nil)
3371 @user, @password, @authname = user, password, authname
3372 @nc, @stage = {}, STAGE_ONE
3373 end
3374
3375 private
3376
3377 STAGE_ONE = :stage_one
3378 STAGE_TWO = :stage_two
3379
3380 def nc(nonce)
3381 if @nc.has_key? nonce
3382 @nc[nonce] = @nc[nonce] + 1
3383 else
3384 @nc[nonce] = 1
3385 end
3386 return @nc[nonce]
3387 end
3388
3389 # some responses need quoting
3390 def qdval(k, v)
3391 return if k.nil? or v.nil?
3392 if %w"username authzid realm nonce cnonce digest-uri qop".include? k
3393 v.gsub!(/([\\"])/, "\\\1")
3394 return '%s="%s"' % [k, v]
3395 else
3396 return '%s=%s' % [k, v]
3397 end
3398 end
3399 end
3400 add_authenticator "DIGEST-MD5", DigestMD5Authenticator
3401
3402 # Superclass of IMAP errors.
3403 class Error < StandardError
3404 end
3405
3406 # Error raised when data is in the incorrect format.
3407 class DataFormatError < Error
3408 end
3409
3410 # Error raised when a response from the server is non-parseable.
3411 class ResponseParseError < Error
3412 end
3413
3414 # Superclass of all errors used to encapsulate "fail" responses
3415 # from the server.
3416 class ResponseError < Error
3417
3418 # The response that caused this error
3419 attr_accessor :response
3420
3421 def initialize(response)
3422 @response = response
3423
3424 super @response.data.text
3425 end
3426
3427 end
3428
3429 # Error raised upon a "NO" response from the server, indicating
3430 # that the client command could not be completed successfully.
3431 class NoResponseError < ResponseError
3432 end
3433
3434 # Error raised upon a "BAD" response from the server, indicating
3435 # that the client command violated the IMAP protocol, or an internal
3436 # server failure has occurred.
3437 class BadResponseError < ResponseError
3438 end
3439
3440 # Error raised upon a "BYE" response from the server, indicating
3441 # that the client is not being allowed to login, or has been timed
3442 # out due to inactivity.
3443 class ByeResponseError < ResponseError
3444 end
3445
3446 # Error raised when too many flags are interned to symbols.
3447 class FlagCountError < Error
3448 end
3449 end
3450 end
3451
3452 if __FILE__ == $0
3453 # :enddoc:
3454 require "getoptlong"
3455
3456 $stdout.sync = true
3457 $port = nil
3458 $user = ENV["USER"] || ENV["LOGNAME"]
3459 $auth = "login"
3460 $ssl = false
3461
3462 def usage
3463 $stderr.print <<EOF
3464 usage: #{$0} [options] <host>
3465
3466 --help print this message
3467 --port=PORT specifies port
3468 --user=USER specifies user
3469 --auth=AUTH specifies auth type
3470 --ssl use ssl
3471 EOF
3472 end
3473
3474 def get_password
3475 print "password: "
3476 system("stty", "-echo")
3477 begin
3478 return gets.chop
3479 ensure
3480 system("stty", "echo")
3481 print "\n"
3482 end
3483 end
3484
3485 def get_command
3486 printf("%s@%s> ", $user, $host)
3487 if line = gets
3488 return line.strip.split(/\s+/)
3489 else
3490 return nil
3491 end
3492 end
3493
3494 parser = GetoptLong.new
3495 parser.set_options(['--debug', GetoptLong::NO_ARGUMENT],
3496 ['--help', GetoptLong::NO_ARGUMENT],
3497 ['--port', GetoptLong::REQUIRED_ARGUMENT],
3498 ['--user', GetoptLong::REQUIRED_ARGUMENT],
3499 ['--auth', GetoptLong::REQUIRED_ARGUMENT],
3500 ['--ssl', GetoptLong::NO_ARGUMENT])
3501 begin
3502 parser.each_option do |name, arg|
3503 case name
3504 when "--port"
3505 $port = arg
3506 when "--user"
3507 $user = arg
3508 when "--auth"
3509 $auth = arg
3510 when "--ssl"
3511 $ssl = true
3512 when "--debug"
3513 Net::IMAP.debug = true
3514 when "--help"
3515 usage
3516 exit(1)
3517 end
3518 end
3519 rescue
3520 usage
3521 exit(1)
3522 end
3523
3524 $host = ARGV.shift
3525 unless $host
3526 usage
3527 exit(1)
3528 end
3529
3530 imap = Net::IMAP.new($host, :port => $port, :ssl => $ssl)
3531 begin
3532 password = get_password
3533 imap.authenticate($auth, $user, password)
3534 while true
3535 cmd, *args = get_command
3536 break unless cmd
3537 begin
3538 case cmd
3539 when "list"
3540 for mbox in imap.list("", args[0] || "*")
3541 if mbox.attr.include?(Net::IMAP::NOSELECT)
3542 prefix = "!"
3543 elsif mbox.attr.include?(Net::IMAP::MARKED)
3544 prefix = "*"
3545 else
3546 prefix = " "
3547 end
3548 print prefix, mbox.name, "\n"
3549 end
3550 when "select"
3551 imap.select(args[0] || "inbox")
3552 print "ok\n"
3553 when "close"
3554 imap.close
3555 print "ok\n"
3556 when "summary"
3557 unless messages = imap.responses["EXISTS"][-1]
3558 puts "not selected"
3559 next
3560 end
3561 if messages > 0
3562 for data in imap.fetch(1..-1, ["ENVELOPE"])
3563 print data.seqno, ": ", data.attr["ENVELOPE"].subject, "\n"
3564 end
3565 else
3566 puts "no message"
3567 end
3568 when "fetch"
3569 if args[0]
3570 data = imap.fetch(args[0].to_i, ["RFC822.HEADER", "RFC822.TEXT"])[0]
3571 puts data.attr["RFC822.HEADER"]
3572 puts data.attr["RFC822.TEXT"]
3573 else
3574 puts "missing argument"
3575 end
3576 when "logout", "exit", "quit"
3577 break
3578 when "help", "?"
3579 print <<EOF
3580 list [pattern] list mailboxes
3581 select [mailbox] select mailbox
3582 close close mailbox
3583 summary display summary
3584 fetch [msgno] display message
3585 logout logout
3586 help, ? display help message
3587 EOF
3588 else
3589 print "unknown command: ", cmd, "\n"
3590 end
3591 rescue Net::IMAP::Error
3592 puts $!
3593 end
3594 end
3595 ensure
3596 imap.logout
3597 imap.disconnect
3598 end
3599 end
3600
0 # A parser for SGML, using the derived class as static DTD.
1 # from http://raa.ruby-lang.org/project/html-parser
2
3 class SGMLParser
4
5 # Regular expressions used for parsing:
6 Interesting = /[&<]/
7 Incomplete = Regexp.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|' +
8 '<([a-zA-Z][^<>]*|/([a-zA-Z][^<>]*)?|' +
9 '![^<>]*)?')
10
11 Entityref = /&([a-zA-Z][-.a-zA-Z0-9]*)[^-.a-zA-Z0-9]/
12 Charref = /&#([0-9]+)[^0-9]/
13
14 Starttagopen = /<[>a-zA-Z]/
15 Endtagopen = /<\/[<>a-zA-Z]/
16 Endbracket = /[<>]/
17 Special = /<![^<>]*>/
18 Commentopen = /<!--/
19 Commentclose = /--[ \t\n]*>/
20 Tagfind = /[a-zA-Z][a-zA-Z0-9.-]*/
21 Attrfind = Regexp.compile('[\s,]*([a-zA-Z_][a-zA-Z_0-9.-]*)' +
22 '(\s*=\s*' +
23 "('[^']*'" +
24 '|"[^"]*"' +
25 '|[-~a-zA-Z0-9,./:+*%?!()_#=]*))?')
26
27 Entitydefs =
28 {'lt'=>'<', 'gt'=>'>', 'amp'=>'&', 'quot'=>'"', 'apos'=>'\''}
29
30 def initialize(verbose=false)
31 @verbose = verbose
32 reset
33 end
34
35 def reset
36 @rawdata = ''
37 @stack = []
38 @lasttag = '???'
39 @nomoretags = false
40 @literal = false
41 end
42
43 def has_context(gi)
44 @stack.include? gi
45 end
46
47 def setnomoretags
48 @nomoretags = true
49 @literal = true
50 end
51
52 def setliteral(*args)
53 @literal = true
54 end
55
56 def feed(data)
57 @rawdata << data
58 goahead(false)
59 end
60
61 def close
62 goahead(true)
63 end
64
65 def goahead(_end)
66 rawdata = @rawdata
67 i = 0
68 n = rawdata.length
69 while i < n
70 if @nomoretags
71 handle_data(rawdata[i..(n-1)])
72 i = n
73 break
74 end
75 j = rawdata.index(Interesting, i)
76 j = n unless j
77 if i < j
78 handle_data(rawdata[i..(j-1)])
79 end
80 i = j
81 break if (i == n)
82 if rawdata[i] == ?< #
83 if rawdata.index(Starttagopen, i) == i
84 if @literal
85 handle_data(rawdata[i, 1])
86 i += 1
87 next
88 end
89 k = parse_starttag(i)
90 break unless k
91 i = k
92 next
93 end
94 if rawdata.index(Endtagopen, i) == i
95 k = parse_endtag(i)
96 break unless k
97 i = k
98 @literal = false
99 next
100 end
101 if rawdata.index(Commentopen, i) == i
102 if @literal
103 handle_data(rawdata[i,1])
104 i += 1
105 next
106 end
107 k = parse_comment(i)
108 break unless k
109 i += k
110 next
111 end
112 if rawdata.index(Special, i) == i
113 if @literal
114 handle_data(rawdata[i, 1])
115 i += 1
116 next
117 end
118 k = parse_special(i)
119 break unless k
120 i += k
121 next
122 end
123 elsif rawdata[i] == ?& #
124 if rawdata.index(Charref, i) == i
125 i += $&.length
126 handle_charref($1)
127 i -= 1 unless rawdata[i-1] == ?;
128 next
129 end
130 if rawdata.index(Entityref, i) == i
131 i += $&.length
132 handle_entityref($1)
133 i -= 1 unless rawdata[i-1] == ?;
134 next
135 end
136 else
137 raise RuntimeError, 'neither < nor & ??'
138 end
139 # We get here only if incomplete matches but
140 # nothing else
141 match = rawdata.index(Incomplete, i)
142 unless match == i
143 handle_data(rawdata[i, 1])
144 i += 1
145 next
146 end
147 j = match + $&.length
148 break if j == n # Really incomplete
149 handle_data(rawdata[i..(j-1)])
150 i = j
151 end
152 # end while
153 if _end and i < n
154 handle_data(@rawdata[i..(n-1)])
155 i = n
156 end
157 @rawdata = rawdata[i..-1]
158 end
159
160 def parse_comment(i)
161 rawdata = @rawdata
162 if rawdata[i, 4] != '<!--'
163 raise RuntimeError, 'unexpected call to handle_comment'
164 end
165 match = rawdata.index(Commentclose, i)
166 return nil unless match
167 matched_length = $&.length
168 j = match
169 handle_comment(rawdata[i+4..(j-1)])
170 j = match + matched_length
171 return j-i
172 end
173
174 def parse_starttag(i)
175 rawdata = @rawdata
176 j = rawdata.index(Endbracket, i + 1)
177 return nil unless j
178 attrs = []
179 if rawdata[i+1] == ?> #
180 # SGML shorthand: <> == <last open tag seen>
181 k = j
182 tag = @lasttag
183 else
184 match = rawdata.index(Tagfind, i + 1)
185 unless match
186 raise RuntimeError, 'unexpected call to parse_starttag'
187 end
188 k = i + 1 + ($&.length)
189 tag = $&.downcase
190 @lasttag = tag
191 end
192 while k < j
193 break unless rawdata.index(Attrfind, k)
194 matched_length = $&.length
195 attrname, rest, attrvalue = $1, $2, $3
196 if not rest
197 attrvalue = '' # was: = attrname
198 elsif (attrvalue[0] == ?' && attrvalue[-1] == ?') or
199 (attrvalue[0] == ?" && attrvalue[-1,1] == ?")
200 attrvalue = attrvalue[1..-2]
201 end
202 attrs << [attrname.downcase, attrvalue]
203 k += matched_length
204 end
205 if rawdata[j] == ?> #
206 j += 1
207 end
208 finish_starttag(tag, attrs)
209 return j
210 end
211
212 def parse_endtag(i)
213 rawdata = @rawdata
214 j = rawdata.index(Endbracket, i + 1)
215 return nil unless j
216 tag = (rawdata[i+2..j-1].strip).downcase
217 if rawdata[j] == ?> #
218 j += 1
219 end
220 finish_endtag(tag)
221 return j
222 end
223
224 def finish_starttag(tag, attrs)
225 method = 'start_' + tag
226 if self.respond_to?(method)
227 @stack << tag
228 handle_starttag(tag, method, attrs)
229 return 1
230 else
231 method = 'do_' + tag
232 if self.respond_to?(method)
233 handle_starttag(tag, method, attrs)
234 return 0
235 else
236 unknown_starttag(tag, attrs)
237 return -1
238 end
239 end
240 end
241
242 def finish_endtag(tag)
243 if tag == ''
244 found = @stack.length - 1
245 if found < 0
246 unknown_endtag(tag)
247 return
248 end
249 else
250 unless @stack.include? tag
251 method = 'end_' + tag
252 unless self.respond_to?(method)
253 unknown_endtag(tag)
254 end
255 return
256 end
257 found = @stack.index(tag) #or @stack.length
258 end
259 while @stack.length > found
260 tag = @stack[-1]
261 method = 'end_' + tag
262 if respond_to?(method)
263 handle_endtag(tag, method)
264 else
265 unknown_endtag(tag)
266 end
267 @stack.pop
268 end
269 end
270
271 def parse_special(i)
272 rawdata = @rawdata
273 match = rawdata.index(Endbracket, i+1)
274 return nil unless match
275 matched_length = $&.length
276 handle_special(rawdata[i+1..(match-1)])
277 return match - i + matched_length
278 end
279
280 def handle_starttag(tag, method, attrs)
281 self.send(method, attrs)
282 end
283
284 def handle_endtag(tag, method)
285 self.send(method)
286 end
287
288 def report_unbalanced(tag)
289 if @verbose
290 print '*** Unbalanced </' + tag + '>', "\n"
291 print '*** Stack:', self.stack, "\n"
292 end
293 end
294
295 def handle_charref(name)
296 n = Integer(name)
297 if !(0 <= n && n <= 255)
298 unknown_charref(name)
299 return
300 end
301 handle_data(n.chr)
302 end
303
304 def handle_entityref(name)
305 table = Entitydefs
306 if table.include?(name)
307 handle_data(table[name])
308 else
309 unknown_entityref(name)
310 return
311 end
312 end
313
314 def handle_data(data)
315 end
316
317 def handle_comment(data)
318 end
319
320 def handle_special(data)
321 end
322
323 def unknown_starttag(tag, attrs)
324 end
325 def unknown_endtag(tag)
326 end
327 def unknown_charref(ref)
328 end
329 def unknown_entityref(ref)
330 end
331
332 end
0 require 'feed2imap/feed2imap'
0 #!/usr/bin/ruby
1 #
2 # setup.rb
3 #
4 # Copyright (c) 2000-2005 Minero Aoki
5 #
6 # This program is free software.
7 # You can distribute/modify this program under the terms of
8 # the GNU LGPL, Lesser General Public License version 2.1.
9 #
10
11 unless Enumerable.method_defined?(:map) # Ruby 1.4.6
12 module Enumerable
13 alias map collect
14 end
15 end
16
17 unless File.respond_to?(:read) # Ruby 1.6
18 def File.read(fname)
19 open(fname) {|f|
20 return f.read
21 }
22 end
23 end
24
25 unless Errno.const_defined?(:ENOTEMPTY) # Windows?
26 module Errno
27 class ENOTEMPTY
28 # We do not raise this exception, implementation is not needed.
29 end
30 end
31 end
32
33 def File.binread(fname)
34 open(fname, 'rb') {|f|
35 return f.read
36 }
37 end
38
39 # for corrupted Windows' stat(2)
40 def File.dir?(path)
41 File.directory?((path[-1,1] == '/') ? path : path + '/')
42 end
43
44
45 class ConfigTable
46
47 include Enumerable
48
49 def initialize(rbconfig)
50 @rbconfig = rbconfig
51 @items = []
52 @table = {}
53 # options
54 @install_prefix = nil
55 @config_opt = nil
56 @verbose = true
57 @no_harm = false
58 end
59
60 attr_accessor :install_prefix
61 attr_accessor :config_opt
62
63 attr_writer :verbose
64
65 def verbose?
66 @verbose
67 end
68
69 attr_writer :no_harm
70
71 def no_harm?
72 @no_harm
73 end
74
75 def [](key)
76 lookup(key).resolve(self)
77 end
78
79 def []=(key, val)
80 lookup(key).set val
81 end
82
83 def names
84 @items.map {|i| i.name }
85 end
86
87 def each(&block)
88 @items.each(&block)
89 end
90
91 def key?(name)
92 @table.key?(name)
93 end
94
95 def lookup(name)
96 @table[name] or setup_rb_error "no such config item: #{name}"
97 end
98
99 def add(item)
100 @items.push item
101 @table[item.name] = item
102 end
103
104 def remove(name)
105 item = lookup(name)
106 @items.delete_if {|i| i.name == name }
107 @table.delete_if {|name, i| i.name == name }
108 item
109 end
110
111 def load_script(path, inst = nil)
112 if File.file?(path)
113 MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
114 end
115 end
116
117 def savefile
118 '.config'
119 end
120
121 def load_savefile
122 begin
123 File.foreach(savefile()) do |line|
124 k, v = *line.split(/=/, 2)
125 self[k] = v.strip
126 end
127 rescue Errno::ENOENT
128 setup_rb_error $!.message + "\n#{File.basename($0)} config first"
129 end
130 end
131
132 def save
133 @items.each {|i| i.value }
134 File.open(savefile(), 'w') {|f|
135 @items.each do |i|
136 f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
137 end
138 }
139 end
140
141 def load_standard_entries
142 standard_entries(@rbconfig).each do |ent|
143 add ent
144 end
145 end
146
147 def standard_entries(rbconfig)
148 c = rbconfig
149
150 rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
151
152 major = c['MAJOR'].to_i
153 minor = c['MINOR'].to_i
154 teeny = c['TEENY'].to_i
155 version = "#{major}.#{minor}"
156
157 # ruby ver. >= 1.4.4?
158 newpath_p = ((major >= 2) or
159 ((major == 1) and
160 ((minor >= 5) or
161 ((minor == 4) and (teeny >= 4)))))
162
163 if c['rubylibdir']
164 # V > 1.6.3
165 libruby = "#{c['prefix']}/lib/ruby"
166 librubyver = c['rubylibdir']
167 librubyverarch = c['archdir']
168 siteruby = c['sitedir']
169 siterubyver = c['sitelibdir']
170 siterubyverarch = c['sitearchdir']
171 elsif newpath_p
172 # 1.4.4 <= V <= 1.6.3
173 libruby = "#{c['prefix']}/lib/ruby"
174 librubyver = "#{c['prefix']}/lib/ruby/#{version}"
175 librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
176 siteruby = c['sitedir']
177 siterubyver = "$siteruby/#{version}"
178 siterubyverarch = "$siterubyver/#{c['arch']}"
179 else
180 # V < 1.4.4
181 libruby = "#{c['prefix']}/lib/ruby"
182 librubyver = "#{c['prefix']}/lib/ruby/#{version}"
183 librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
184 siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
185 siterubyver = siteruby
186 siterubyverarch = "$siterubyver/#{c['arch']}"
187 end
188 parameterize = lambda {|path|
189 path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
190 }
191
192 if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
193 makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
194 else
195 makeprog = 'make'
196 end
197
198 [
199 ExecItem.new('installdirs', 'std/site/home',
200 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
201 {|val, table|
202 case val
203 when 'std'
204 table['rbdir'] = '$librubyver'
205 table['sodir'] = '$librubyverarch'
206 when 'site'
207 table['rbdir'] = '$siterubyver'
208 table['sodir'] = '$siterubyverarch'
209 when 'home'
210 setup_rb_error '$HOME was not set' unless ENV['HOME']
211 table['prefix'] = ENV['HOME']
212 table['rbdir'] = '$libdir/ruby'
213 table['sodir'] = '$libdir/ruby'
214 end
215 },
216 PathItem.new('prefix', 'path', c['prefix'],
217 'path prefix of target environment'),
218 PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
219 'the directory for commands'),
220 PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
221 'the directory for libraries'),
222 PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
223 'the directory for shared data'),
224 PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
225 'the directory for man pages'),
226 PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
227 'the directory for system configuration files'),
228 PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
229 'the directory for local state data'),
230 PathItem.new('libruby', 'path', libruby,
231 'the directory for ruby libraries'),
232 PathItem.new('librubyver', 'path', librubyver,
233 'the directory for standard ruby libraries'),
234 PathItem.new('librubyverarch', 'path', librubyverarch,
235 'the directory for standard ruby extensions'),
236 PathItem.new('siteruby', 'path', siteruby,
237 'the directory for version-independent aux ruby libraries'),
238 PathItem.new('siterubyver', 'path', siterubyver,
239 'the directory for aux ruby libraries'),
240 PathItem.new('siterubyverarch', 'path', siterubyverarch,
241 'the directory for aux ruby binaries'),
242 PathItem.new('rbdir', 'path', '$siterubyver',
243 'the directory for ruby scripts'),
244 PathItem.new('sodir', 'path', '$siterubyverarch',
245 'the directory for ruby extentions'),
246 PathItem.new('rubypath', 'path', rubypath,
247 'the path to set to #! line'),
248 ProgramItem.new('rubyprog', 'name', rubypath,
249 'the ruby program using for installation'),
250 ProgramItem.new('makeprog', 'name', makeprog,
251 'the make program to compile ruby extentions'),
252 SelectItem.new('shebang', 'all/ruby/never', 'never',
253 'shebang line (#!) editing mode'),
254 BoolItem.new('without-ext', 'yes/no', 'no',
255 'does not compile/install ruby extentions')
256 ]
257 end
258 private :standard_entries
259
260 def load_multipackage_entries
261 multipackage_entries().each do |ent|
262 add ent
263 end
264 end
265
266 def multipackage_entries
267 [
268 PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
269 'package names that you want to install'),
270 PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
271 'package names that you do not want to install')
272 ]
273 end
274 private :multipackage_entries
275
276 ALIASES = {
277 'std-ruby' => 'librubyver',
278 'stdruby' => 'librubyver',
279 'rubylibdir' => 'librubyver',
280 'archdir' => 'librubyverarch',
281 'site-ruby-common' => 'siteruby', # For backward compatibility
282 'site-ruby' => 'siterubyver', # For backward compatibility
283 'bin-dir' => 'bindir',
284 'bin-dir' => 'bindir',
285 'rb-dir' => 'rbdir',
286 'so-dir' => 'sodir',
287 'data-dir' => 'datadir',
288 'ruby-path' => 'rubypath',
289 'ruby-prog' => 'rubyprog',
290 'ruby' => 'rubyprog',
291 'make-prog' => 'makeprog',
292 'make' => 'makeprog'
293 }
294
295 def fixup
296 ALIASES.each do |ali, name|
297 @table[ali] = @table[name]
298 end
299 @items.freeze
300 @table.freeze
301 @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
302 end
303
304 def parse_opt(opt)
305 m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}"
306 m.to_a[1,2]
307 end
308
309 def dllext
310 @rbconfig['DLEXT']
311 end
312
313 def value_config?(name)
314 lookup(name).value?
315 end
316
317 class Item
318 def initialize(name, template, default, desc)
319 @name = name.freeze
320 @template = template
321 @value = default
322 @default = default
323 @description = desc
324 end
325
326 attr_reader :name
327 attr_reader :description
328
329 attr_accessor :default
330 alias help_default default
331
332 def help_opt
333 "--#{@name}=#{@template}"
334 end
335
336 def value?
337 true
338 end
339
340 def value
341 @value
342 end
343
344 def resolve(table)
345 @value.gsub(%r<\$([^/]+)>) { table[$1] }
346 end
347
348 def set(val)
349 @value = check(val)
350 end
351
352 private
353
354 def check(val)
355 setup_rb_error "config: --#{name} requires argument" unless val
356 val
357 end
358 end
359
360 class BoolItem < Item
361 def config_type
362 'bool'
363 end
364
365 def help_opt
366 "--#{@name}"
367 end
368
369 private
370
371 def check(val)
372 return 'yes' unless val
373 case val
374 when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
375 when /\An(o)?\z/i, /\Af(alse)\z/i then 'no'
376 else
377 setup_rb_error "config: --#{@name} accepts only yes/no for argument"
378 end
379 end
380 end
381
382 class PathItem < Item
383 def config_type
384 'path'
385 end
386
387 private
388
389 def check(path)
390 setup_rb_error "config: --#{@name} requires argument" unless path
391 path[0,1] == '$' ? path : File.expand_path(path)
392 end
393 end
394
395 class ProgramItem < Item
396 def config_type
397 'program'
398 end
399 end
400
401 class SelectItem < Item
402 def initialize(name, selection, default, desc)
403 super
404 @ok = selection.split('/')
405 end
406
407 def config_type
408 'select'
409 end
410
411 private
412
413 def check(val)
414 unless @ok.include?(val.strip)
415 setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
416 end
417 val.strip
418 end
419 end
420
421 class ExecItem < Item
422 def initialize(name, selection, desc, &block)
423 super name, selection, nil, desc
424 @ok = selection.split('/')
425 @action = block
426 end
427
428 def config_type
429 'exec'
430 end
431
432 def value?
433 false
434 end
435
436 def resolve(table)
437 setup_rb_error "$#{name()} wrongly used as option value"
438 end
439
440 undef set
441
442 def evaluate(val, table)
443 v = val.strip.downcase
444 unless @ok.include?(v)
445 setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
446 end
447 @action.call v, table
448 end
449 end
450
451 class PackageSelectionItem < Item
452 def initialize(name, template, default, help_default, desc)
453 super name, template, default, desc
454 @help_default = help_default
455 end
456
457 attr_reader :help_default
458
459 def config_type
460 'package'
461 end
462
463 private
464
465 def check(val)
466 unless File.dir?("packages/#{val}")
467 setup_rb_error "config: no such package: #{val}"
468 end
469 val
470 end
471 end
472
473 class MetaConfigEnvironment
474 def initialize(config, installer)
475 @config = config
476 @installer = installer
477 end
478
479 def config_names
480 @config.names
481 end
482
483 def config?(name)
484 @config.key?(name)
485 end
486
487 def bool_config?(name)
488 @config.lookup(name).config_type == 'bool'
489 end
490
491 def path_config?(name)
492 @config.lookup(name).config_type == 'path'
493 end
494
495 def value_config?(name)
496 @config.lookup(name).config_type != 'exec'
497 end
498
499 def add_config(item)
500 @config.add item
501 end
502
503 def add_bool_config(name, default, desc)
504 @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
505 end
506
507 def add_path_config(name, default, desc)
508 @config.add PathItem.new(name, 'path', default, desc)
509 end
510
511 def set_config_default(name, default)
512 @config.lookup(name).default = default
513 end
514
515 def remove_config(name)
516 @config.remove(name)
517 end
518
519 # For only multipackage
520 def packages
521 raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
522 @installer.packages
523 end
524
525 # For only multipackage
526 def declare_packages(list)
527 raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
528 @installer.packages = list
529 end
530 end
531
532 end # class ConfigTable
533
534
535 # This module requires: #verbose?, #no_harm?
536 module FileOperations
537
538 def mkdir_p(dirname, prefix = nil)
539 dirname = prefix + File.expand_path(dirname) if prefix
540 $stderr.puts "mkdir -p #{dirname}" if verbose?
541 return if no_harm?
542
543 # Does not check '/', it's too abnormal.
544 dirs = File.expand_path(dirname).split(%r<(?=/)>)
545 if /\A[a-z]:\z/i =~ dirs[0]
546 disk = dirs.shift
547 dirs[0] = disk + dirs[0]
548 end
549 dirs.each_index do |idx|
550 path = dirs[0..idx].join('')
551 Dir.mkdir path unless File.dir?(path)
552 end
553 end
554
555 def rm_f(path)
556 $stderr.puts "rm -f #{path}" if verbose?
557 return if no_harm?
558 force_remove_file path
559 end
560
561 def rm_rf(path)
562 $stderr.puts "rm -rf #{path}" if verbose?
563 return if no_harm?
564 remove_tree path
565 end
566
567 def remove_tree(path)
568 if File.symlink?(path)
569 remove_file path
570 elsif File.dir?(path)
571 remove_tree0 path
572 else
573 force_remove_file path
574 end
575 end
576
577 def remove_tree0(path)
578 Dir.foreach(path) do |ent|
579 next if ent == '.'
580 next if ent == '..'
581 entpath = "#{path}/#{ent}"
582 if File.symlink?(entpath)
583 remove_file entpath
584 elsif File.dir?(entpath)
585 remove_tree0 entpath
586 else
587 force_remove_file entpath
588 end
589 end
590 begin
591 Dir.rmdir path
592 rescue Errno::ENOTEMPTY
593 # directory may not be empty
594 end
595 end
596
597 def move_file(src, dest)
598 force_remove_file dest
599 begin
600 File.rename src, dest
601 rescue
602 File.open(dest, 'wb') {|f|
603 f.write File.binread(src)
604 }
605 File.chmod File.stat(src).mode, dest
606 File.unlink src
607 end
608 end
609
610 def force_remove_file(path)
611 begin
612 remove_file path
613 rescue
614 end
615 end
616
617 def remove_file(path)
618 File.chmod 0777, path
619 File.unlink path
620 end
621
622 def install(from, dest, mode, prefix = nil)
623 $stderr.puts "install #{from} #{dest}" if verbose?
624 return if no_harm?
625
626 realdest = prefix ? prefix + File.expand_path(dest) : dest
627 realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
628 str = File.binread(from)
629 if diff?(str, realdest)
630 verbose_off {
631 rm_f realdest if File.exist?(realdest)
632 }
633 File.open(realdest, 'wb') {|f|
634 f.write str
635 }
636 File.chmod mode, realdest
637
638 File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
639 if prefix
640 f.puts realdest.sub(prefix, '')
641 else
642 f.puts realdest
643 end
644 }
645 end
646 end
647
648 def diff?(new_content, path)
649 return true unless File.exist?(path)
650 new_content != File.binread(path)
651 end
652
653 def command(*args)
654 $stderr.puts args.join(' ') if verbose?
655 system(*args) or raise RuntimeError,
656 "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
657 end
658
659 def ruby(*args)
660 command config('rubyprog'), *args
661 end
662
663 def make(task = nil)
664 command(*[config('makeprog'), task].compact)
665 end
666
667 def extdir?(dir)
668 File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
669 end
670
671 def files_of(dir)
672 Dir.open(dir) {|d|
673 return d.select {|ent| File.file?("#{dir}/#{ent}") }
674 }
675 end
676
677 DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
678
679 def directories_of(dir)
680 Dir.open(dir) {|d|
681 return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
682 }
683 end
684
685 end
686
687
688 # This module requires: #srcdir_root, #objdir_root, #relpath
689 module HookScriptAPI
690
691 def get_config(key)
692 @config[key]
693 end
694
695 alias config get_config
696
697 # obsolete: use metaconfig to change configuration
698 def set_config(key, val)
699 @config[key] = val
700 end
701
702 #
703 # srcdir/objdir (works only in the package directory)
704 #
705
706 def curr_srcdir
707 "#{srcdir_root()}/#{relpath()}"
708 end
709
710 def curr_objdir
711 "#{objdir_root()}/#{relpath()}"
712 end
713
714 def srcfile(path)
715 "#{curr_srcdir()}/#{path}"
716 end
717
718 def srcexist?(path)
719 File.exist?(srcfile(path))
720 end
721
722 def srcdirectory?(path)
723 File.dir?(srcfile(path))
724 end
725
726 def srcfile?(path)
727 File.file?(srcfile(path))
728 end
729
730 def srcentries(path = '.')
731 Dir.open("#{curr_srcdir()}/#{path}") {|d|
732 return d.to_a - %w(. ..)
733 }
734 end
735
736 def srcfiles(path = '.')
737 srcentries(path).select {|fname|
738 File.file?(File.join(curr_srcdir(), path, fname))
739 }
740 end
741
742 def srcdirectories(path = '.')
743 srcentries(path).select {|fname|
744 File.dir?(File.join(curr_srcdir(), path, fname))
745 }
746 end
747
748 end
749
750
751 class ToplevelInstaller
752
753 Version = '3.4.1'
754 Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
755
756 TASKS = [
757 [ 'all', 'do config, setup, then install' ],
758 [ 'config', 'saves your configurations' ],
759 [ 'show', 'shows current configuration' ],
760 [ 'setup', 'compiles ruby extentions and others' ],
761 [ 'install', 'installs files' ],
762 [ 'test', 'run all tests in test/' ],
763 [ 'clean', "does `make clean' for each extention" ],
764 [ 'distclean',"does `make distclean' for each extention" ]
765 ]
766
767 def ToplevelInstaller.invoke
768 config = ConfigTable.new(load_rbconfig())
769 config.load_standard_entries
770 config.load_multipackage_entries if multipackage?
771 config.fixup
772 klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
773 klass.new(File.dirname($0), config).invoke
774 end
775
776 def ToplevelInstaller.multipackage?
777 File.dir?(File.dirname($0) + '/packages')
778 end
779
780 def ToplevelInstaller.load_rbconfig
781 if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
782 ARGV.delete(arg)
783 load File.expand_path(arg.split(/=/, 2)[1])
784 $".push 'rbconfig.rb'
785 else
786 require 'rbconfig'
787 end
788 ::Config::CONFIG
789 end
790
791 def initialize(ardir_root, config)
792 @ardir = File.expand_path(ardir_root)
793 @config = config
794 # cache
795 @valid_task_re = nil
796 end
797
798 def config(key)
799 @config[key]
800 end
801
802 def inspect
803 "#<#{self.class} #{__id__()}>"
804 end
805
806 def invoke
807 run_metaconfigs
808 case task = parsearg_global()
809 when nil, 'all'
810 parsearg_config
811 init_installers
812 exec_config
813 exec_setup
814 exec_install
815 else
816 case task
817 when 'config', 'test'
818 ;
819 when 'clean', 'distclean'
820 @config.load_savefile if File.exist?(@config.savefile)
821 else
822 @config.load_savefile
823 end
824 __send__ "parsearg_#{task}"
825 init_installers
826 __send__ "exec_#{task}"
827 end
828 end
829
830 def run_metaconfigs
831 @config.load_script "#{@ardir}/metaconfig"
832 end
833
834 def init_installers
835 @installer = Installer.new(@config, @ardir, File.expand_path('.'))
836 end
837
838 #
839 # Hook Script API bases
840 #
841
842 def srcdir_root
843 @ardir
844 end
845
846 def objdir_root
847 '.'
848 end
849
850 def relpath
851 '.'
852 end
853
854 #
855 # Option Parsing
856 #
857
858 def parsearg_global
859 while arg = ARGV.shift
860 case arg
861 when /\A\w+\z/
862 setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
863 return arg
864 when '-q', '--quiet'
865 @config.verbose = false
866 when '--verbose'
867 @config.verbose = true
868 when '--help'
869 print_usage $stdout
870 exit 0
871 when '--version'
872 puts "#{File.basename($0)} version #{Version}"
873 exit 0
874 when '--copyright'
875 puts Copyright
876 exit 0
877 else
878 setup_rb_error "unknown global option '#{arg}'"
879 end
880 end
881 nil
882 end
883
884 def valid_task?(t)
885 valid_task_re() =~ t
886 end
887
888 def valid_task_re
889 @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/
890 end
891
892 def parsearg_no_options
893 unless ARGV.empty?
894 task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1)
895 setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}"
896 end
897 end
898
899 alias parsearg_show parsearg_no_options
900 alias parsearg_setup parsearg_no_options
901 alias parsearg_test parsearg_no_options
902 alias parsearg_clean parsearg_no_options
903 alias parsearg_distclean parsearg_no_options
904
905 def parsearg_config
906 evalopt = []
907 set = []
908 @config.config_opt = []
909 while i = ARGV.shift
910 if /\A--?\z/ =~ i
911 @config.config_opt = ARGV.dup
912 break
913 end
914 name, value = *@config.parse_opt(i)
915 if @config.value_config?(name)
916 @config[name] = value
917 else
918 evalopt.push [name, value]
919 end
920 set.push name
921 end
922 evalopt.each do |name, value|
923 @config.lookup(name).evaluate value, @config
924 end
925 # Check if configuration is valid
926 set.each do |n|
927 @config[n] if @config.value_config?(n)
928 end
929 end
930
931 def parsearg_install
932 @config.no_harm = false
933 @config.install_prefix = ''
934 while a = ARGV.shift
935 case a
936 when '--no-harm'
937 @config.no_harm = true
938 when /\A--prefix=/
939 path = a.split(/=/, 2)[1]
940 path = File.expand_path(path) unless path[0,1] == '/'
941 @config.install_prefix = path
942 else
943 setup_rb_error "install: unknown option #{a}"
944 end
945 end
946 end
947
948 def print_usage(out)
949 out.puts 'Typical Installation Procedure:'
950 out.puts " $ ruby #{File.basename $0} config"
951 out.puts " $ ruby #{File.basename $0} setup"
952 out.puts " # ruby #{File.basename $0} install (may require root privilege)"
953 out.puts
954 out.puts 'Detailed Usage:'
955 out.puts " ruby #{File.basename $0} <global option>"
956 out.puts " ruby #{File.basename $0} [<global options>] <task> [<task options>]"
957
958 fmt = " %-24s %s\n"
959 out.puts
960 out.puts 'Global options:'
961 out.printf fmt, '-q,--quiet', 'suppress message outputs'
962 out.printf fmt, ' --verbose', 'output messages verbosely'
963 out.printf fmt, ' --help', 'print this message'
964 out.printf fmt, ' --version', 'print version and quit'
965 out.printf fmt, ' --copyright', 'print copyright and quit'
966 out.puts
967 out.puts 'Tasks:'
968 TASKS.each do |name, desc|
969 out.printf fmt, name, desc
970 end
971
972 fmt = " %-24s %s [%s]\n"
973 out.puts
974 out.puts 'Options for CONFIG or ALL:'
975 @config.each do |item|
976 out.printf fmt, item.help_opt, item.description, item.help_default
977 end
978 out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
979 out.puts
980 out.puts 'Options for INSTALL:'
981 out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
982 out.printf fmt, '--prefix=path', 'install path prefix', ''
983 out.puts
984 end
985
986 #
987 # Task Handlers
988 #
989
990 def exec_config
991 @installer.exec_config
992 @config.save # must be final
993 end
994
995 def exec_setup
996 @installer.exec_setup
997 end
998
999 def exec_install
1000 @installer.exec_install
1001 end
1002
1003 def exec_test
1004 @installer.exec_test
1005 end
1006
1007 def exec_show
1008 @config.each do |i|
1009 printf "%-20s %s\n", i.name, i.value if i.value?
1010 end
1011 end
1012
1013 def exec_clean
1014 @installer.exec_clean
1015 end
1016
1017 def exec_distclean
1018 @installer.exec_distclean
1019 end
1020
1021 end # class ToplevelInstaller
1022
1023
1024 class ToplevelInstallerMulti < ToplevelInstaller
1025
1026 include FileOperations
1027
1028 def initialize(ardir_root, config)
1029 super
1030 @packages = directories_of("#{@ardir}/packages")
1031 raise 'no package exists' if @packages.empty?
1032 @root_installer = Installer.new(@config, @ardir, File.expand_path('.'))
1033 end
1034
1035 def run_metaconfigs
1036 @config.load_script "#{@ardir}/metaconfig", self
1037 @packages.each do |name|
1038 @config.load_script "#{@ardir}/packages/#{name}/metaconfig"
1039 end
1040 end
1041
1042 attr_reader :packages
1043
1044 def packages=(list)
1045 raise 'package list is empty' if list.empty?
1046 list.each do |name|
1047 raise "directory packages/#{name} does not exist"\
1048 unless File.dir?("#{@ardir}/packages/#{name}")
1049 end
1050 @packages = list
1051 end
1052
1053 def init_installers
1054 @installers = {}
1055 @packages.each do |pack|
1056 @installers[pack] = Installer.new(@config,
1057 "#{@ardir}/packages/#{pack}",
1058 "packages/#{pack}")
1059 end
1060 with = extract_selection(config('with'))
1061 without = extract_selection(config('without'))
1062 @selected = @installers.keys.select {|name|
1063 (with.empty? or with.include?(name)) \
1064 and not without.include?(name)
1065 }
1066 end
1067
1068 def extract_selection(list)
1069 a = list.split(/,/)
1070 a.each do |name|
1071 setup_rb_error "no such package: #{name}" unless @installers.key?(name)
1072 end
1073 a
1074 end
1075
1076 def print_usage(f)
1077 super
1078 f.puts 'Inluded packages:'
1079 f.puts ' ' + @packages.sort.join(' ')
1080 f.puts
1081 end
1082
1083 #
1084 # Task Handlers
1085 #
1086
1087 def exec_config
1088 run_hook 'pre-config'
1089 each_selected_installers {|inst| inst.exec_config }
1090 run_hook 'post-config'
1091 @config.save # must be final
1092 end
1093
1094 def exec_setup
1095 run_hook 'pre-setup'
1096 each_selected_installers {|inst| inst.exec_setup }
1097 run_hook 'post-setup'
1098 end
1099
1100 def exec_install
1101 run_hook 'pre-install'
1102 each_selected_installers {|inst| inst.exec_install }
1103 run_hook 'post-install'
1104 end
1105
1106 def exec_test
1107 run_hook 'pre-test'
1108 each_selected_installers {|inst| inst.exec_test }
1109 run_hook 'post-test'
1110 end
1111
1112 def exec_clean
1113 rm_f @config.savefile
1114 run_hook 'pre-clean'
1115 each_selected_installers {|inst| inst.exec_clean }
1116 run_hook 'post-clean'
1117 end
1118
1119 def exec_distclean
1120 rm_f @config.savefile
1121 run_hook 'pre-distclean'
1122 each_selected_installers {|inst| inst.exec_distclean }
1123 run_hook 'post-distclean'
1124 end
1125
1126 #
1127 # lib
1128 #
1129
1130 def each_selected_installers
1131 Dir.mkdir 'packages' unless File.dir?('packages')
1132 @selected.each do |pack|
1133 $stderr.puts "Processing the package `#{pack}' ..." if verbose?
1134 Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
1135 Dir.chdir "packages/#{pack}"
1136 yield @installers[pack]
1137 Dir.chdir '../..'
1138 end
1139 end
1140
1141 def run_hook(id)
1142 @root_installer.run_hook id
1143 end
1144
1145 # module FileOperations requires this
1146 def verbose?
1147 @config.verbose?
1148 end
1149
1150 # module FileOperations requires this
1151 def no_harm?
1152 @config.no_harm?
1153 end
1154
1155 end # class ToplevelInstallerMulti
1156
1157
1158 class Installer
1159
1160 FILETYPES = %w( bin lib ext data conf man )
1161
1162 include FileOperations
1163 include HookScriptAPI
1164
1165 def initialize(config, srcroot, objroot)
1166 @config = config
1167 @srcdir = File.expand_path(srcroot)
1168 @objdir = File.expand_path(objroot)
1169 @currdir = '.'
1170 end
1171
1172 def inspect
1173 "#<#{self.class} #{File.basename(@srcdir)}>"
1174 end
1175
1176 def noop(rel)
1177 end
1178
1179 #
1180 # Hook Script API base methods
1181 #
1182
1183 def srcdir_root
1184 @srcdir
1185 end
1186
1187 def objdir_root
1188 @objdir
1189 end
1190
1191 def relpath
1192 @currdir
1193 end
1194
1195 #
1196 # Config Access
1197 #
1198
1199 # module FileOperations requires this
1200 def verbose?
1201 @config.verbose?
1202 end
1203
1204 # module FileOperations requires this
1205 def no_harm?
1206 @config.no_harm?
1207 end
1208
1209 def verbose_off
1210 begin
1211 save, @config.verbose = @config.verbose?, false
1212 yield
1213 ensure
1214 @config.verbose = save
1215 end
1216 end
1217
1218 #
1219 # TASK config
1220 #
1221
1222 def exec_config
1223 exec_task_traverse 'config'
1224 end
1225
1226 alias config_dir_bin noop
1227 alias config_dir_lib noop
1228
1229 def config_dir_ext(rel)
1230 extconf if extdir?(curr_srcdir())
1231 end
1232
1233 alias config_dir_data noop
1234 alias config_dir_conf noop
1235 alias config_dir_man noop
1236
1237 def extconf
1238 ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt
1239 end
1240
1241 #
1242 # TASK setup
1243 #
1244
1245 def exec_setup
1246 exec_task_traverse 'setup'
1247 end
1248
1249 def setup_dir_bin(rel)
1250 files_of(curr_srcdir()).each do |fname|
1251 update_shebang_line "#{curr_srcdir()}/#{fname}"
1252 end
1253 end
1254
1255 alias setup_dir_lib noop
1256
1257 def setup_dir_ext(rel)
1258 make if extdir?(curr_srcdir())
1259 end
1260
1261 alias setup_dir_data noop
1262 alias setup_dir_conf noop
1263 alias setup_dir_man noop
1264
1265 def update_shebang_line(path)
1266 return if no_harm?
1267 return if config('shebang') == 'never'
1268 old = Shebang.load(path)
1269 if old
1270 $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1
1271 new = new_shebang(old)
1272 return if new.to_s == old.to_s
1273 else
1274 return unless config('shebang') == 'all'
1275 new = Shebang.new(config('rubypath'))
1276 end
1277 $stderr.puts "updating shebang: #{File.basename(path)}" if verbose?
1278 open_atomic_writer(path) {|output|
1279 File.open(path, 'rb') {|f|
1280 f.gets if old # discard
1281 output.puts new.to_s
1282 output.print f.read
1283 }
1284 }
1285 end
1286
1287 def new_shebang(old)
1288 if /\Aruby/ =~ File.basename(old.cmd)
1289 Shebang.new(config('rubypath'), old.args)
1290 elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby'
1291 Shebang.new(config('rubypath'), old.args[1..-1])
1292 else
1293 return old unless config('shebang') == 'all'
1294 Shebang.new(config('rubypath'))
1295 end
1296 end
1297
1298 def open_atomic_writer(path, &block)
1299 tmpfile = File.basename(path) + '.tmp'
1300 begin
1301 File.open(tmpfile, 'wb', &block)
1302 File.rename tmpfile, File.basename(path)
1303 ensure
1304 File.unlink tmpfile if File.exist?(tmpfile)
1305 end
1306 end
1307
1308 class Shebang
1309 def Shebang.load(path)
1310 line = nil
1311 File.open(path) {|f|
1312 line = f.gets
1313 }
1314 return nil unless /\A#!/ =~ line
1315 parse(line)
1316 end
1317
1318 def Shebang.parse(line)
1319 cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ')
1320 new(cmd, args)
1321 end
1322
1323 def initialize(cmd, args = [])
1324 @cmd = cmd
1325 @args = args
1326 end
1327
1328 attr_reader :cmd
1329 attr_reader :args
1330
1331 def to_s
1332 "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}")
1333 end
1334 end
1335
1336 #
1337 # TASK install
1338 #
1339
1340 def exec_install
1341 rm_f 'InstalledFiles'
1342 exec_task_traverse 'install'
1343 end
1344
1345 def install_dir_bin(rel)
1346 install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755
1347 end
1348
1349 def install_dir_lib(rel)
1350 install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644
1351 end
1352
1353 def install_dir_ext(rel)
1354 return unless extdir?(curr_srcdir())
1355 install_files rubyextentions('.'),
1356 "#{config('sodir')}/#{File.dirname(rel)}",
1357 0555
1358 end
1359
1360 def install_dir_data(rel)
1361 install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644
1362 end
1363
1364 def install_dir_conf(rel)
1365 # FIXME: should not remove current config files
1366 # (rename previous file to .old/.org)
1367 install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644
1368 end
1369
1370 def install_dir_man(rel)
1371 install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644
1372 end
1373
1374 def install_files(list, dest, mode)
1375 mkdir_p dest, @config.install_prefix
1376 list.each do |fname|
1377 install fname, dest, mode, @config.install_prefix
1378 end
1379 end
1380
1381 def libfiles
1382 glob_reject(%w(*.y *.output), targetfiles())
1383 end
1384
1385 def rubyextentions(dir)
1386 ents = glob_select("*.#{@config.dllext}", targetfiles())
1387 if ents.empty?
1388 setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first"
1389 end
1390 ents
1391 end
1392
1393 def targetfiles
1394 mapdir(existfiles() - hookfiles())
1395 end
1396
1397 def mapdir(ents)
1398 ents.map {|ent|
1399 if File.exist?(ent)
1400 then ent # objdir
1401 else "#{curr_srcdir()}/#{ent}" # srcdir
1402 end
1403 }
1404 end
1405
1406 # picked up many entries from cvs-1.11.1/src/ignore.c
1407 JUNK_FILES = %w(
1408 core RCSLOG tags TAGS .make.state
1409 .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
1410 *~ *.old *.bak *.BAK *.orig *.rej _$* *$
1411
1412 *.org *.in .*
1413 )
1414
1415 def existfiles
1416 glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.')))
1417 end
1418
1419 def hookfiles
1420 %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
1421 %w( config setup install clean ).map {|t| sprintf(fmt, t) }
1422 }.flatten
1423 end
1424
1425 def glob_select(pat, ents)
1426 re = globs2re([pat])
1427 ents.select {|ent| re =~ ent }
1428 end
1429
1430 def glob_reject(pats, ents)
1431 re = globs2re(pats)
1432 ents.reject {|ent| re =~ ent }
1433 end
1434
1435 GLOB2REGEX = {
1436 '.' => '\.',
1437 '$' => '\$',
1438 '#' => '\#',
1439 '*' => '.*'
1440 }
1441
1442 def globs2re(pats)
1443 /\A(?:#{
1444 pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|')
1445 })\z/
1446 end
1447
1448 #
1449 # TASK test
1450 #
1451
1452 TESTDIR = 'test'
1453
1454 def exec_test
1455 unless File.directory?('test')
1456 $stderr.puts 'no test in this package' if verbose?
1457 return
1458 end
1459 $stderr.puts 'Running tests...' if verbose?
1460 begin
1461 require 'test/unit'
1462 rescue LoadError
1463 setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.'
1464 end
1465 runner = Test::Unit::AutoRunner.new(true)
1466 runner.to_run << TESTDIR
1467 runner.run
1468 end
1469
1470 #
1471 # TASK clean
1472 #
1473
1474 def exec_clean
1475 exec_task_traverse 'clean'
1476 rm_f @config.savefile
1477 rm_f 'InstalledFiles'
1478 end
1479
1480 alias clean_dir_bin noop
1481 alias clean_dir_lib noop
1482 alias clean_dir_data noop
1483 alias clean_dir_conf noop
1484 alias clean_dir_man noop
1485
1486 def clean_dir_ext(rel)
1487 return unless extdir?(curr_srcdir())
1488 make 'clean' if File.file?('Makefile')
1489 end
1490
1491 #
1492 # TASK distclean
1493 #
1494
1495 def exec_distclean
1496 exec_task_traverse 'distclean'
1497 rm_f @config.savefile
1498 rm_f 'InstalledFiles'
1499 end
1500
1501 alias distclean_dir_bin noop
1502 alias distclean_dir_lib noop
1503
1504 def distclean_dir_ext(rel)
1505 return unless extdir?(curr_srcdir())
1506 make 'distclean' if File.file?('Makefile')
1507 end
1508
1509 alias distclean_dir_data noop
1510 alias distclean_dir_conf noop
1511 alias distclean_dir_man noop
1512
1513 #
1514 # Traversing
1515 #
1516
1517 def exec_task_traverse(task)
1518 run_hook "pre-#{task}"
1519 FILETYPES.each do |type|
1520 if type == 'ext' and config('without-ext') == 'yes'
1521 $stderr.puts 'skipping ext/* by user option' if verbose?
1522 next
1523 end
1524 traverse task, type, "#{task}_dir_#{type}"
1525 end
1526 run_hook "post-#{task}"
1527 end
1528
1529 def traverse(task, rel, mid)
1530 dive_into(rel) {
1531 run_hook "pre-#{task}"
1532 __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
1533 directories_of(curr_srcdir()).each do |d|
1534 traverse task, "#{rel}/#{d}", mid
1535 end
1536 run_hook "post-#{task}"
1537 }
1538 end
1539
1540 def dive_into(rel)
1541 return unless File.dir?("#{@srcdir}/#{rel}")
1542
1543 dir = File.basename(rel)
1544 Dir.mkdir dir unless File.dir?(dir)
1545 prevdir = Dir.pwd
1546 Dir.chdir dir
1547 $stderr.puts '---> ' + rel if verbose?
1548 @currdir = rel
1549 yield
1550 Dir.chdir prevdir
1551 $stderr.puts '<--- ' + rel if verbose?
1552 @currdir = File.dirname(rel)
1553 end
1554
1555 def run_hook(id)
1556 path = [ "#{curr_srcdir()}/#{id}",
1557 "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) }
1558 return unless path
1559 begin
1560 instance_eval File.read(path), path, 1
1561 rescue
1562 raise if $DEBUG
1563 setup_rb_error "hook #{path} failed:\n" + $!.message
1564 end
1565 end
1566
1567 end # class Installer
1568
1569
1570 class SetupError < StandardError; end
1571
1572 def setup_rb_error(msg)
1573 raise SetupError, msg
1574 end
1575
1576 if $0 == __FILE__
1577 begin
1578 ToplevelInstaller.invoke
1579 rescue SetupError
1580 raise if $DEBUG
1581 $stderr.puts $!.message
1582 $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
1583 exit 1
1584 end
1585 end
0 #!/usr/bin/ruby -w
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
4 require 'test/unit'
5 require 'feed2imap/cache'
6 require 'feedparser'
7 require 'pp'
8
9 class ItemCacheTest < Test::Unit::TestCase
10 def test_create
11 cache = ItemCache::new
12 assert(! cache.nil?)
13 end
14
15 def test_cache_lastcheck
16 cache = ItemCache::new
17 assert_equal(Time::at(0), cache.get_last_check('coucou'))
18 t = Time::now
19 cache.set_last_check('coucou', t)
20 assert_equal(t, cache.get_last_check('coucou'))
21 end
22
23 def test_cache_management
24 c = ItemCache::new
25 assert_equal(0, c.nbchannels)
26 assert_equal(0, c.nbitems)
27 i1 = FeedParser::FeedItem::new
28 i1.title = 'title1'
29 i1.link = 'link1'
30 i1.content = 'content1'
31 i2 = FeedParser::FeedItem::new
32 i2.title = 'title2'
33 i2.link = 'link2'
34 i2.content = 'content2'
35 i3 = FeedParser::FeedItem::new
36 i3.title = 'title3'
37 i3.link = 'link3'
38 i3.content = 'content3'
39 assert_equal([i1, i2], c.get_new_items('id', [i1, i2])[0])
40 c.commit_cache('id')
41 assert_equal(2, c.nbitems)
42 assert_equal([i3], c.get_new_items('id', [i2, i3])[0])
43 end
44
45 def test_cache_management_updated
46 c = ItemCache::new
47 assert_equal(0, c.nbchannels)
48 assert_equal(0, c.nbitems)
49 i1 = FeedParser::FeedItem::new
50 i1.title = 'title1'
51 i1.link = 'link1'
52 i1.content = 'content1'
53 i2 = FeedParser::FeedItem::new
54 i2.title = 'title2'
55 i2.link = 'link2'
56 i2.content = 'content2'
57 news = c.get_new_items('id', [i1, i2])[0]
58 assert_equal([i1, i2], news)
59 idx1 = i1.cacheditem.index
60 assert_equal(0, idx1)
61 idx2 = i2.cacheditem.index
62 assert_equal(1, idx2)
63 c.commit_cache('id')
64 i3 = FeedParser::FeedItem::new
65 i3.title = 'title 1 - updated'
66 i3.link = 'link1'
67 i3.content = 'content1'
68 news, updated = c.get_new_items('id', [i3])
69 assert_equal([], news)
70 assert_equal([i3], updated)
71 assert_equal(idx1, i3.cacheditem.index)
72 i4 = FeedParser::FeedItem::new
73 i4.title = 'title 1 - updated'
74 i4.link = 'link1'
75 i4.content = 'content1 - modified'
76 news, updated = c.get_new_items('id', [i4])
77 assert_equal([], news)
78 assert_equal([i4], updated)
79 assert_equal(idx1, i4.cacheditem.index)
80 end
81 end
0 #!/usr/bin/ruby -w
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
4 require 'test/unit'
5 require 'feed2imap/config'
6 require 'stringio'
7
8 CONF1 = <<EOF
9 cache: /home/lucas/.feed2imap_cachedatabase
10 feeds:
11 - name: feed1
12 url: http://something
13 target: imap://login:pasword@ezaezae/Feeds/A
14 - name: feed2
15 url: http://something2
16 target: imap://login:pasword@ezaezae/Feeds/B
17 EOF
18 CONF2 = <<EOF
19 feeds:
20 - name: feed1
21 url: http://something
22 target: imap://login:pasword@ezaezae/Feeds/A
23 - name: feed2
24 url: http://something2
25 target: imaps://login:pasword@ezaezae/Feeds/B
26 EOF
27 CONFFEED = <<EOF
28 feeds:
29 - name: feed1
30 url: feed:http://something
31 target: imap://login:pasword@ezaezae/Feeds/A
32 - name: feed2
33 url: http://something2
34 target: imaps://login:pasword@ezaezae/Feeds/B
35 EOF
36
37 class ConfigTest < Test::Unit::TestCase
38 def test_cache
39 sio = StringIO::new CONF1
40 conf = F2IConfig::new(sio)
41 assert_equal('/home/lucas/.feed2imap_cachedatabase', conf.cache)
42 # testing default value
43 sio = StringIO::new CONF2
44 conf = F2IConfig::new(sio)
45 assert_equal(ENV['HOME'] + '/.feed2imap.cache', conf.cache)
46 end
47
48 def test_accounts
49 sio = StringIO::new CONF1
50 conf = F2IConfig::new(sio)
51 assert_equal(1, conf.imap_accounts.length)
52 sio = StringIO::new CONF2
53 conf = F2IConfig::new(sio)
54 assert_equal(2, conf.imap_accounts.length)
55 end
56
57 def test_feedurls
58 sio = StringIO::new CONFFEED
59 conf = F2IConfig::new(sio)
60 assert_equal('http://something', conf.feeds[0].url)
61 assert_equal('http://something2', conf.feeds[1].url)
62 end
63 end
0 #!/usr/bin/ruby
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
4 require 'test/unit'
5 require 'feed2imap/httpfetcher'
6
7 class HttpFetcherTest < Test::Unit::TestCase
8 def test_get_https
9 s = ''
10 assert_nothing_raised do
11 s = HTTPFetcher::fetch('https://linuxfr.org/pub/', Time::at(0))
12 end
13 assert(s.length > 20)
14 end
15
16 def test_get_http
17
18 end
19
20 def test_get_httpnotmodif
21 s = 'aaa'
22 assert_nothing_raised do
23 s = HTTPFetcher::fetch('http://www.lucas-nussbaum.net/feed2imap_tests/notmodified.php', Time::new())
24 end
25 assert_nil(s)
26 end
27
28 def test_get_redir1
29 s = 'aaa'
30 assert_nothing_raised do
31 s = HTTPFetcher::fetch("http://www.lucas-nussbaum.net/feed2imap_tests/redir.php?redir=#{MAXREDIR}", Time::at(0))
32 end
33 assert_equal('OK', s)
34 end
35
36 def test_get_redir2
37 s = ''
38 assert_raise(RuntimeError) do
39 s = HTTPFetcher::fetch("http://www.lucas-nussbaum.net/feed2imap_tests/redir.php?redir=#{MAXREDIR + 1}", Time::at(0))
40 end
41 end
42
43 def test_httpauth
44 s = ''
45 assert_nothing_raised do
46 s = HTTPFetcher::fetch("http://aaa:bbb@ensilinx1.imag.fr/~lucas/f2i_redirauth.php", Time::at(0))
47 end
48 assert_equal("Login: aaa / Password: bbb \n", s)
49 end
50
51 def test_redirauth
52 s = ''
53 assert_nothing_raised do
54 s = HTTPFetcher::fetch("http://aaa:bbb@ensilinx1.imag.fr/~lucas/f2i_redirauth.php?redir=1", Time::at(0))
55 end
56 assert_equal("Login: aaa / Password: bbb \n", s)
57 end
58
59 def test_notfound
60 s = ''
61 assert_raises(RuntimeError) do
62 s = HTTPFetcher::fetch("http://ensilinx1.imag.fr/~lucas/notfound.html", Time::at(0))
63 end
64 end
65 end
0 #!/usr/bin/ruby -w
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
4 require 'test/unit'
5 require 'rmail'
6
7 class MailTest < Test::Unit::TestCase
8 def test_require_rmail
9 # let's just test Rubymail is loaded
10 m = RMail::Message::new
11 assert_equal(m.class, RMail::Message)
12 end
13 end
0 #!/usr/bin/ruby -w
1
2 $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3 $:.unshift File.join(File.dirname(__FILE__), '..', 'test')
4 $:.unshift File.join(File.dirname(__FILE__), 'lib')
5 $:.unshift File.join(File.dirname(__FILE__), 'test')
6
7 require 'feed2imap'
8 require 'tc_cache'
9 require 'tc_config'
10 require 'tc_mail'
11 require 'tc_httpfetcher'