feed2email 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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