Imported Upstream version 1.0
Lucas Nussbaum
13 years ago
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' |