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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ac8af629627f99050a52d97c8c83ae87c03663c4
4
- data.tar.gz: cfc63b86eb14679e57d8f445559b682e2edeea16
3
+ metadata.gz: f9a92661dec1838e02af880c1e7c0e4c2a37dced
4
+ data.tar.gz: 879f563ae943c253b987a1c5beeec95b41bb95c3
5
5
  SHA512:
6
- metadata.gz: 8609da85c1b80a45cc69fa03ef66abbd3b5a4d0e588a720c3700fe887fe884afacbfa33f4b975037fed7d4480a2a6b219a9bebf01273cf28b5c1e25fd75fa23e
7
- data.tar.gz: 3434bf8b41b8a0528f215a273c337198c16986e8ad04732e27417aa2b8af3b33d268110dbf94f4592679833b63fa10fb3ae77e0da06aefb6f33f07db3eaf6dfa
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` contains
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` (optional) is the email address to send email from (default is taken
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 below)
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 temporarily, comment it:
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
- You are now ready to run the program:
104
+ ### Running
105
+
106
+ Simply:
104
107
 
105
108
  ~~~ sh
106
109
  $ feed2email
107
110
  ~~~
108
111
 
109
- When run for the first time, feed2email enters "dry run" mode and exits almost
110
- immediately. During dry run mode:
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
- * No feeds are fetched and, thus, no email is sent (existing feed entries are
113
- considered already seen)
114
- * `~/.feed2email/history.yml` is created containing processed (seen) entries per
115
- feed
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
- If you want to receive existing entries from a specific feed, you can manually
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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'feed2email'
4
+ require 'feed2email/feed'
4
5
 
5
6
  Feed2Email::Feed.process_all
@@ -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"
@@ -1,31 +1,106 @@
1
- module Feed2Email
2
- CONFIG_DIR = File.expand_path('~/.feed2email')
1
+ require 'yaml'
3
2
 
3
+ module Feed2Email
4
4
  class Config
5
- include Singleton
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
- CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
16
+ def [](option)
17
+ merged_config[option] # delegate
18
+ end
8
19
 
9
- attr_reader :config
20
+ private
10
21
 
11
- def read!
12
- FileUtils.mkdir_p(CONFIG_DIR)
22
+ def path
23
+ @path
24
+ end
13
25
 
14
- @config = YAML.load(open(CONFIG_FILE)) rescue nil
26
+ def data
27
+ @data
28
+ end
15
29
 
16
- if !@config.is_a? Hash
17
- STDERR.puts "Error: missing or invalid config file #{CONFIG_FILE}"
18
- exit 1
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
- if '%o' % (File.stat(CONFIG_FILE).mode & 0777) != '600'
22
- STDERR.puts "Error: invalid permissions for config file #{CONFIG_FILE}"
23
- exit 2
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
- if @config['recipient'].nil?
27
- STDERR.puts "Error: recipient missing from config file #{CONFIG_FILE}"
28
- exit 3
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
@@ -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
@@ -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 process
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 ||= begin
27
- if @data.url[0] == '/' # invalid entry URL is a path
28
- @feed_uri[%r{https?://[^/]+}] + @data.url # prepend feed URI
29
- else
30
- @data.url
31
- end
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