Imported Upstream version 1.1
Antonio Terceiro
10 years ago
0 | 0 | require 'rake/testtask' |
1 | require 'rake/rdoctask' | |
1 | require 'rdoc/task' | |
2 | 2 | require 'rake/packagetask' |
3 | 3 | require 'rake' |
4 | 4 | require 'find' |
5 | 5 | |
6 | task :default => [:package] | |
6 | task :default => [:test] | |
7 | 7 | |
8 | 8 | PKG_NAME = 'feed2imap' |
9 | PKG_VERSION = '1.0' | |
9 | PKG_VERSION = '1.1' | |
10 | 10 | PKG_FILES = [ 'ChangeLog', 'README', 'COPYING', 'setup.rb', 'Rakefile'] |
11 | 11 | Find.find('bin/', 'lib/', 'test/', 'data/') do |f| |
12 | 12 | if FileTest.directory?(f) and f =~ /\.svn/ |
18 | 18 | Rake::TestTask.new do |t| |
19 | 19 | t.libs << "libs/feed2imap" |
20 | 20 | t.libs << "test" |
21 | t.test_files = FileList['test/tc_*.rb'] | |
21 | t.test_files = FileList['test/tc_*.rb'] - ['test/tc_httpfetcher.rb'] | |
22 | 22 | end |
23 | 23 | |
24 | Rake::RDocTask.new do |rd| | |
24 | RDoc::Task.new do |rd| | |
25 | 25 | rd.main = 'README' |
26 | 26 | rd.rdoc_files.include('lib/*.rb', 'lib/feed2imap/*.rb') |
27 | 27 | rd.options << '--all' |
40 | 40 | |
41 | 41 | # "Gem" part of the Rakefile |
42 | 42 | begin |
43 | require 'rake/gempackagetask' | |
43 | require 'rubygems/package_task' | |
44 | 44 | |
45 | 45 | spec = Gem::Specification.new do |s| |
46 | 46 | s.platform = Gem::Platform::RUBY |
49 | 49 | s.version = PKG_VERSION |
50 | 50 | s.requirements << 'feedparser' |
51 | 51 | s.require_path = 'lib' |
52 | s.executables = PKG_FILES.grep(%r{\Abin\/.}).map { |bin| | |
53 | bin.gsub(%r{\Abin/}, '') | |
54 | } | |
52 | 55 | s.files = PKG_FILES |
53 | 56 | s.description = "RSS/Atom feed aggregator" |
57 | s.authors = ['Lucas Nussbaum'] | |
54 | 58 | end |
55 | 59 | |
56 | Rake::GemPackageTask.new(spec) do |pkg| | |
60 | Gem::PackageTask.new(spec) do |pkg| | |
57 | 61 | pkg.need_zip = true |
58 | 62 | pkg.need_tar = true |
59 | 63 | end |
6 | 6 | # debug-updated: (for debugging purposes) if true, display a lot of information |
7 | 7 | # about the "updated-items" algorithm. |
8 | 8 | # include-images: download images and include them in the mail? (true/false) |
9 | # reupload-if-updated: when an item is updated, and was previously deleted, | |
10 | # reupload it? (true/false, default true) | |
9 | 11 | # default-email: default email address in the format foo@example.com |
10 | 12 | # disable-ssl-verification: disable SSL certification when connecting |
11 | 13 | # to IMAPS accounts (true/false) |
14 | # timeout: time before getting timeout when fetching feeds (default 30) in seconds | |
12 | 15 | # |
13 | 16 | # Per-feed options: |
14 | 17 | # name: name of the feed (must be unique) |
19 | 22 | # feed will be fetched |
20 | 23 | # disable: if set to something, the feed will be ignored |
21 | 24 | # include-images: download images and include them in the mail? (true/false) |
25 | # reupload-if-updated: when an item is updated, and was previously deleted, | |
26 | # reupload it? (true/false, default true) | |
22 | 27 | # always-new: feed2imap tries to use a clever algorithm to determine whether |
23 | 28 | # an item is new or has been updated. It doesn't work well with some web apps |
24 | 29 | # like mediawiki. When this flag is enabled, all items which don't match |
62 | 67 | # - name: test2 |
63 | 68 | # target: [ *target, 'test2' ] |
64 | 69 | # ... |
70 | ||
71 | # vim: ft=yaml:sts=2:expandtab |
192 | 192 | @itemstemp.unshift(j) |
193 | 193 | break |
194 | 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 | |
195 | # If we didn't find exact match, try to check if we have an update | |
196 | if j.is_ancestor_of(i) | |
197 | i.cacheditem.index = j.index | |
198 | i.cacheditem.updated = true | |
199 | updateditems.push(i) | |
200 | found = true | |
201 | # let's put j in front of itemstemp | |
202 | @itemstemp.delete(j) | |
203 | @itemstemp.unshift(i.cacheditem) | |
204 | break | |
211 | 205 | end |
212 | 206 | end |
213 | 207 | next if found |
22 | 22 | require 'feed2imap/maildir' |
23 | 23 | require 'etc' |
24 | 24 | require 'socket' |
25 | require 'set' | |
25 | 26 | |
26 | 27 | # Default cache file |
27 | 28 | DEFCACHE = ENV['HOME'] + '/.feed2imap.cache' |
32 | 33 | |
33 | 34 | # Feed2imap configuration |
34 | 35 | class F2IConfig |
35 | attr_reader :imap_accounts, :cache, :feeds, :dumpdir, :updateddebug, :max_failures, :include_images, :default_email, :hostname | |
36 | attr_reader :imap_accounts, :cache, :feeds, :dumpdir, :updateddebug, :max_failures, :include_images, :default_email, :hostname, :reupload_if_updated, :parts, :timeout | |
36 | 37 | |
37 | 38 | # Load the configuration from the IO stream |
38 | 39 | # TODO should do some sanity check on the data read. |
43 | 44 | @conf['feeds'] ||= [] |
44 | 45 | @feeds = [] |
45 | 46 | @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') | |
47 | ||
48 | @updateddebug = false | |
49 | @updateddebug = @conf['debug-updated'] if @conf.has_key?('debug-updated') | |
50 | ||
51 | @parts = %w(text html) | |
52 | @parts = Array(@conf['parts']) if @conf.has_key?('parts') && !@conf['parts'].empty? | |
53 | @parts = Set.new(@parts) | |
54 | ||
55 | @include_images = true | |
56 | @include_images = @conf['include-images'] if @conf.has_key?('include-images') | |
57 | @parts << 'html' if @include_images && ! @parts.include?('html') | |
58 | ||
59 | @reupload_if_updated = true | |
60 | @reupload_if_updated = @conf['reupload-if-updated'] if @conf.has_key?('reupload-if-updated') | |
61 | ||
62 | @timeout = if @conf['timeout'] == nil then 30 else @conf['timeout'].to_i end | |
63 | ||
48 | 64 | @default_email = (@conf['default-email'] || "#{LOGNAME}@#{HOSTNAME}") |
49 | ImapAccount.no_ssl_verify = (@conf['disable-ssl-verification'] and @conf['disable-ssl-verification'] != 'false') | |
65 | ImapAccount.no_ssl_verify = (@conf.has_key?('disable-ssl-verification') and @conf['disable-ssl-verification'] == true) | |
50 | 66 | @hostname = HOSTNAME # FIXME: should this be configurable as well? |
51 | 67 | @imap_accounts = ImapAccounts::new |
52 | 68 | maildir_account = MaildirAccount::new |
54 | 70 | if f['disable'].nil? |
55 | 71 | uri = URI::parse(f['target'].to_s) |
56 | 72 | path = URI::unescape(uri.path) |
57 | path = path[1..-1] if path[0,1] == '/' | |
58 | 73 | if uri.scheme == 'maildir' |
59 | 74 | @feeds.push(ConfigFeed::new(f, maildir_account, path, self)) |
60 | 75 | else |
76 | # remove leading slash from IMAP mailbox names | |
77 | path = path[1..-1] if path[0,1] == '/' | |
61 | 78 | @feeds.push(ConfigFeed::new(f, @imap_accounts.add_account(uri), path, self)) |
62 | 79 | end |
63 | 80 | end |
93 | 110 | |
94 | 111 | # A configured feed. simple data container. |
95 | 112 | class ConfigFeed |
96 | attr_reader :name, :url, :imapaccount, :folder, :always_new, :execurl, :filter, :ignore_hash, :dumpdir, :wrapto, :include_images | |
113 | attr_reader :name, :url, :imapaccount, :folder, :always_new, :execurl, :filter, :ignore_hash, :dumpdir, :wrapto, :include_images, :reupload_if_updated | |
97 | 114 | attr_accessor :body |
98 | 115 | |
99 | 116 | def initialize(f, imapaccount, folder, f2iconfig) |
100 | 117 | @name = f['name'] |
101 | 118 | @url = f['url'] |
102 | 119 | @url.sub!(/^feed:/, '') if @url =~ /^feed:/ |
103 | @imapaccount, @folder = imapaccount, folder | |
120 | @imapaccount = imapaccount | |
121 | @folder = encode_utf7 folder | |
104 | 122 | @freq = f['min-frequency'] |
105 | @always_new = (f['always-new'] and f['always-new'] != 'false') | |
123 | ||
124 | @always_new = false | |
125 | @always_new = f['always-new'] if f.has_key?('always-new') | |
126 | ||
106 | 127 | @execurl = f['execurl'] |
107 | 128 | @filter = f['filter'] |
108 | @ignore_hash = f['ignore-hash'] || false | |
129 | ||
130 | @ignore_hash = false | |
131 | @ignore_hash = f['ignore-hash'] if f.has_key?('ignore-hash') | |
132 | ||
109 | 133 | @freq = @freq.to_i if @freq |
110 | 134 | @dumpdir = f['dumpdir'] || nil |
111 | 135 | @wrapto = if f['wrapto'] == nil then 72 else f['wrapto'].to_i end |
136 | ||
112 | 137 | @include_images = f2iconfig.include_images |
113 | if f['include-images'] | |
114 | @include_images = (f['include-images'] != 'false') | |
115 | end | |
138 | @include_images = f['include-images'] if f.has_key?('include-images') | |
139 | ||
140 | @reupload_if_updated = f2iconfig.reupload_if_updated | |
141 | @reupload_if_updated = f['reupload-if-updated'] if f.has_key?('reupload-if-updated') | |
142 | ||
116 | 143 | end |
117 | 144 | |
118 | 145 | def needfetch(lastcheck) |
119 | 146 | return true if @freq.nil? |
120 | 147 | return (lastcheck + @freq * 3600) < Time::now |
121 | 148 | end |
149 | ||
150 | def encode_utf7(s) | |
151 | if "foo".respond_to?(:force_encoding) | |
152 | return Net::IMAP::encode_utf7 s | |
153 | else | |
154 | # this is a copy of the Net::IMAP::encode_utf7 w/o the force_encoding | |
155 | return s.gsub(/(&)|([^\x20-\x7e]+)/u) { | |
156 | if $1 | |
157 | "&-" | |
158 | else | |
159 | base64 = [$&.unpack("U*").pack("n*")].pack("m") | |
160 | "&" + base64.delete("=\n").tr("/", ",") + "-" | |
161 | end } | |
162 | end | |
163 | end | |
122 | 164 | end |
120 | 120 | end |
121 | 121 | fetch_start = Time::now |
122 | 122 | if feed.url |
123 | s = HTTPFetcher::fetch(feed.url, @cache.get_last_check(feed.name)) | |
123 | fetcher = HTTPFetcher::new | |
124 | fetcher::timeout = @config.timeout | |
125 | s = fetcher::fetch(feed.url, @cache.get_last_check(feed.name)) | |
124 | 126 | elsif feed.execurl |
125 | 127 | # avoid running more than one command at the same time. |
126 | 128 | # We need it because the called command might not be |
220 | 222 | next |
221 | 223 | end |
222 | 224 | begin |
223 | feed = FeedParser::Feed::new(f.body) | |
225 | feed = FeedParser::Feed::new(f.body.force_encoding('UTF-8')) | |
224 | 226 | rescue Exception |
225 | 227 | n = @cache.parse_failed(f.name) |
226 | 228 | m = "Error while parsing #{f.name}: #{$!} (failed #{n} times)" |
246 | 248 | id = "<#{fn}-#{i.cacheditem.index}@#{@config.hostname}>" |
247 | 249 | email = item_to_mail(@config, i, id, true, f.name, f.include_images, f.wrapto) |
248 | 250 | f.imapaccount.updatemail(f.folder, email, |
249 | id, i.date || Time::new) | |
251 | id, i.date || Time::new, f.reupload_if_updated) | |
250 | 252 | end |
251 | 253 | # reverse is needed to upload older items first (fixes gna#8986) |
252 | 254 | newitems.reverse.each do |i| |
16 | 16 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
17 | 17 | =end |
18 | 18 | |
19 | require 'zlib' | |
19 | 20 | require 'net/http' |
20 | 21 | # get openssl if available |
21 | 22 | begin |
32 | 33 | |
33 | 34 | # Class used to retrieve the feed over HTTP |
34 | 35 | class HTTPFetcher |
35 | def HTTPFetcher::fetcher(baseuri, uri, lastcheck, recursion) | |
36 | ||
37 | @timeout = 30 # should be enough for everybody... | |
38 | ||
39 | def timeout=(value) | |
40 | @timeout = value | |
41 | end | |
42 | ||
43 | def fetcher(baseuri, uri, lastcheck, recursion) | |
36 | 44 | proxy_host = nil |
37 | 45 | proxy_port = nil |
38 | 46 | proxy_user = nil |
48 | 56 | proxy_port, |
49 | 57 | proxy_user, |
50 | 58 | proxy_pass ).new(uri.host, uri.port) |
51 | http.read_timeout = 30 # should be enough for everybody... | |
52 | http.open_timeout = 30 | |
59 | http.read_timeout = @timeout | |
60 | http.open_timeout = @timeout | |
53 | 61 | if uri.scheme == 'https' |
54 | 62 | http.use_ssl = true |
55 | 63 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE |
60 | 68 | useragent = 'Feed2Imap http://home.gna.org/feed2imap/' |
61 | 69 | end |
62 | 70 | |
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}) | |
71 | headers = { | |
72 | 'User-Agent' => useragent, | |
73 | 'Accept-Encoding' => 'gzip', | |
74 | } | |
75 | if lastcheck != Time::at(0) | |
76 | headers.merge!('If-Modified-Since' => lastcheck.httpdate) | |
67 | 77 | end |
78 | req = Net::HTTP::Get::new(uri.request_uri, headers) | |
68 | 79 | if uri.userinfo |
69 | 80 | login, pw = uri.userinfo.split(':') |
70 | 81 | req.basic_auth(login, pw) |
80 | 91 | end |
81 | 92 | case response |
82 | 93 | when Net::HTTPSuccess |
83 | return response.body | |
94 | case response['Content-Encoding'] | |
95 | when 'gzip' | |
96 | return Zlib::GzipReader.new(StringIO.new(response.body)).read | |
97 | else | |
98 | return response.body | |
99 | end | |
84 | 100 | when Net::HTTPRedirection |
85 | 101 | # if not modified |
86 | 102 | if Net::HTTPNotModified === response |
98 | 114 | end |
99 | 115 | end |
100 | 116 | |
101 | def HTTPFetcher::fetch(url, lastcheck) | |
117 | def fetch(url, lastcheck) | |
102 | 118 | uri = URI::parse(url) |
103 | return HTTPFetcher::fetcher(uri, uri, lastcheck, MAXREDIR) | |
119 | return fetcher(uri, uri, lastcheck, MAXREDIR) | |
104 | 120 | end |
105 | 121 | end |
107 | 107 | end |
108 | 108 | |
109 | 109 | # update a mail |
110 | def updatemail(folder, mail, id, date = Time::now) | |
110 | def updatemail(folder, mail, id, date = Time::now, reupload_if_updated = true) | |
111 | 111 | create_folder_if_not_exists(folder) |
112 | 112 | @connection.select(folder) |
113 | 113 | searchres = @connection.search(['HEADER', 'Message-Id', id]) |
118 | 118 | searchres.each { |m| @connection.store(m, "+FLAGS", [:Deleted]) } |
119 | 119 | @connection.expunge |
120 | 120 | flags -= [ :Recent ] # avoids errors with dovecot |
121 | elsif not reupload_if_updated | |
122 | # mail not present, and we don't want to re-upload it | |
123 | return | |
121 | 124 | end |
122 | 125 | @connection.append(folder, mail.gsub(/\n/, "\r\n"), flags, date) |
123 | 126 | end |
140 | 143 | d = f[0].attr['INTERNALDATE'] |
141 | 144 | s = f[0].attr['ENVELOPE'].subject |
142 | 145 | if s =~ /^=\?utf-8\?b\?/ |
143 | s = Base64::decode64(s.gsub(/^=\?utf-8\?b\?(.*)\?=$/, '\1')).toISO_8859_1('utf-8') | |
146 | s = Base64::decode64(s.gsub(/^=\?utf-8\?b\?(.*)\?=$/, '\1')).force_encoding('utf-8') | |
147 | elsif s =~ /^=\?iso-8859-1\?b\?/ | |
148 | s = Base64::decode64(s.gsub(/^=\?iso-8859-1\?b\?(.*)\?=$/, '\1')).force_encoding('iso-8859-1').encode('utf-8') | |
144 | 149 | end |
145 | 150 | if dryrun |
146 | 151 | puts "To remove: #{s} (#{d})" |
76 | 76 | message.header['Subject'] = subj |
77 | 77 | end |
78 | 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 | |
79 | textpart = htmlpart = nil | |
80 | parts = config.parts | |
81 | if parts.include?('text') | |
82 | textpart = parts.size == 1 ? message : RMail::Message::new | |
83 | textpart.header['Content-Type'] = 'text/plain; charset=utf-8; format=flowed' | |
84 | textpart.header['Content-Transfer-Encoding'] = '8bit' | |
85 | textpart.body = item.to_text(true, wrapto, false) | |
86 | end | |
87 | if parts.include?('html') | |
88 | htmlpart = parts.size == 1 ? message : RMail::Message::new | |
89 | htmlpart.header['Content-Type'] = 'text/html; charset=utf-8' | |
90 | htmlpart.header['Content-Transfer-Encoding'] = '8bit' | |
91 | htmlpart.body = item.to_html | |
92 | end | |
87 | 93 | |
88 | 94 | # inline images as attachments |
89 | 95 | imgs = [] |
126 | 132 | imgs.each do |i| |
127 | 133 | message.add_part(i) |
128 | 134 | end |
129 | else | |
135 | elsif parts.size != 1 | |
130 | 136 | message.header['Content-Type'] = 'multipart/alternative' |
131 | 137 | message.add_part(textpart) |
132 | 138 | message.add_part(htmlpart) |
18 | 18 | require 'uri' |
19 | 19 | require 'fileutils' |
20 | 20 | require 'fcntl' |
21 | require 'rmail' | |
22 | require 'socket' | |
21 | 23 | |
22 | 24 | class MaildirAccount |
23 | 25 | MYHOSTNAME = Socket.gethostname |
26 | ||
27 | @@seq_num = 0 | |
24 | 28 | |
25 | 29 | attr_reader :uri |
26 | 30 | |
30 | 34 | end |
31 | 35 | end |
32 | 36 | |
33 | def updatemail(folder, mail, idx, date = Time::now) | |
37 | def updatemail(folder, mail, idx, date = Time::now, reupload_if_updated = true) | |
34 | 38 | dir = folder_dir(folder) |
35 | 39 | guarantee_maildir(dir) |
36 | 40 | mail_files = find_mails(dir, idx) |
39 | 43 | # get the info from the first result and delete everything |
40 | 44 | info = maildir_file_info(mail_files[0]) |
41 | 45 | mail_files.each { |f| File.delete(File.join(dir, f)) } |
46 | elsif not reupload_if_updated | |
47 | # mail not present, and we don't want to re-upload it | |
48 | return | |
42 | 49 | end |
43 | 50 | store_message(dir, date, info) { |f| f.puts(mail) } |
44 | 51 | end |
61 | 68 | next if (not flags.index('S') or |
62 | 69 | flags.index('F') or |
63 | 70 | mtime > recent_time) |
64 | File.open(fn) do |f| | |
65 | mail = RMail::Parser.read(f) | |
71 | mail = File.open(fn) do |f| | |
72 | RMail::Parser.read(f) | |
66 | 73 | end |
74 | subject = mail.header['Subject'] | |
67 | 75 | if dryrun |
68 | 76 | puts "To remove: #{subject} #{mtime}" |
69 | 77 | else |
83 | 91 | end |
84 | 92 | |
85 | 93 | def store_message(dir, date, info, &block) |
86 | # TODO: handle `date' | |
87 | 94 | |
88 | 95 | guarantee_maildir(dir) |
89 | 96 | |
92 | 99 | timer = 30 |
93 | 100 | fd = nil |
94 | 101 | while timer >= 0 |
95 | new_fn = new_maildir_basefn | |
102 | new_fn = new_maildir_basefn(date) | |
96 | 103 | tmp_path = File.join(dir, 'tmp', new_fn) |
97 | 104 | new_path = File.join(dir, 'new', new_fn) |
98 | 105 | begin |
136 | 143 | Dir[File.join(subdir, '*')].each do |fn| |
137 | 144 | File.open(fn) do |f| |
138 | 145 | 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))) | |
146 | cache_index = mail.header['Message-ID'] | |
147 | if cache_index && (cache_index == idx || cache_index == "<#{idx}>") | |
148 | dir_paths.push(File.join(d, File.basename(fn))) | |
149 | end | |
142 | 150 | end |
143 | 151 | end |
144 | 152 | end |
156 | 164 | basename = File.basename(file) |
157 | 165 | colon = basename.rindex(':') |
158 | 166 | |
159 | return (colon and basename.slice(colon + 1, -1)) | |
167 | return (colon and basename[colon + 1 .. -1]) | |
160 | 168 | end |
161 | 169 | |
162 | # Shamelessly taken from | |
170 | # Re-written and no longer shamelessly taken from | |
163 | 171 | # 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}" | |
172 | def new_maildir_basefn(date) | |
173 | fn = "#{date.to_i.to_s}.#{@@seq_num.to_s}.#{MYHOSTNAME}" | |
174 | @@seq_num += 1 | |
175 | fn | |
167 | 176 | end |
177 | ||
178 | def maildir_file_info_flags(fn) | |
179 | parts = fn.split(',') | |
180 | if parts.size == 1 | |
181 | '' | |
182 | else | |
183 | parts.last | |
184 | end | |
185 | end | |
186 | ||
168 | 187 | end |
169 | 188 |
25 | 25 | module REXML |
26 | 26 | module Encoding |
27 | 27 | def decode(str) |
28 | return str.toUTF8(@encoding) | |
28 | return str.encode(@encoding) | |
29 | 29 | end |
30 | 30 | |
31 | 31 | def encode(str) |
0 | Date: Mon, 12 Aug 2013 16:25:20 +0200 | |
1 | From: Antonio Terceiro <terceiro@debian.org> | |
2 | To: terceiro@debian.org | |
3 | Subject: UTF-8 data: =?iso-8859-1?B?4ent8/o=?= | |
4 | Message-ID: <regular-message-id@debian.org> | |
5 | MIME-Version: 1.0 | |
6 | Content-Type: text/plain; charset=us-ascii | |
7 | Content-Disposition: inline | |
8 | User-Agent: Mutt/1.5.21 (2010-09-15) | |
9 | ||
10 | This is a sample email |
0 | Date: Mon, 12 Aug 2013 16:52:17 +0200 | |
1 | From: Antonio Terceiro <terceiro@debian.org> | |
2 | To: terceiro@debian.org | |
3 | Subject: an unread message | |
4 | Message-ID: <unread-message-id@debian.org> | |
5 | MIME-Version: 1.0 | |
6 | Content-Type: text/plain; charset=us-ascii | |
7 | Content-Disposition: inline | |
8 | User-Agent: Mutt/1.5.21 (2010-09-15) | |
9 | ||
10 | This message was not read yet |
0 | Date: Mon, 12 Aug 2013 17:07:02 +0200 | |
1 | From: Antonio Terceiro <terceiro@debian.org> | |
2 | To: terceiro@debian.org | |
3 | Subject: a flagged message | |
4 | Message-ID: <flagged-message-id@debian.org> | |
5 | MIME-Version: 1.0 | |
6 | Content-Type: text/plain; charset=us-ascii | |
7 | Content-Disposition: inline | |
8 | User-Agent: Mutt/1.5.21 (2010-09-15) | |
9 | ||
10 | This message is flagged. |
0 | Date: Mon, 12 Aug 2013 17:08:19 +0200 | |
1 | From: Antonio Terceiro <terceiro@debian.org> | |
2 | To: terceiro@debian.org | |
3 | Subject: a new message | |
4 | Message-ID: <new-message-id@debian.org> | |
5 | MIME-Version: 1.0 | |
6 | Content-Type: text/plain; charset=us-ascii | |
7 | Content-Disposition: inline | |
8 | User-Agent: Mutt/1.5.21 (2010-09-15) | |
9 | ||
10 | This message is new |
33 | 33 | url: http://something2 |
34 | 34 | target: imaps://login:pasword@ezaezae/Feeds/B |
35 | 35 | EOF |
36 | CONFPARTS = <<EOF | |
37 | parts: text | |
38 | include-images: false | |
39 | feeds: | |
40 | - name: feed1 | |
41 | url: http://something | |
42 | target: imap://login:pasword@ezaezae/Feeds/A | |
43 | - name: feed2 | |
44 | url: http://something2 | |
45 | target: imap://login:pasword@ezaezae/Feeds/B | |
46 | EOF | |
36 | 47 | |
37 | 48 | class ConfigTest < Test::Unit::TestCase |
38 | 49 | def test_cache |
60 | 71 | assert_equal('http://something', conf.feeds[0].url) |
61 | 72 | assert_equal('http://something2', conf.feeds[1].url) |
62 | 73 | end |
74 | ||
75 | def test_parts | |
76 | sio = StringIO::new CONFPARTS | |
77 | conf = F2IConfig::new(sio) | |
78 | assert conf.parts.include?('text') | |
79 | assert ! conf.parts.include?('html') | |
80 | end | |
63 | 81 | 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 | require 'test/unit' | |
1 | require 'fileutils' | |
2 | require 'tmpdir' | |
3 | require 'mocha/setup' | |
4 | ||
5 | require 'feed2imap/maildir' | |
6 | ||
7 | class TestMaildir < Test::Unit::TestCase | |
8 | ||
9 | def setup | |
10 | @tmpdirs = [] | |
11 | end | |
12 | ||
13 | def tear_down | |
14 | @tmpdirs.each do |dir| | |
15 | FileUtils.rm_rf(dir) | |
16 | end | |
17 | end | |
18 | ||
19 | def test_cleanup | |
20 | folder = create_maildir | |
21 | msgs = message_count(folder) | |
22 | ||
23 | maildir_account.cleanup(folder) | |
24 | ||
25 | assert_equal msgs - 1, message_count(folder) | |
26 | end | |
27 | ||
28 | def test_putmail | |
29 | folder = create_maildir | |
30 | msgs = message_count(folder) | |
31 | ||
32 | mail = RMail::Message.new | |
33 | mail.header['Subject'] = 'a message I just created' | |
34 | mail.body = 'to test maildir' | |
35 | maildir_account.putmail(folder, mail) | |
36 | ||
37 | assert_equal msgs + 1, message_count(folder) | |
38 | end | |
39 | ||
40 | def test_updatemail | |
41 | folder = create_maildir | |
42 | path = maildir_account.send( | |
43 | :find_mails, | |
44 | folder, | |
45 | 'regular-message-id@debian.org' | |
46 | ).first | |
47 | assert_not_nil path | |
48 | mail = RMail::Message.new | |
49 | mail.header['Subject'] = 'a different subject' | |
50 | mail.header['Message-ID'] = 'regular-message-id@debian.org' | |
51 | mail.body = 'This is the body of the message' | |
52 | maildir_account.updatemail(folder, mail, 'regular-message-id@debian.org') | |
53 | ||
54 | updated_path = maildir_account.send( | |
55 | :find_mails, | |
56 | folder, | |
57 | 'regular-message-id@debian.org' | |
58 | ).first | |
59 | updated_mail = RMail::Parser.read(File.open(File.join(folder, updated_path))) | |
60 | ||
61 | assert_equal 'a different subject', updated_mail.header['Subject'] | |
62 | end | |
63 | ||
64 | def test_find_mails | |
65 | folder = create_maildir | |
66 | assert_equal 0, maildir_account.send(:find_mails, folder, 'SomeRandomMessageID').size | |
67 | end | |
68 | ||
69 | private | |
70 | ||
71 | def create_maildir | |
72 | parent = Dir.mktmpdir | |
73 | @tmpdirs << parent | |
74 | FileUtils.cp_r('test/maildir', parent) | |
75 | return File.join(parent, 'maildir') | |
76 | end | |
77 | ||
78 | def message_count(folder) | |
79 | Dir.glob(File.join(folder, '**', '*')).reject { |f| File.directory?(f) }.size | |
80 | end | |
81 | ||
82 | def maildir_account | |
83 | @maildir_account ||= | |
84 | begin | |
85 | MaildirAccount.new.tap do |account| | |
86 | account.stubs(:puts) | |
87 | end | |
88 | end | |
89 | end | |
90 | ||
91 | end | |
92 |