whisperblog 0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +23 -0
- data/LICENSE +661 -0
- data/README +59 -0
- data/bin/whisper +91 -0
- data/bin/whisper-init +40 -0
- data/bin/whisper-post +88 -0
- data/bin/whisper-process-email +54 -0
- data/lib/whisper.rb +52 -0
- data/lib/whisper/author_tracker.rb +46 -0
- data/lib/whisper/blog.rb +156 -0
- data/lib/whisper/cached_file.rb +65 -0
- data/lib/whisper/comment.rb +35 -0
- data/lib/whisper/comment_set.rb +41 -0
- data/lib/whisper/common.rb +276 -0
- data/lib/whisper/config.rb +49 -0
- data/lib/whisper/dir_scanner.rb +76 -0
- data/lib/whisper/dir_set.rb +50 -0
- data/lib/whisper/email_receiver.rb +212 -0
- data/lib/whisper/email_sender.rb +114 -0
- data/lib/whisper/entry.rb +44 -0
- data/lib/whisper/entry_set.rb +41 -0
- data/lib/whisper/handler.rb +99 -0
- data/lib/whisper/mbox.rb +141 -0
- data/lib/whisper/page.rb +118 -0
- data/lib/whisper/rfc2047.rb +79 -0
- data/lib/whisper/router.rb +50 -0
- data/lib/whisper/server.rb +88 -0
- data/lib/whisper/text.rb +252 -0
- data/lib/whisper/timed_map.rb +43 -0
- data/lib/whisper/version.rb +3 -0
- data/share/whisper/comment-email.rtxt +36 -0
- data/share/whisper/config.yaml +53 -0
- data/share/whisper/entry-email.rtxt +35 -0
- data/share/whisper/entry.rhtml +88 -0
- data/share/whisper/entry.rtxt +19 -0
- data/share/whisper/formatter.rb +52 -0
- data/share/whisper/helper.rb +147 -0
- data/share/whisper/list.rhtml +51 -0
- data/share/whisper/list.rrss +9 -0
- data/share/whisper/list.rtxt +25 -0
- data/share/whisper/master.rhtml +81 -0
- data/share/whisper/master.rrss +21 -0
- data/share/whisper/master.rtxt +5 -0
- data/share/whisper/mootools.js +282 -0
- data/share/whisper/rss-badge.png +0 -0
- data/share/whisper/spinner.gif +0 -0
- data/share/whisper/style.css +314 -0
- metadata +239 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require "whisper/common"
|
2
|
+
require "whisper/text"
|
3
|
+
|
4
|
+
module Whisper
|
5
|
+
|
6
|
+
## a blog entry. a text plus a title, basically.
|
7
|
+
class Entry
|
8
|
+
include Loggy
|
9
|
+
include Dependency
|
10
|
+
|
11
|
+
def initialize meta_file, content_file
|
12
|
+
@text = Text.new meta_file, content_file
|
13
|
+
dependency_init
|
14
|
+
end
|
15
|
+
|
16
|
+
def dependencies; [@text] end
|
17
|
+
def title type; content type end
|
18
|
+
def build old, type
|
19
|
+
case type
|
20
|
+
when :html, :rss; build_html_title
|
21
|
+
when :txt, :email; build_textile_title
|
22
|
+
else raise ArgumentError, "unknown type #{type.inspect}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
[:labels, :published, :updated, :id, :author, :meta_file, :content_file].each { |m| define_method(m) { @text.send m } }
|
27
|
+
def body format, opts={}; @text.body format, opts end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_html_title
|
32
|
+
debug "rendering HTML title for #{@text.meta_file.path}"
|
33
|
+
RedCloth.new(@text.meta[:title]).to_html.gsub(%r,^<p>|</p>$,, "") # TODO: figure out a better way to remove these
|
34
|
+
end
|
35
|
+
|
36
|
+
## since the title typically is one line and without any of the textile stuff
|
37
|
+
## we actually want to tweak, we also use this for the text title.
|
38
|
+
def build_textile_title
|
39
|
+
debug "rendering textile title"
|
40
|
+
@text.meta[:title]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "whisper/common"
|
2
|
+
require "whisper/dir_set"
|
3
|
+
require "whisper/entry"
|
4
|
+
|
5
|
+
module Whisper
|
6
|
+
|
7
|
+
## the set of all entries
|
8
|
+
class EntrySet
|
9
|
+
include Loggy
|
10
|
+
include Dependency
|
11
|
+
|
12
|
+
def initialize dir
|
13
|
+
@dirset = DirSet.new dir, Entry
|
14
|
+
@entries = []
|
15
|
+
dependency_init
|
16
|
+
end
|
17
|
+
|
18
|
+
def dependencies; [@dirset] + @entries end
|
19
|
+
|
20
|
+
%w(entries entries_by_id entries_by_label entries_by_author entries_by_page).each do |f|
|
21
|
+
f = f.intern
|
22
|
+
define_method(f) { content[f] }
|
23
|
+
end
|
24
|
+
|
25
|
+
def size; entries.size end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build old
|
30
|
+
debug "resorting entries and recompiling entry indices"
|
31
|
+
|
32
|
+
@entries = @dirset.things.sort_by { |e| e.published }.reverse
|
33
|
+
{ :entries => @entries,
|
34
|
+
:entries_by_id => @entries.map_by { |e| e.id },
|
35
|
+
:entries_by_author => @entries.group_by { |e| e.author.obfuscated_name },
|
36
|
+
:entries_by_label => @entries.group_by_multiple { |e| e.labels },
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require "whisper/common"
|
3
|
+
require "whisper/timed_map"
|
4
|
+
|
5
|
+
module Whisper
|
6
|
+
|
7
|
+
class InvalidPageError < StandardError; end
|
8
|
+
|
9
|
+
## I borrowed a bit from Rails's syntax, but the semantics are not the same.
|
10
|
+
## Anything with + signs will be treated as a substitutable path component.
|
11
|
+
## You can optionally end the path spec with :format or /:page:format.
|
12
|
+
class Handler
|
13
|
+
include Loggy
|
14
|
+
|
15
|
+
DEFAULT_FORMAT = "html"
|
16
|
+
|
17
|
+
def initialize request_method, pathspec, &page_creator
|
18
|
+
@request_method = request_method
|
19
|
+
@pathspec = pathspec
|
20
|
+
@page_creator = page_creator
|
21
|
+
@map = TimedMap.new
|
22
|
+
@uses_page = @uses_format = false
|
23
|
+
|
24
|
+
re = @pathspec.gsub(/\+(\w+)/) do
|
25
|
+
'([^\.\/][^\/]*?)' # regular path components must start with a non-., non-/, followed by 0 or more non-/es
|
26
|
+
end.sub(/\/:page:format$/) do
|
27
|
+
@uses_page = @uses_format = true
|
28
|
+
'\/?(\d+)?(?:\.([^\/]+))?'
|
29
|
+
end.sub(/:format$/) do
|
30
|
+
@uses_format = true
|
31
|
+
'(?:\.([a-zA-Z]+))?'
|
32
|
+
end
|
33
|
+
@re = /^#{re}$/
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle path, params, request_method, route
|
37
|
+
#debug "comparing #{request_method.inspect} against #{@request_method.inspect}"
|
38
|
+
return unless (request_method == @request_method) || # special case:
|
39
|
+
(request_method == :head && @request_method == :get) # proceed!
|
40
|
+
|
41
|
+
#debug "comparing #{path.inspect} against #{@re} => #{@re.match(path) ? true : false}"
|
42
|
+
match = @re.match(path) or return
|
43
|
+
#debug "have captures: #{match.captures.inspect}"
|
44
|
+
|
45
|
+
## assemble the variables to be passed to the caller function
|
46
|
+
vars = match.captures
|
47
|
+
vars[vars.length - 1] ||= DEFAULT_FORMAT if @uses_format
|
48
|
+
vars[vars.length - 2] = (vars[vars.length - 2].to_i || 0) if @uses_page
|
49
|
+
vars << params
|
50
|
+
|
51
|
+
## make a key from the path and the query parameters, if any. the key is basically
|
52
|
+
## a normalized query string.
|
53
|
+
key = path + "?" + params.sort_by { |k, v| [k, v] }.map { |k, v| k + "=" + v }.join("&")
|
54
|
+
|
55
|
+
case request_method
|
56
|
+
when :post # don't cache
|
57
|
+
make route, vars
|
58
|
+
else
|
59
|
+
@map.prune!
|
60
|
+
@map[key] ||= begin
|
61
|
+
make(route, vars) || :invalid
|
62
|
+
rescue SystemCallError => e
|
63
|
+
:invalid
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def contents; @map.values end
|
69
|
+
|
70
|
+
def url_for opts={}
|
71
|
+
url = @pathspec.gsub(/[:+](\w+)/) do
|
72
|
+
spec = $1.intern
|
73
|
+
val = opts[spec]
|
74
|
+
val = CGI.escape(val.to_s) if val
|
75
|
+
case spec
|
76
|
+
when :format; val.nil? || (val == DEFAULT_FORMAT) ? "" : ".#{val}"
|
77
|
+
when :page; val == "0" ? "" : val
|
78
|
+
else
|
79
|
+
raise ArgumentError, "missing required path spec #{spec}" unless val
|
80
|
+
val
|
81
|
+
end
|
82
|
+
end
|
83
|
+
url += "##{opts[:anchor]}" if opts[:anchor]
|
84
|
+
url.gsub!(/\/\.([^\/\.]+)$/, ".\\1")
|
85
|
+
url
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def make route, vars
|
91
|
+
begin
|
92
|
+
@page_creator.call route, *vars
|
93
|
+
rescue InvalidPageError => e
|
94
|
+
:invalid
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/lib/whisper/mbox.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'rmail'
|
2
|
+
require 'thread'
|
3
|
+
require "whisper/common"
|
4
|
+
require "whisper/rfc2047"
|
5
|
+
|
6
|
+
## stolen from sup
|
7
|
+
class RMail::Message
|
8
|
+
def charset
|
9
|
+
if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/i
|
10
|
+
$1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Whisper
|
16
|
+
|
17
|
+
class Mbox
|
18
|
+
include Loggy
|
19
|
+
|
20
|
+
attr_reader :offset
|
21
|
+
|
22
|
+
def initialize fn, start_offset
|
23
|
+
@fn = fn
|
24
|
+
@offset = start_offset
|
25
|
+
@mutex = Mutex.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def eof?
|
29
|
+
@mutex.synchronize { @offset >= File.size(@fn) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def next_message
|
33
|
+
m = @mutex.synchronize do
|
34
|
+
debug "reading #{@fn}@#{@offset}..."
|
35
|
+
with_filehandle do |f|
|
36
|
+
string = ""
|
37
|
+
l = f.gets
|
38
|
+
string << l until f.eof? || is_break_line?(l = f.gets)
|
39
|
+
debug "read #{string.size} bytes"
|
40
|
+
return nil if string.empty?
|
41
|
+
@offset += string.size
|
42
|
+
begin
|
43
|
+
RMail::Parser.read string
|
44
|
+
rescue ArgumentError # sigh. invalid utf throws this completely generic exception
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
return if m.nil? # hit EOF
|
51
|
+
|
52
|
+
headers = {
|
53
|
+
"message-id" => m.header["message-id"],
|
54
|
+
"in-reply-to" => m.header["in-reply-to"],
|
55
|
+
"date" => m.header["date"],
|
56
|
+
"from" => Rfc2047.decode_to("utf-8", m.header["from"]),
|
57
|
+
}
|
58
|
+
|
59
|
+
text_part = find_text_part m
|
60
|
+
debug "text part of message has size #{text_part.body.size}: #{text_part.body[0..50].inspect}..."
|
61
|
+
body = if text_part
|
62
|
+
x = text_part.decode
|
63
|
+
x = Iconv.easy_decode("utf-8", text_part.charset, x) if text_part.charset
|
64
|
+
munge_body x
|
65
|
+
else
|
66
|
+
"_[This message contains no text/plain part. Bad commenter, no cookie! --ed.]_\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
[body, headers]
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
## recurse through MIME structure looking for text
|
75
|
+
def find_text_part m
|
76
|
+
if m.header.content_type.nil? || m.header.content_type == "text/plain"
|
77
|
+
m
|
78
|
+
elsif m.multipart?
|
79
|
+
m.body.find { |p| find_text_part p }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def with_filehandle
|
84
|
+
begin
|
85
|
+
f = File.open @fn
|
86
|
+
f.seek @offset
|
87
|
+
correction = correct_offset f
|
88
|
+
if correction > 0
|
89
|
+
@offset += correction
|
90
|
+
f.seek @offset
|
91
|
+
debug "corrected offset forward by #{correction} bytes"
|
92
|
+
end
|
93
|
+
x = yield f
|
94
|
+
f.close
|
95
|
+
x
|
96
|
+
rescue SystemCallError => e
|
97
|
+
warn "can't open mbox: #{e.message}"
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def munge_body body
|
103
|
+
## ok, not sure why this is happening, but at least in gmail messages
|
104
|
+
## random spaces seem to turn into =A0's in a quoted-printable
|
105
|
+
## encoding, which then turn into \240's when decoded. so i'm manually
|
106
|
+
## forcing these back to spaces. whether this is a good idea or not
|
107
|
+
## i don't know.
|
108
|
+
body#.
|
109
|
+
#gsub("\240", " ").
|
110
|
+
#gsub("\302", " ") # here's another one. what is this shit?
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
BREAK_RE = /^From \S+ (.+)$/ # stolen from sup
|
116
|
+
require 'time'
|
117
|
+
def is_break_line? l # stolen from sup
|
118
|
+
time = begin
|
119
|
+
l =~ BREAK_RE or return false
|
120
|
+
$1
|
121
|
+
rescue ArgumentError # happens with invalid utf-8, for example
|
122
|
+
return false
|
123
|
+
end
|
124
|
+
begin
|
125
|
+
## hack -- make Time.parse fail when trying to substitute values from Time.now
|
126
|
+
Time.parse time, 0
|
127
|
+
true
|
128
|
+
rescue NoMethodError
|
129
|
+
false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def correct_offset f # stolen from sup
|
134
|
+
string = ""
|
135
|
+
until f.eof? || is_break_line?(l = f.gets)
|
136
|
+
string << l
|
137
|
+
end
|
138
|
+
string.size
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/whisper/page.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'cgi'
|
3
|
+
require "whisper/common"
|
4
|
+
|
5
|
+
module Whisper
|
6
|
+
|
7
|
+
class Page
|
8
|
+
include Loggy
|
9
|
+
include Dependency
|
10
|
+
|
11
|
+
## kind of insane the amount of stuff this thing actually needs
|
12
|
+
def initialize opts, &rebuilder
|
13
|
+
@format = opts[:format]
|
14
|
+
@url_vars = opts[:url_vars] # used for making links to related pages
|
15
|
+
@master_template = opts[:master_template]
|
16
|
+
@template = opts[:template]
|
17
|
+
@helper = opts[:helper]
|
18
|
+
@rebuilder = rebuilder
|
19
|
+
@dependencies = opts[:extra_deps] + [@master_template, @template, @helper]
|
20
|
+
@config = opts[:config]
|
21
|
+
@router = opts[:router]
|
22
|
+
@entryset = opts[:entryset]
|
23
|
+
|
24
|
+
dependency_init
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :dependencies
|
28
|
+
|
29
|
+
def content_type
|
30
|
+
case @format
|
31
|
+
when "html"; "text/html"
|
32
|
+
when "rss"; "application/rss+xml"
|
33
|
+
when "txt"; "text/plain"
|
34
|
+
else raise "unknown format #@format"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
## an environment for erb
|
39
|
+
class HTMLBuilder
|
40
|
+
attr_accessor :content, :entryset
|
41
|
+
|
42
|
+
def initialize entryset, config, router, page_vars, url_vars, format
|
43
|
+
@entryset = entryset
|
44
|
+
@config = config
|
45
|
+
@router = router
|
46
|
+
@page_vars = page_vars
|
47
|
+
@url_vars = url_vars
|
48
|
+
@format = format
|
49
|
+
@content = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def method_missing m, *a
|
53
|
+
if @page_vars.member?(m) && a.empty?
|
54
|
+
@page_vars[m]
|
55
|
+
elsif @config.respond_to? m
|
56
|
+
@config.send m, *a
|
57
|
+
else
|
58
|
+
raise NoMethodError, "undefined method '#{m}' for #{self.class}. Possible ERB error."
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
## utility methods for templates. TODO: move to router
|
63
|
+
def url_for opts={}; @router.url_for({ :format => @format }.merge(opts)) end
|
64
|
+
def full_url_for opts={}; @config.public_url_root + url_for(opts) end
|
65
|
+
|
66
|
+
def url_for_current opts={}; url_for @url_vars.merge(opts) end
|
67
|
+
def full_url_for_current opts={}; @config.public_url_root + url_for_current(opts) end
|
68
|
+
|
69
|
+
def link_to text, opts={}
|
70
|
+
raise "bad: #{text.inspect}, #{opts.inspect}" unless opts.is_a?(Hash)
|
71
|
+
html_opts = (opts[:html_opts] || {}).map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
|
72
|
+
"<a #{html_opts} href=\"" + url_for(opts) + "\">#{text}</a>"
|
73
|
+
end
|
74
|
+
|
75
|
+
def link_to_current text, opts={}
|
76
|
+
html_opts = (opts[:html_opts] || {}).map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
|
77
|
+
"<a #{html_opts} href=\"" + url_for_current(opts) + "\">#{text}</a>"
|
78
|
+
end
|
79
|
+
|
80
|
+
def my_binding; binding end
|
81
|
+
|
82
|
+
def self.install content, filename; eval content, binding, filename end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def build old
|
88
|
+
debug "building erb -> #{@format} from #{@template.path}"
|
89
|
+
bc = case @format
|
90
|
+
when "html"; HTMLBuilder
|
91
|
+
when "rss"; HTMLBuilder
|
92
|
+
when "txt"; HTMLBuilder
|
93
|
+
else raise "unknown format #@format"
|
94
|
+
end
|
95
|
+
erb = ERB.new @template.content, nil, "%-"
|
96
|
+
erb.filename = @template.path
|
97
|
+
|
98
|
+
bc.install @helper.content, @helper.path
|
99
|
+
b = bc.new @entryset, @config, @router, @rebuilder.call, @url_vars, @format
|
100
|
+
b.content = rewrite_image_links_in erb.result(b.my_binding)
|
101
|
+
|
102
|
+
erb = ERB.new @master_template.content, nil, "%-"
|
103
|
+
erb.filename = @master_template.path
|
104
|
+
erb.result b.my_binding
|
105
|
+
end
|
106
|
+
|
107
|
+
## rewrite any image HTML links in here to the static location
|
108
|
+
## (these links are created by redcloth)
|
109
|
+
def rewrite_image_links_in text
|
110
|
+
text.gsub(/<img src=\"(.*?)\"/) do |x|
|
111
|
+
path = $1
|
112
|
+
next x if path =~ /^\w+:/
|
113
|
+
"<img src=\"/static/#{path}\""
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
## from: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/101949
|
2
|
+
|
3
|
+
# $Id: rfc2047.rb,v 1.4 2003/04/18 20:55:56 sam Exp $
|
4
|
+
# MODIFIED slightly by William Morgan
|
5
|
+
#
|
6
|
+
# An implementation of RFC 2047 decoding.
|
7
|
+
#
|
8
|
+
# This module depends on the iconv library by Nobuyoshi Nakada, which I've
|
9
|
+
# heard may be distributed as a standard part of Ruby 1.8. Many thanks to him
|
10
|
+
# for helping with building and using iconv.
|
11
|
+
#
|
12
|
+
# Thanks to "Josef 'Jupp' Schugt" <jupp / gmx.de> for pointing out an error with
|
13
|
+
# stateful character sets.
|
14
|
+
#
|
15
|
+
# Copyright (c) Sam Roberts <sroberts / uniserve.com> 2004
|
16
|
+
#
|
17
|
+
# This file is distributed under the same terms as Ruby.
|
18
|
+
|
19
|
+
require 'iconv'
|
20
|
+
|
21
|
+
## stolen from sup
|
22
|
+
class Iconv
|
23
|
+
def self.easy_decode target, charset, text
|
24
|
+
return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
|
25
|
+
charset = case charset
|
26
|
+
when /UTF[-_ ]?8/i; "utf-8"
|
27
|
+
when /(iso[-_ ])?latin[-_ ]?1$/i; "ISO-8859-1"
|
28
|
+
when /iso[-_ ]?8859[-_ ]?15/i; 'ISO-8859-15'
|
29
|
+
when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i; "utf-7"
|
30
|
+
else charset
|
31
|
+
end
|
32
|
+
|
33
|
+
# Convert:
|
34
|
+
#
|
35
|
+
# Remember - Iconv.open(to, from)!
|
36
|
+
Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Rfc2047
|
41
|
+
WORD = %r{=\?([!\#$%&'*+-/0-9A-Z\\^\`a-z{|}~]+)\?([BbQq])\?([!->@-~]+)\?=} # :nodoc: 'stupid ruby-mode
|
42
|
+
WORDSEQ = %r{(#{WORD.source})\s+(?=#{WORD.source})}
|
43
|
+
|
44
|
+
def Rfc2047.is_encoded? s; s =~ WORD end
|
45
|
+
|
46
|
+
# Decodes a string, +from+, containing RFC 2047 encoded words into a target
|
47
|
+
# character set, +target+. See iconv_open(3) for information on the
|
48
|
+
# supported target encodings. If one of the encoded words cannot be
|
49
|
+
# converted to the target encoding, it is left in its encoded form.
|
50
|
+
def Rfc2047.decode_to(target, from)
|
51
|
+
from = from.gsub(WORDSEQ, '\1')
|
52
|
+
out = from.gsub(WORD) do |word|
|
53
|
+
charset, encoding, text = $1, $2, $3
|
54
|
+
|
55
|
+
# B64 or QP decode, as necessary:
|
56
|
+
case encoding
|
57
|
+
when 'b', 'B'
|
58
|
+
text = text.unpack('m*')[0]
|
59
|
+
|
60
|
+
when 'q', 'Q'
|
61
|
+
# RFC 2047 has a variant of quoted printable where a ' ' character
|
62
|
+
# can be represented as an '_', rather than =32, so convert
|
63
|
+
# any of these that we find before doing the QP decoding.
|
64
|
+
text = text.tr("_", " ")
|
65
|
+
text = text.unpack('M*')[0]
|
66
|
+
|
67
|
+
# Don't need an else, because no other values can be matched in a
|
68
|
+
# WORD.
|
69
|
+
end
|
70
|
+
|
71
|
+
begin
|
72
|
+
Iconv.easy_decode target, charset, text
|
73
|
+
rescue Iconv::InvalidCharacter => e
|
74
|
+
puts "ICONV ERROR: #{e.message}"
|
75
|
+
text
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|