podgraph 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2010 Alexander Gromnitsky.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,95 @@
1
+ -*- text -*-
2
+ = Name
3
+
4
+ podgraph -- Create an email from a XHTML and inline images and send it
5
+ to posterous.com.
6
+
7
+ = Synopsis
8
+
9
+ podgraph [-Sv] [-m ARG] file.html
10
+
11
+ = Description
12
+
13
+ It's a simple program that reads a XHTML file, extracts subject and body
14
+ from the file, constructs an email and delivers it.
15
+
16
+ The options are as follows:
17
+
18
+ -v Be more verbose.
19
+ -S Don't send, just dump the mail to stdout.
20
+ -m ARG 1 of modes: Podgraph::["related", "mixed"].
21
+
22
+ The file is supposed to be a XHTML result from python's docutils
23
+ reStructuredText. The idea is:
24
+
25
+ 1. You're writing a post (for example, for posterous.com) in
26
+ reStructuredText in Emacs.
27
+
28
+ 2. You're converting it with help of docutils to the XHTML.
29
+
30
+ 3. Finally, you're creating a proper MIME mail from raw XHTML and
31
+ sending it to posterous.com.
32
+
33
+ Step 3 is what podgraph does.
34
+
35
+ == Features
36
+
37
+ * Creates mails with inline images if it can found links in the XHTML
38
+ file to local images.
39
+
40
+ * Charset of 'text/html' portion of a mail is always UTF-8.
41
+
42
+ = Exit status
43
+
44
+ Program exits 0 on success (something was generated or even delivered),
45
+ or >= 1 if en error occurs.
46
+
47
+ = Examples
48
+
49
+ Create a config.yaml file in the directory with your writing. For
50
+ example:
51
+
52
+ % cd ~/lib/writing/posterous
53
+ % cat config.yaml
54
+ :to: post@posterous.com
55
+ :from: me@example.org
56
+
57
+ Start writing a .rest file:
58
+
59
+ % emacsclient 0001.rest &
60
+
61
+ [...]
62
+
63
+ To give podgraph a chance to find a subject for the mail, write .rest
64
+ file as:
65
+
66
+ This is a subject
67
+ *****************
68
+
69
+ My usual reStructuredText.
70
+
71
+ Or in another words, the first 2 tags in the body of the the XHTML must
72
+ be:
73
+
74
+ <div><h1>My subject</h1> [...]
75
+
76
+ Convert it to XHTML:
77
+
78
+ % rst2html < 0001.rest > 0001.html
79
+
80
+ Send to posterous.com:
81
+
82
+ % podgraph 0001.html
83
+
84
+ Also, you can preview the mail without sending it:
85
+
86
+ % podgraph -S 0001.html
87
+
88
+ If you have links to local images in your .rest file (and
89
+ correspondingly in .html) then podgraph will include images in the mail as
90
+ inline images. But if you don't want this and want images to become a
91
+ typical gallery on posterous.com, run:
92
+
93
+ % podgraph -m mixed 0001.html
94
+
95
+ This will generate the mail with usual boring attachments.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rake'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/clean'
6
+ require 'rake/rdoctask'
7
+ require 'rake/testtask'
8
+
9
+ spec = Gem::Specification.new do |s|
10
+ s.name = "podgraph"
11
+ s.summary = 'Creates a MIME mail from a XHTML source and delivers it to Posterous.com.'
12
+ s.version = '0.0.1'
13
+ s.author = 'Alexander Gromnitsky'
14
+ s.email = 'alexander.gromnitsky@gmail.com'
15
+ s.platform = Gem::Platform::RUBY
16
+ s.required_ruby_version = '>= 1.9'
17
+ s.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'test/**/*'].to_a
18
+ s.executables = ['podgraph']
19
+ s.has_rdoc = true
20
+ s.test_files = FileList['test/ts_*.rb'].to_a
21
+ s.rdoc_options << '-m' << 'Podgraph'
22
+ end
23
+ spec.add_dependency('mail', '>= 2.1.3')
24
+
25
+ Rake::GemPackageTask.new(spec).define
26
+
27
+ task :default => %(repackage)
28
+
29
+ Rake::RDocTask.new('doc') do |rd|
30
+ rd.main = "Podgraph"
31
+ rd.rdoc_dir = 'doc'
32
+ rd.rdoc_files.include("lib/**/*.rb")
33
+ end
34
+
35
+ Rake::TestTask.new do |t|
36
+ t.test_files = FileList['test/ts_*.rb']
37
+ t.verbose = true
38
+ end
data/TODO ADDED
@@ -0,0 +1,12 @@
1
+ -*- text -*-
2
+
3
+ 0.0.2
4
+
5
+ - read input from stdin
6
+ - CL options for email
7
+
8
+ 0.0.1
9
+
10
+ + rewrite README.rdoc to make it look more like a manpage
11
+ + add simple tests
12
+ + check for hostname
data/bin/podgraph ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby19
2
+ # -*- ruby -*-
3
+
4
+ # Copyright (c) 2010 Alexander Gromnitsky <mailto:alexander.gromnitsky@gmail.com>.
5
+ #
6
+ # $Id: podgraph 102 2010-03-10 18:41:28Z alex $
7
+
8
+ require_relative '../lib/podgraph/posterous'
9
+
10
+ $conf = {
11
+ prog_ver: '0.0.1',
12
+ send?: true,
13
+ config: 'config.yaml',
14
+ modes: %w(related mixed),
15
+ mode: 'related',
16
+ to: nil,
17
+ from: nil
18
+ }
19
+
20
+ def config_load()
21
+ begin
22
+ myconf = YAML.load_file($conf[:config])
23
+ rescue
24
+ abort("cannot parse #{$conf[:config]} in the current directory")
25
+ end
26
+ %w(to from).each { |i|
27
+ abort("missing #{i} in #{$conf[:config]}") if ! myconf.key?(i.to_sym)
28
+ }
29
+ $conf.merge!(myconf)
30
+ end
31
+
32
+ def cl_parse(myargs)
33
+ o = OptionParser.new()
34
+ o.banner = "#{File.basename($PROGRAM_NAME)} #{$conf[:prog_ver]}.
35
+ Create an email from a XHTML and inline images and send it to posterous.com.
36
+ http://podgraph.posterous.com/
37
+
38
+ Usage: #{File.basename($PROGRAM_NAME)} [options] file.html"
39
+ o.separator ""
40
+ o.on('-v', 'Be more verbose.') { |v| Podgraph::cfg[:verbose] += 1 }
41
+ o.on('-S', "Don't send, just dump the mail to stdout.") { |v| $conf[:send?] = false }
42
+ o.on('-m ARG', "1 of modes: Podgraph::#{$conf[:modes]}.",
43
+ $conf[:modes]) { |v| $conf[:mode] = v }
44
+
45
+ begin
46
+ o.parse!(myargs)
47
+ rescue
48
+ abort("cl parse error: #{$!}")
49
+ end
50
+ end
51
+
52
+ # --[ main ]------------------------------------------------------------
53
+
54
+ config_load()
55
+
56
+ cl_parse ARGV
57
+ Podgraph::veputs(1, "CL options: #{ARGV}")
58
+ unless ARGV.size >= 1
59
+ abort("Usage: #{File.basename($PROGRAM_NAME)} [options] filename.html
60
+ Type \"#{File.basename($PROGRAM_NAME)} -h\" for the help.")
61
+ end
62
+
63
+ Podgraph::veputs(2, "cfg #{Podgraph::cfg}")
64
+ begin
65
+ p = Podgraph::Posterous.new(ARGV[0], $conf[:to], $conf[:from], $conf[:mode])
66
+ Podgraph::veputs(2, "o: #{p.o}".to_s.encode('koi8-u'))
67
+ mail = p.generate()
68
+ rescue
69
+ abort("HTML parsing failed: #{$!}")
70
+ end
71
+
72
+ if ! $conf[:send?]
73
+ puts mail.to_s()
74
+ exit 0
75
+ end
76
+
77
+ begin
78
+ mail.delivery_method :sendmail
79
+ mail.deliver
80
+ rescue
81
+ abort("cannot send mail: #{$!}")
82
+ end
@@ -0,0 +1,168 @@
1
+ # $Id: posterous.rb 104 2010-03-10 18:46:53Z alex $
2
+
3
+ require 'mail'
4
+ require 'rexml/document'
5
+ require 'yaml'
6
+ require 'optparse'
7
+
8
+ # :include: ../../README.rdoc
9
+ module Podgraph
10
+
11
+ mattr_accessor :cfg
12
+
13
+ self.cfg = Hash.new()
14
+ cfg[:verbose] = 0
15
+
16
+ def self.veputs(level, s)
17
+ puts(s) if cfg[:verbose] >= level
18
+ end
19
+
20
+ # Reads XHTML file, analyses it, finds images, checks if they can be inlined,
21
+ # generates multipart/relative or multipart/mixed MIME mail.
22
+ class Posterous
23
+
24
+ # some options for mail generator; change with care
25
+ attr_accessor :o
26
+
27
+ # Analyses _filename_. It must be a XHTML file.
28
+ # _to_, _from_ are email.
29
+ # _mode_ is 1 of 'related' or 'mixed' string.
30
+ def initialize(filename, to, from, mode)
31
+ @o = Hash.new()
32
+ @o[:lib_ver] = '0.0.1'
33
+ @o[:user_agent] = 'podgraph/' + @o[:lib_ver]
34
+ @o[:subject] = ''
35
+ @o[:body] = []
36
+ @o[:attachment] = []
37
+ @o[:a_marks] = {}
38
+ @o[:mode] = mode
39
+ @o[:to] = to
40
+ @o[:from] = from
41
+
42
+ fp = File.new(filename)
43
+ begin
44
+ make(fp)
45
+ rescue
46
+ raise $!
47
+ ensure
48
+ fp.close()
49
+ end
50
+ end
51
+
52
+ def make(fp)
53
+ xml = REXML::Document.new(fp)
54
+ begin
55
+ @o[:subject].replace(REXML::XPath.first(xml, "/html/body/div/h1").text.gsub(/\s+/, " "))
56
+ raise if @o[:subject] =~ /^\s*$/
57
+ rescue
58
+ raise 'cannot extract the subject from <h1>'
59
+ end
60
+
61
+ img_collect = ->(i, a) {
62
+ if i.name == 'img'
63
+ if (src = i.attributes['src']) =~ /^\s*$/
64
+ raise '<img> tag with missing or empty src attribute'
65
+ elsif src =~ /\s*(http|ftp):\/\//
66
+ # we are ignoring URL's
67
+ return
68
+ else
69
+ a << src
70
+ if @o[:mode] == 'related'
71
+ # replace src attribute with a random chars--later
72
+ # we'll replace such marks with corrent content-id
73
+ random = Mail.random_tag()
74
+ i.attributes['src'] = random
75
+ @o[:a_marks][src] = random # save an act of the replacement
76
+
77
+ @o.rehash() # does is this really necessary?
78
+ end
79
+ end
80
+ end
81
+ }
82
+
83
+ f = 1
84
+ xml.elements.each('/html/body/div/*') { |i|
85
+ if f == 1
86
+ f = 0 # skip first <h1>
87
+ next
88
+ end
89
+
90
+ Podgraph::veputs(2, "node: #{i.name}")
91
+ img_collect.call(i, @o[:attachment])
92
+ i.each_recursive { |j|
93
+ Podgraph::veputs(2, "node recursive: #{j.name}")
94
+ img_collect.call(j, @o[:attachment])
95
+ }
96
+
97
+ @o[:body] << i
98
+ }
99
+
100
+ raise "body is empty or filled with nonsence" if @o[:body].size == 0
101
+ end
102
+ private :make
103
+
104
+ # Returns ready for delivery Mail object.
105
+ def generate()
106
+ m = Mail.new()
107
+ m.from(@o[:from])
108
+ m.to(@o[:to])
109
+ m.content_transfer_encoding('8bit')
110
+ m.subject(@o[:subject])
111
+ m.headers({'User-Agent' => @o[:user_agent]})
112
+
113
+ Podgraph::veputs(2, "Body lines=#{@o[:body].size}, bytes=#{@o[:body].to_s.bytesize}")
114
+ if @o[:attachment].size == 0
115
+ m.content_disposition('inline')
116
+ m.content_type('text/html; charset="UTF-8"')
117
+ m.body(@o[:body])
118
+ else
119
+ if @o[:mode] == 'related'
120
+ m.content_type('Multipart/Related')
121
+ end
122
+ m.html_part = Mail::Part.new {
123
+ content_transfer_encoding('8bit')
124
+ content_type('text/html; charset=UTF-8')
125
+ }
126
+ m.html_part.body = @o[:body]
127
+ m.html_part.content_disposition('inline') if @o[:mode] == 'mixed'
128
+
129
+ begin
130
+ @o[:attachment].each { |i| m.add_file(i) }
131
+ rescue
132
+ raise("cannot attach: #{$!}")
133
+ end
134
+
135
+ if @o[:mode] == 'related'
136
+ if (fqdn = Socket.gethostname() ) == ''
137
+ raise 'hostname is not set!'
138
+ end
139
+ cid = {}
140
+ m.parts[1..-1].each { |i|
141
+ i.content_disposition('inline')
142
+ cid[i.filename] = i.content_id("<#{Mail.random_tag}@#{fqdn}.NO_mail>")
143
+ }
144
+
145
+ @o[:a_marks].each { |k, v|
146
+ if cid.key?(k)
147
+ Podgraph::veputs(2, "mark #{k} = #{v}; -> to #{cid[k]}")
148
+ # replace marks with corresponding content-id
149
+ m.html_part.body.raw_source.sub!(v, "cid:#{cid[k][1..-1]}")
150
+ else
151
+ raise("orphan key in cid: #{k}")
152
+ end
153
+ }
154
+ end
155
+ end # a.size
156
+
157
+ return m
158
+ end
159
+
160
+ # Print Mail object to stdout.
161
+ # _e_ is an optional encoding.
162
+ def dump(e = '')
163
+ puts (e == '' ? generate().to_s : generate().to_s.encode(e))
164
+ end
165
+
166
+ end # Posterous
167
+
168
+ end # Podgraph
data/test/blue.png ADDED
Binary file
data/test/config.yaml ADDED
@@ -0,0 +1,3 @@
1
+ :to: alex@goliard
2
+ # :to: post@podgraph-test.posterous.com
3
+ :from: alexander.gromnitsky@gmail.com
data/test/empty.html ADDED
File without changes
@@ -0,0 +1 @@
1
+ fukuyama
@@ -0,0 +1 @@
1
+ <html><lolipop>1</lolipop>
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <body>
3
+ <div>
4
+ <h1>Really, dude</h1>
5
+ This doesn't work.
6
+ </div>
7
+ </body>
8
+ </html>
@@ -0,0 +1,9 @@
1
+ <html>
2
+ <body>
3
+ <div>
4
+ <h1>zzz</h1>
5
+ <p>Missing <img src='yobo'/> inline image.</p>
6
+ (hidden text)
7
+ </div>
8
+ </body>
9
+ </html>