feed2email 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +1 -1
- data/README.md +48 -20
- data/TODO.md +13 -0
- data/bin/feed2email +1 -0
- data/bin/feed2email-migrate-history +29 -0
- data/lib/feed2email/config.rb +92 -17
- data/lib/feed2email/core_ext.rb +12 -0
- data/lib/feed2email/entry.rb +13 -8
- data/lib/feed2email/feed.rb +183 -84
- data/lib/feed2email/feed_data_file.rb +65 -0
- data/lib/feed2email/feed_history.rb +28 -0
- data/lib/feed2email/feed_meta.rb +28 -0
- data/lib/feed2email/feeds.rb +77 -0
- data/lib/feed2email/logger.rb +23 -22
- data/lib/feed2email/mail.rb +24 -47
- data/lib/feed2email/version.rb +1 -1
- data/lib/feed2email.rb +15 -14
- metadata +127 -27
- data/.gitignore +0 -4
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -39
- data/Rakefile +0 -1
- data/feed2email.gemspec +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9a92661dec1838e02af880c1e7c0e4c2a37dced
|
4
|
+
data.tar.gz: 879f563ae943c253b987a1c5beeec95b41bb95c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad5c3c4fe63c1e5e860a3ea7bfaa5af4b345806c0569853e4a365f34f53cd0c3672fac725e3e2067cca5f5b9a1348f0a1651e5113a2488ee4d8ed80ad82d14be
|
7
|
+
data.tar.gz: f90bb39169407674065bedd943645ec94aee5f2ab825070e0563cc51fd5053665efdc15e054bb51146f466715b06387db668b127bb3faf6e232d48377ecdbe29
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
### 0.6.0
|
2
|
+
|
3
|
+
* Render text/plain body as Markdown
|
4
|
+
* Cache feed fetching with Last-Modified and ETag HTTP headers
|
5
|
+
* Update feed URI on permanent redirect
|
6
|
+
* Make sender a required config option
|
7
|
+
* Maintain a separate history file per feed
|
8
|
+
|
1
9
|
### 0.5.0
|
2
10
|
|
3
11
|
* Sanitize SMTP user in from address
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License
|
2
2
|
|
3
|
-
Copyright (c) 2013 Aggelos Orfanakos
|
3
|
+
Copyright (c) 2013, 2014, 2015 Aggelos Orfanakos
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
6
|
this software and associated documentation files (the "Software"), to deal in
|
data/README.md
CHANGED
@@ -27,8 +27,8 @@ $ gem install feed2email
|
|
27
27
|
|
28
28
|
Through a [YAML][] file at `~/.feed2email/config.yml`.
|
29
29
|
|
30
|
-
It is possible to send email via SMTP or an [MTA][]. If `config.yml`
|
31
|
-
options for both, feed2email will use SMTP.
|
30
|
+
It is possible to send email via SMTP or an [MTA][] (default). If `config.yml`
|
31
|
+
contains options for both, feed2email will use SMTP.
|
32
32
|
|
33
33
|
[YAML]: http://en.wikipedia.org/wiki/YAML
|
34
34
|
[MTA]: http://en.wikipedia.org/wiki/Message_transfer_agent
|
@@ -41,13 +41,11 @@ pair is separated with a colon: `foo: bar`
|
|
41
41
|
### Generic options
|
42
42
|
|
43
43
|
* `recipient` (required) is the email address to send email to
|
44
|
-
* `sender` (
|
45
|
-
from the feed entry author or, if missing, it is generated from the SMTP user
|
46
|
-
and host or, if missing, it is the same as `recipient`)
|
44
|
+
* `sender` (required) is the email address to send email from (can be any)
|
47
45
|
* `send_delay` (optional) is the number of seconds to wait between each email to
|
48
46
|
avoid SMTP server throttling errors (default is `10`; use `0` to disable)
|
49
47
|
* `log_path` (optional) is the _absolute_ path to the log file (default is
|
50
|
-
`true` which logs to standard output; use `false` to disable)
|
48
|
+
`true` which logs to standard output; use `false` to disable logging)
|
51
49
|
* `log_level` (optional) is the logging verbosity level and can be `fatal`
|
52
50
|
(least verbose), `error`, `warn`, `info` (default) and `debug` (most verbose)
|
53
51
|
* `max_entries` (optional) is the maximum number of entries to process per feed
|
@@ -58,10 +56,11 @@ pair is separated with a colon: `foo: bar`
|
|
58
56
|
For this method you need to have access to an SMTP service. [Mailgun][] has a
|
59
57
|
free plan.
|
60
58
|
|
61
|
-
* `smtp_host` is the SMTP service hostname to connect to
|
62
|
-
* `smtp_port` is the SMTP service port to connect to
|
63
|
-
* `smtp_user` is the username of your email account
|
64
|
-
* `smtp_pass` is the password of your email account (see the warning
|
59
|
+
* `smtp_host` (required) is the SMTP service hostname to connect to
|
60
|
+
* `smtp_port` (required) is the SMTP service port to connect to
|
61
|
+
* `smtp_user` (required) is the username of your email account
|
62
|
+
* `smtp_pass` (required) is the password of your email account (see the warning
|
63
|
+
below)
|
65
64
|
* `smtp_tls` (optional) controls TLS (default is `true`; can also be `false`)
|
66
65
|
* `smtp_auth` (optional) controls the authentication method (default is `login`;
|
67
66
|
can also be `plain` or `cram_md5`)
|
@@ -87,6 +86,8 @@ interface setup and working in your system like [msmtp][] or [Postfix][].
|
|
87
86
|
|
88
87
|
## Use
|
89
88
|
|
89
|
+
### Managing feeds
|
90
|
+
|
90
91
|
Create `~/.feed2email/feeds.yml` and add the address of each feed you want to
|
91
92
|
subscribe to, prefixed with a dash and a space:
|
92
93
|
|
@@ -94,30 +95,57 @@ subscribe to, prefixed with a dash and a space:
|
|
94
95
|
- https://github.com/agorf/feed2email/commits.atom
|
95
96
|
~~~
|
96
97
|
|
97
|
-
To disable a feed
|
98
|
+
To disable a feed, comment it:
|
98
99
|
|
99
100
|
~~~ yaml
|
100
101
|
#- https://github.com/agorf/feed2email/commits.atom
|
101
102
|
~~~
|
102
103
|
|
103
|
-
|
104
|
+
### Running
|
105
|
+
|
106
|
+
Simply:
|
104
107
|
|
105
108
|
~~~ sh
|
106
109
|
$ feed2email
|
107
110
|
~~~
|
108
111
|
|
109
|
-
When
|
110
|
-
|
112
|
+
When feed2email runs for the first time or after adding a new feed:
|
113
|
+
|
114
|
+
* All feed entries are skipped (no email sent)
|
115
|
+
* `~/.feed2email/history-<digest>.yml` is created for each feed containing these
|
116
|
+
(old) entries, where `<digest>` is the MD5 hex digest of the feed URL
|
117
|
+
|
118
|
+
**Warning:** Versions prior to 0.6.0 used a single history file for all feeds.
|
119
|
+
Before using version 0.6.0 for the first time, please make sure you run the
|
120
|
+
provided migration script: `feed2email-migrate-history` If you don't, feed2email
|
121
|
+
will think it's run for the first time and will treat all entries as old (thus
|
122
|
+
no email will be sent and you may miss some entries).
|
123
|
+
|
124
|
+
To receive existing entries from a new feed:
|
111
125
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
126
|
+
1. Add it to `feeds.yml` (see above)
|
127
|
+
1. Run feed2email once so that the feed's history file is generated
|
128
|
+
1. Remove the entries you want to receive from the feed's history (i.e. with
|
129
|
+
your text editor)
|
130
|
+
1. Remove the feed's meta file (`meta-<digest>.yml`) to bust feed fetching
|
131
|
+
caching
|
116
132
|
|
117
|
-
|
118
|
-
delete them from `history.yml`. Next time feed2email runs, they will be
|
133
|
+
Next time feed2email runs, these entries will be treated as new and will be
|
119
134
|
processed (sent as email).
|
120
135
|
|
136
|
+
### Permanent redirections
|
137
|
+
|
138
|
+
Before processing each feed, feed2email issues a [HEAD request][] to check
|
139
|
+
whether it has been permanently moved by looking for a _301 Moved Permanently_
|
140
|
+
HTTP status and its respective _Location_ header. In such case, feed2email
|
141
|
+
updates `feeds.yml` with the new location and all feed entries are skipped (no
|
142
|
+
email sent). If you do want to have some of them sent as email, please refer to
|
143
|
+
the _Running_ section.
|
144
|
+
|
145
|
+
[HEAD request]: http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
|
146
|
+
|
147
|
+
### Automating
|
148
|
+
|
121
149
|
You can use [cron][] to run feed2email automatically e.g. once every hour.
|
122
150
|
|
123
151
|
[cron]: http://en.wikipedia.org/wiki/Cron
|
data/TODO.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# TODO
|
2
|
+
|
3
|
+
* Specs
|
4
|
+
* Do not mark entry as sent if email was not sent
|
5
|
+
* Show entry metadata in email (e.g. pubdate, author)
|
6
|
+
* Implement a command-line interface to manage feeds.yml
|
7
|
+
* Detect entry URI changes (maybe by comparing body hashes?)
|
8
|
+
* Filters (e.g. skip entries matching a pattern)
|
9
|
+
* Support "dispatch interfaces" where email is one such interface (another could
|
10
|
+
be writing to the filesystem)
|
11
|
+
* Profiles (support many feed lists and recipients)
|
12
|
+
* Send email notifications to user (e.g. when a feed is not available anymore)
|
13
|
+
* Plugin architecture
|
data/bin/feed2email
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
CONFIG_DIR = File.expand_path('~/.feed2email')
|
7
|
+
HISTORY_FILE = File.join(CONFIG_DIR, 'history.yml')
|
8
|
+
|
9
|
+
if !File.exist?(HISTORY_FILE)
|
10
|
+
$stderr.puts "Missing history file #{HISTORY_FILE}"
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
history_data = YAML.load(open(HISTORY_FILE))
|
15
|
+
|
16
|
+
if !history_data.is_a?(Hash)
|
17
|
+
$stderr.puts "Invalid data type for history file #{HISTORY_FILE}"
|
18
|
+
exit 2
|
19
|
+
end
|
20
|
+
|
21
|
+
history_data.each do |feed_uri, entries|
|
22
|
+
hist_filename = "history-#{Digest::MD5.hexdigest(feed_uri)}.yml"
|
23
|
+
hist_path = File.join(CONFIG_DIR, hist_filename)
|
24
|
+
open(hist_path, 'w') {|f| f.write(entries.to_yaml) }
|
25
|
+
puts "Extracted history of #{feed_uri} to #{hist_filename}"
|
26
|
+
end
|
27
|
+
|
28
|
+
File.rename(HISTORY_FILE, "#{HISTORY_FILE}.bak")
|
29
|
+
puts "Renamed #{HISTORY_FILE} to #{HISTORY_FILE}.bak"
|
data/lib/feed2email/config.rb
CHANGED
@@ -1,31 +1,106 @@
|
|
1
|
-
|
2
|
-
CONFIG_DIR = File.expand_path('~/.feed2email')
|
1
|
+
require 'yaml'
|
3
2
|
|
3
|
+
module Feed2Email
|
4
4
|
class Config
|
5
|
-
|
5
|
+
class MissingConfigError < StandardError; end
|
6
|
+
class InvalidConfigPermissionsError < StandardError; end
|
7
|
+
class InvalidConfigSyntaxError < StandardError; end
|
8
|
+
class InvalidConfigDataTypeError < StandardError; end
|
9
|
+
class MissingConfigOptionError < StandardError; end
|
10
|
+
|
11
|
+
def initialize(path)
|
12
|
+
@path = File.expand_path(path)
|
13
|
+
check
|
14
|
+
end
|
6
15
|
|
7
|
-
|
16
|
+
def [](option)
|
17
|
+
merged_config[option] # delegate
|
18
|
+
end
|
8
19
|
|
9
|
-
|
20
|
+
private
|
10
21
|
|
11
|
-
def
|
12
|
-
|
22
|
+
def path
|
23
|
+
@path
|
24
|
+
end
|
13
25
|
|
14
|
-
|
26
|
+
def data
|
27
|
+
@data
|
28
|
+
end
|
15
29
|
|
16
|
-
|
17
|
-
|
18
|
-
|
30
|
+
def check
|
31
|
+
check_existence
|
32
|
+
check_permissions
|
33
|
+
check_syntax
|
34
|
+
check_data_type
|
35
|
+
check_recipient_existence
|
36
|
+
check_sender_existence
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_existence
|
40
|
+
if !File.exist?(path)
|
41
|
+
raise MissingConfigError, "Missing config file #{path}"
|
19
42
|
end
|
43
|
+
end
|
20
44
|
|
21
|
-
|
22
|
-
|
23
|
-
|
45
|
+
def check_permissions
|
46
|
+
if '%o' % (File.stat(path).mode & 0777) != '600'
|
47
|
+
raise InvalidConfigPermissionsError,
|
48
|
+
"Invalid permissions for config file #{path}"
|
24
49
|
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_syntax
|
53
|
+
begin
|
54
|
+
load_yaml
|
55
|
+
rescue Psych::SyntaxError
|
56
|
+
raise InvalidConfigSyntaxError,
|
57
|
+
"Invalid YAML syntax for config file #{path}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def check_data_type
|
62
|
+
if !data.is_a?(Hash)
|
63
|
+
raise InvalidConfigDataTypeError,
|
64
|
+
"Invalid data type (not a Hash) for config file #{path}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_recipient_existence
|
69
|
+
check_option_existence('recipient')
|
70
|
+
end
|
71
|
+
|
72
|
+
def check_sender_existence
|
73
|
+
check_option_existence('sender')
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_yaml
|
77
|
+
@data = YAML.load(read_file)
|
78
|
+
end
|
79
|
+
|
80
|
+
def read_file
|
81
|
+
File.read(path)
|
82
|
+
end
|
83
|
+
|
84
|
+
def merged_config
|
85
|
+
@merged_config ||= defaults.merge(data)
|
86
|
+
end
|
87
|
+
|
88
|
+
def defaults
|
89
|
+
{
|
90
|
+
'log_level' => 'info',
|
91
|
+
'log_path' => true,
|
92
|
+
'max_entries' => 20,
|
93
|
+
'send_delay' => 10,
|
94
|
+
'sendmail_path' => '/usr/sbin/sendmail',
|
95
|
+
'smtp_auth' => 'login',
|
96
|
+
'smtp_tls' => true,
|
97
|
+
}
|
98
|
+
end
|
25
99
|
|
26
|
-
|
27
|
-
|
28
|
-
|
100
|
+
def check_option_existence(option)
|
101
|
+
if data[option].nil?
|
102
|
+
raise MissingConfigOptionError,
|
103
|
+
"Option #{option} missing from config file #{path}"
|
29
104
|
end
|
30
105
|
end
|
31
106
|
end
|
data/lib/feed2email/core_ext.rb
CHANGED
@@ -1,11 +1,23 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'reverse_markdown'
|
3
|
+
require 'sanitize'
|
4
|
+
|
1
5
|
class String
|
2
6
|
def escape_html
|
3
7
|
CGI.escapeHTML(self)
|
4
8
|
end
|
5
9
|
|
10
|
+
def pluralize(count, plural = self + 's')
|
11
|
+
"#{count} #{count == 1 ? self : plural}"
|
12
|
+
end
|
13
|
+
|
6
14
|
def strip_html
|
7
15
|
CGI.unescapeHTML(Sanitize.clean(self))
|
8
16
|
end
|
17
|
+
|
18
|
+
def to_markdown
|
19
|
+
ReverseMarkdown.convert(self, unknown_tags: :drop)
|
20
|
+
end
|
9
21
|
end
|
10
22
|
|
11
23
|
class Time
|
data/lib/feed2email/entry.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'feed2email/mail'
|
2
|
+
|
1
3
|
module Feed2Email
|
2
4
|
class Entry
|
3
5
|
def initialize(data, feed_uri, feed_title)
|
@@ -14,22 +16,25 @@ module Feed2Email
|
|
14
16
|
@data.content || @data.summary
|
15
17
|
end
|
16
18
|
|
17
|
-
def
|
19
|
+
def send_mail
|
18
20
|
Mail.new(self, @feed_title).send
|
19
21
|
end
|
20
22
|
|
21
23
|
def title
|
22
|
-
@data.title
|
24
|
+
@data.title.strip
|
23
25
|
end
|
24
26
|
|
25
27
|
def uri
|
26
|
-
@uri
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
return @uri if @uri
|
29
|
+
|
30
|
+
@uri = @data.url
|
31
|
+
|
32
|
+
# Make relative entry URL absolute by prepending feed URL
|
33
|
+
if @uri && @uri.start_with?('/')
|
34
|
+
@uri = @feed_uri[%r{https?://[^/]+}] + @uri
|
32
35
|
end
|
36
|
+
|
37
|
+
@uri
|
33
38
|
end
|
34
39
|
end
|
35
40
|
end
|