pakyow-mailer 0.11.3 → 1.0.0.rc1

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
- SHA1:
3
- metadata.gz: d3fb6682629614cabf8cbc9ae9f48700bad85348
4
- data.tar.gz: d801d0bf1f4a8c80fa3abe409fe584d8bf18d0d8
2
+ SHA256:
3
+ metadata.gz: c3144870c88c7af9b1290b67d100e29e40fac2a8c64232580c3a51e702b4919f
4
+ data.tar.gz: ec24d4b029cf1968221bfcb8e97023bb14eb427b8230dfa0b964979b37ce66e0
5
5
  SHA512:
6
- metadata.gz: 271a54f59f748f688d9214b7cf546def21ea10d035eca1ca36b71ad5bf8c146faf545080619933c1cd9553aadb27da21933b7a485b92000b62d2d7b0b10ed234
7
- data.tar.gz: e32862e3638c9da4a06afc5d0b6b057e0188c5e0a323fc3a972e1d0dc0495ed93d3832042ea0238c7cebada26b35c5eaf69ba79cfb9062a9d2a45909998b64a8
6
+ metadata.gz: 780ff66d0c451eefd0293e08e1725886fddedbcd7cde4eeb551a0695fc9c8433f8a7c3208ecc70d929b272fd6d3b285e6ae005256da2a8b6dd3c8e84b24d3caa
7
+ data.tar.gz: 3a7251f70f859a906edb038dcd203c34d37910aa2adb32a710c71c845c8dc3f5902f64ea0e1b58c8cfc09410c1c5bae1995521babcaac41aa6427d032c882d58
File without changes
data/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) Metabahn, LLC
2
+
3
+ Pakyow Mailer is an open-source project licensed under the terms of the LGPLv3 license.
4
+ See <https://choosealicense.com/licenses/lgpl-3.0/> for license text.
@@ -16,8 +16,7 @@ Source code can be downloaded as part of the Pakyow project on Github:
16
16
 
17
17
  # License
18
18
 
19
- Pakyow Mailer is released free and open-source under the [MIT
20
- License](http://opensource.org/licenses/MIT).
19
+ Pakyow Mailer is free and open-source under the [LGPLv3 license](https://choosealicense.com/licenses/lgpl-3.0/).
21
20
 
22
21
  # Support
23
22
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Mailer
7
+ module Behavior
8
+ module Config
9
+ extend Support::Extension
10
+
11
+ apply_extension do
12
+ configurable :mailer do
13
+ setting :default_sender, "Pakyow"
14
+ setting :default_content_type, "text/html"
15
+ setting :delivery_method, :sendmail
16
+ setting :delivery_options, {}
17
+ setting :encoding, "UTF-8"
18
+ setting :silent, true
19
+
20
+ defaults :development do
21
+ setting :silent, false
22
+ end
23
+
24
+ defaults :test do
25
+ setting :delivery_method, :test
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/framework"
4
+
5
+ require "pakyow/mailer/behavior/config"
6
+ require "pakyow/mailer/helpers"
7
+
8
+ module Pakyow
9
+ module Mailer
10
+ class Framework < Pakyow::Framework(:mailer)
11
+ def boot
12
+ object.class_eval do
13
+ include Behavior::Config
14
+
15
+ register_helper :active, Helpers
16
+
17
+ mail_renderer = Class.new(isolated(:Renderer)) do
18
+ # Override so we don't trigger any hooks.
19
+ #
20
+ def perform(output = String.new)
21
+ @presenter.to_html(output)
22
+ end
23
+ end
24
+
25
+ # Delete the create_template_nodes build step since we don't want to mail templates.
26
+ #
27
+ mail_renderer.__build_fns.delete_if { |fn|
28
+ fn.source_location[0].end_with?("create_template_nodes.rb")
29
+ }
30
+
31
+ unless const_defined?(:MailRenderer, false)
32
+ const_set(:MailRenderer, mail_renderer)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Mailer
5
+ module Helpers
6
+ def mailer(path = nil)
7
+ if path
8
+ connection = @connection.dup
9
+
10
+ renderer = connection.app.isolated(:MailRenderer).new(
11
+ app: connection.app,
12
+ presentables: connection.values,
13
+ presenter_class: connection.app.isolated(:MailRenderer).find_presenter(connection.app, path),
14
+ composer: Presenter::Composers::View.new(path, app: @connection.app)
15
+ )
16
+
17
+ Mailer.new(
18
+ renderer: renderer,
19
+ config: app.config.mailer,
20
+ logger: connection.logger
21
+ ).tap do |mailer|
22
+ if block_given?
23
+ context = dup
24
+ context.instance_variable_set(:@connection, connection)
25
+ context.instance_exec(mailer, &Proc.new)
26
+ end
27
+ end
28
+ else
29
+ Mailer.new(
30
+ config: app.config.mailer,
31
+ logger: @connection.logger
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "oga"
4
+
5
+ require "pakyow/mailer/plaintext"
6
+ require "pakyow/mailer/style_inliner"
7
+
8
+ module Pakyow
9
+ module Mailer
10
+ class Mailer
11
+ def initialize(config:, renderer: nil, logger: Pakyow.logger)
12
+ @config, @renderer, @logger = config, renderer, logger
13
+ end
14
+
15
+ def deliver_to(recipient, subject: nil, sender: nil, content: nil, type: nil)
16
+ processed_content = if content
17
+ process(content, type || "text/plain")
18
+ elsif @renderer
19
+ process(@renderer.perform, @config.default_content_type)
20
+ else
21
+ {}
22
+ end
23
+
24
+ html = processed_content[:html]
25
+ text = processed_content[:text]
26
+
27
+ mail = Mail.new
28
+ mail.from = sender || @config.default_sender
29
+ mail.content_type = type || @config.default_content_type
30
+ mail.delivery_method(@config.delivery_method, @config.delivery_options)
31
+
32
+ if html.nil?
33
+ mail.body = text
34
+ else
35
+ encoding = @config.encoding
36
+
37
+ mail.html_part do
38
+ content_type "text/html; charset=" + encoding
39
+ body html
40
+ end
41
+
42
+ mail.text_part do
43
+ content_type "text/plain; charset=" + encoding
44
+ body text
45
+ end
46
+ end
47
+
48
+ if subject
49
+ mail.subject = subject
50
+ end
51
+
52
+ Array(recipient).map { |to|
53
+ deliverable_mail = mail.dup
54
+ deliverable_mail.to = to
55
+ deliverable_mail.deliver.tap do |delivered_mail|
56
+ unless @config.silent
57
+ log_outgoing(delivered_mail)
58
+ end
59
+ end
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def process(content, content_type)
66
+ {}.tap do |processed_content|
67
+ if content_type.include?("text/html")
68
+ document = Oga.parse_html(content)
69
+ mailable_document = document.at_css("body") || document
70
+
71
+ processed_content[:text] = Plaintext.convert_to_text(
72
+ mailable_document.to_xml
73
+ )
74
+
75
+ stylesheets = if @renderer
76
+ @renderer.app.packs(@renderer.presenter.view).select(&:stylesheets?).map(&:stylesheets)
77
+ else
78
+ []
79
+ end
80
+
81
+ processed_content[:html] = StyleInliner.new(
82
+ mailable_document,
83
+ stylesheets: stylesheets
84
+ ).inlined
85
+ else
86
+ processed_content[:text] = content
87
+ end
88
+ end
89
+ end
90
+
91
+ # @api private
92
+ def log_outgoing(delivered_mail)
93
+ message = String.new
94
+ message << "┌──────────────────────────────────────────────────────────────────────────────┐\n"
95
+ message << "│ Subject: #{rpad(delivered_mail.subject, -9)} │\n"
96
+
97
+ if plaintext = delivered_mail.body.parts.find { |part|
98
+ part.content_type.include?("text/plain")
99
+ }
100
+
101
+ message << "├──────────────────────────────────────────────────────────────────────────────┤\n"
102
+
103
+ plaintext.body.to_s.split("\n").each do |line|
104
+ message << "│ #{rpad(line)} │\n"
105
+ end
106
+ end
107
+
108
+ message << "├──────────────────────────────────────────────────────────────────────────────┤\n"
109
+ message << "│ → #{rpad(delivered_mail.to.join(", "), -2)} │\n"
110
+ message << "└──────────────────────────────────────────────────────────────────────────────┘\n"
111
+ @logger.debug message
112
+ end
113
+
114
+ # @api private
115
+ def rpad(message, offset = 0)
116
+ message + " " * (76 + offset - message.length)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "htmlentities"
4
+
5
+ module Pakyow
6
+ module Mailer
7
+ module Plaintext
8
+ # Pulled from premailer (https://github.com/premailer/premailer/)
9
+ # Copyright (c) 2007-2017, Alex Dunae
10
+
11
+ # Returns the text in UTF-8 format with all HTML tags removed
12
+ #
13
+ # TODO: add support for DL, OL
14
+ def self.convert_to_text(html, line_length = 65, _from_charset = "UTF-8")
15
+ txt = html
16
+
17
+ # strip text ignored html. Useful for removing
18
+ # headers and footers that aren't needed in the
19
+ # text version
20
+ txt.gsub!(/<!-- start text\/html -->.*?<!-- end text\/html -->/m, "")
21
+
22
+ # replace images with their alt attributes
23
+ # for img tags with "" for attribute quotes
24
+ # with or without closing tag
25
+ # eg. the following formats:
26
+ # <img alt="" />
27
+ # <img alt="">
28
+ txt.gsub!(/<img.+?alt=\"([^\"]*)\"[^>]*\>/i, '\1')
29
+
30
+ # for img tags with '' for attribute quotes
31
+ # with or without closing tag
32
+ # eg. the following formats:
33
+ # <img alt='' />
34
+ # <img alt=''>
35
+ txt.gsub!(/<img.+?alt=\'([^\']*)\'[^>]*\>/i, '\1')
36
+
37
+ # links
38
+ txt.gsub!(/<a\s[^\n]*?href=["'](mailto:)?([^"']*)["'][^>]*>(.*?)<\/a>/im) do |_s|
39
+ if $3.empty?
40
+ ""
41
+ elsif $3.strip.downcase == $2.strip.downcase
42
+ $3.strip
43
+ else
44
+ $3.strip + " ( " + $2.strip + " )"
45
+ end
46
+ end
47
+
48
+ # handle headings (H1-H6)
49
+ txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
50
+ txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |_s|
51
+ hlevel = $1.to_i
52
+
53
+ htext = $2
54
+ htext.gsub!(/<br[\s]*\/?>/i, "\n") # handle <br>s
55
+ htext.gsub!(/<\/?[^>]*>/i, "") # strip tags
56
+
57
+ # determine maximum line length
58
+ hlength = 0
59
+ htext.each_line do |l| llength = l.strip.length; hlength = llength if llength > hlength end
60
+ hlength = line_length if hlength > line_length
61
+
62
+ case hlevel
63
+ when 1 # H1, asterisks above and below
64
+ htext = ("*" * hlength) + "\n" + htext + "\n" + ("*" * hlength)
65
+ when 2 # H1, dashes above and below
66
+ htext = ("-" * hlength) + "\n" + htext + "\n" + ("-" * hlength)
67
+ else # H3-H6, dashes below
68
+ htext = htext + "\n" + ("-" * hlength)
69
+ end
70
+
71
+ "\n\n" + htext + "\n\n"
72
+ end
73
+
74
+ # wrap spans
75
+ txt.gsub!(/(<\/span>)[\s]+(<span)/mi, '\1 \2')
76
+
77
+ # lists -- TODO: should handle ordered lists
78
+ txt.gsub!(/[\s]*(<li[^>]*>)[\s]*/i, "* ")
79
+ # list not followed by a newline
80
+ txt.gsub!(/<\/li>[\s]*(?![\n])/i, "\n")
81
+
82
+ # paragraphs and line breaks
83
+ txt.gsub!(/<\/p>/i, "\n\n")
84
+ txt.gsub!(/<br[\/ ]*>/i, "\n")
85
+
86
+ # strip remaining tags
87
+ txt.gsub!(/<\/?[^>]*>/, "")
88
+
89
+ # decode HTML entities
90
+ he = HTMLEntities.new
91
+ txt = he.decode(txt)
92
+
93
+ # word wrap
94
+ txt = word_wrap(txt, line_length)
95
+
96
+ # remove linefeeds (\r\n and \r -> \n)
97
+ txt.gsub!(/\r\n?/, "\n")
98
+
99
+ # strip extra spaces
100
+ txt.gsub!(/[ \t]*\302\240+[ \t]*/, " ") # non-breaking spaces -> spaces
101
+ txt.gsub!(/\n[ \t]+/, "\n") # space at start of lines
102
+ txt.gsub!(/[ \t]+\n/, "\n") # space at end of lines
103
+
104
+ # no more than two consecutive newlines
105
+ txt.gsub!(/[\n]{3,}/, "\n\n")
106
+
107
+ # the word messes up the parens
108
+ txt.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do |_s|
109
+ ($1 == "\n" ? $1 : "") + "( " + $2 + " )" + ($3 == "\n" ? $1 : "")
110
+ end
111
+
112
+ txt.strip
113
+ end
114
+
115
+ # Taken from Rails' word_wrap helper (http://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-word_wrap)
116
+ def self.word_wrap(txt, line_length)
117
+ txt.split("\n").collect { |line|
118
+ line.length > line_length ? line.gsub(/(.{1,#{line_length}})(\s+|$)/, "\\1\n").strip : line
119
+ } * "\n"
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "css_parser"
4
+ require "oga"
5
+
6
+ module Pakyow
7
+ module Mailer
8
+ # Inlines styles into html content.
9
+ #
10
+ class StyleInliner
11
+ def initialize(doc_or_html, stylesheets: [])
12
+ @css_parser = CssParser::Parser.new.tap do |parser|
13
+ stylesheets.each do |stylesheet|
14
+ parser.load_string!(stylesheet.read)
15
+ end
16
+ end
17
+
18
+ @doc = case doc_or_html
19
+ when Oga::XML::Document
20
+ doc_or_html
21
+ when Oga::XML::Element
22
+ doc_or_html
23
+ else
24
+ Oga.parse_html(doc_or_html.to_s)
25
+ end
26
+ end
27
+
28
+ def inlined
29
+ @css_parser.each_selector(:all) do |selector, declaration|
30
+ @doc.css(selector).each do |node|
31
+ node.set(:style, [declaration, node.get(:style).to_s].join(" "))
32
+ end
33
+ end
34
+
35
+ @doc.to_xml
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mail"
4
+
5
+ require "pakyow/assets"
6
+ require "pakyow/routing"
7
+ require "pakyow/presenter"
8
+
9
+ require "pakyow/mailer/mailer"
10
+ require "pakyow/mailer/framework"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pakyow-mailer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.3
4
+ version: 1.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Powell
@@ -9,128 +9,171 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-07-15 00:00:00.000000000 Z
12
+ date: 2019-07-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: pakyow-support
15
+ name: pakyow-assets
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
18
  - - '='
19
19
  - !ruby/object:Gem::Version
20
- version: 0.11.3
20
+ version: 1.0.0.rc1
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - '='
26
26
  - !ruby/object:Gem::Version
27
- version: 0.11.3
27
+ version: 1.0.0.rc1
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: pakyow-core
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - '='
33
33
  - !ruby/object:Gem::Version
34
- version: 0.11.3
34
+ version: 1.0.0.rc1
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - '='
40
40
  - !ruby/object:Gem::Version
41
- version: 0.11.3
41
+ version: 1.0.0.rc1
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: pakyow-presenter
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - '='
47
47
  - !ruby/object:Gem::Version
48
- version: 0.11.3
48
+ version: 1.0.0.rc1
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - '='
54
54
  - !ruby/object:Gem::Version
55
- version: 0.11.3
55
+ version: 1.0.0.rc1
56
56
  - !ruby/object:Gem::Dependency
57
- name: mail
57
+ name: pakyow-routing
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '='
61
+ - !ruby/object:Gem::Version
62
+ version: 1.0.0.rc1
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '='
68
+ - !ruby/object:Gem::Version
69
+ version: 1.0.0.rc1
70
+ - !ruby/object:Gem::Dependency
71
+ name: pakyow-support
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '='
75
+ - !ruby/object:Gem::Version
76
+ version: 1.0.0.rc1
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '='
82
+ - !ruby/object:Gem::Version
83
+ version: 1.0.0.rc1
84
+ - !ruby/object:Gem::Dependency
85
+ name: css_parser
58
86
  requirement: !ruby/object:Gem::Requirement
59
87
  requirements:
60
88
  - - "~>"
61
89
  - !ruby/object:Gem::Version
62
- version: '2.6'
90
+ version: '1.7'
63
91
  type: :runtime
64
92
  prerelease: false
65
93
  version_requirements: !ruby/object:Gem::Requirement
66
94
  requirements:
67
95
  - - "~>"
68
96
  - !ruby/object:Gem::Version
69
- version: '2.6'
97
+ version: '1.7'
70
98
  - !ruby/object:Gem::Dependency
71
- name: premailer
99
+ name: htmlentities
72
100
  requirement: !ruby/object:Gem::Requirement
73
101
  requirements:
74
102
  - - "~>"
75
103
  - !ruby/object:Gem::Version
76
- version: '1.8'
104
+ version: '4.3'
77
105
  type: :runtime
78
106
  prerelease: false
79
107
  version_requirements: !ruby/object:Gem::Requirement
80
108
  requirements:
81
109
  - - "~>"
82
110
  - !ruby/object:Gem::Version
83
- version: '1.8'
111
+ version: '4.3'
84
112
  - !ruby/object:Gem::Dependency
85
- name: minitest
113
+ name: mail
86
114
  requirement: !ruby/object:Gem::Requirement
87
115
  requirements:
88
116
  - - "~>"
89
117
  - !ruby/object:Gem::Version
90
- version: '5.6'
91
- type: :development
118
+ version: '2.7'
119
+ type: :runtime
92
120
  prerelease: false
93
121
  version_requirements: !ruby/object:Gem::Requirement
94
122
  requirements:
95
123
  - - "~>"
96
124
  - !ruby/object:Gem::Version
97
- version: '5.6'
125
+ version: '2.7'
126
+ - !ruby/object:Gem::Dependency
127
+ name: oga
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '2.15'
133
+ type: :runtime
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '2.15'
98
140
  description: Mailers for Pakyow
99
141
  email: bryan@metabahn.com
100
142
  executables: []
101
143
  extensions: []
102
144
  extra_rdoc_files: []
103
145
  files:
104
- - pakyow-mailer/CHANGELOG.md
105
- - pakyow-mailer/LICENSE
106
- - pakyow-mailer/README.md
107
- - pakyow-mailer/lib/pakyow-mailer.rb
108
- - pakyow-mailer/lib/pakyow/mailer.rb
109
- - pakyow-mailer/lib/pakyow/mailer/config/mailer.rb
110
- - pakyow-mailer/lib/pakyow/mailer/ext/premailer/adapter/oga.rb
111
- - pakyow-mailer/lib/pakyow/mailer/helpers.rb
112
- - pakyow-mailer/lib/pakyow/mailer/mailer.rb
113
- homepage: http://pakyow.org
146
+ - CHANGELOG.md
147
+ - LICENSE
148
+ - README.md
149
+ - lib/pakyow/mailer.rb
150
+ - lib/pakyow/mailer/behavior/config.rb
151
+ - lib/pakyow/mailer/framework.rb
152
+ - lib/pakyow/mailer/helpers.rb
153
+ - lib/pakyow/mailer/mailer.rb
154
+ - lib/pakyow/mailer/plaintext.rb
155
+ - lib/pakyow/mailer/style_inliner.rb
156
+ homepage: https://pakyow.org
114
157
  licenses:
115
- - MIT
158
+ - LGPL-3.0
116
159
  metadata: {}
117
160
  post_install_message:
118
161
  rdoc_options: []
119
162
  require_paths:
120
- - pakyow-mailer/lib
163
+ - lib
121
164
  required_ruby_version: !ruby/object:Gem::Requirement
122
165
  requirements:
123
166
  - - ">="
124
167
  - !ruby/object:Gem::Version
125
- version: 2.0.0
168
+ version: 2.5.0
126
169
  required_rubygems_version: !ruby/object:Gem::Requirement
127
170
  requirements:
128
- - - ">="
171
+ - - ">"
129
172
  - !ruby/object:Gem::Version
130
- version: '0'
173
+ version: 1.3.1
131
174
  requirements: []
132
175
  rubyforge_project:
133
- rubygems_version: 2.5.1
176
+ rubygems_version: 2.7.6
134
177
  signing_key:
135
178
  specification_version: 4
136
179
  summary: Pakyow Mailer
@@ -1,20 +0,0 @@
1
- Copyright (c) 2011-2015 Bryan Powell
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,16 +0,0 @@
1
- Pakyow::Config.register :mailer do |config|
2
- # the default sender name
3
- config.opt :default_sender, 'Pakyow'
4
-
5
- # the default mimetype to use
6
- config.opt :default_content_type, -> { 'text/html; charset=' + Pakyow::Config.mailer.encoding }
7
-
8
- # the delivery method to use for sending mail
9
- config.opt :delivery_method, :sendmail
10
-
11
- # any delivery options necessary for `delivery_method`
12
- config.opt :delivery_options, { enable_starttls_auto: false }
13
-
14
- # the default encoding to use
15
- config.opt :encoding, 'UTF-8'
16
- end
@@ -1,303 +0,0 @@
1
- require 'oga'
2
-
3
- module Pakyow
4
- class OgaAttributes
5
- def initialize(attributes)
6
- @attributes = attributes
7
- end
8
-
9
- def [](key)
10
- if key.is_a?(Integer)
11
- super
12
- else
13
- @attributes.find { |a| a.name.to_sym == key.to_sym }
14
- end
15
- end
16
-
17
- def method_missing(*args, &block)
18
- @attributes.send(*args, &block)
19
- end
20
- end
21
- end
22
-
23
- module Oga
24
- module XML
25
- class Document
26
- # Premailer builds its css searches with `@` prepended to an attribute,
27
- # which breaks Oga. Since Premailer also expects `search` to be defined
28
- # on a document, we define it and just strip the `@` out before calling
29
- # Oga's build-in #css method.
30
- def search(query)
31
- css(query.gsub('@', ''))
32
- end
33
- end
34
-
35
- class Element
36
- def attributes
37
- Pakyow::OgaAttributes.new(@attributes)
38
- end
39
-
40
- def []=(name, value)
41
- set(name, value)
42
- end
43
-
44
- def [](name)
45
- get(name)
46
- end
47
- end
48
-
49
- class Attribute
50
- def to_str
51
- value
52
- end
53
- end
54
- end
55
- end
56
-
57
- class Premailer
58
- # This method was tied to a specific type of document rather than being
59
- # adapter-agnostic (honestly it's a stretch to call them adapters). So,
60
- # we just pulled this method in and made it work w/ Oga.
61
- def load_css_from_html! # :nodoc:
62
- tags = @doc.css("link[rel='stylesheet']").reject { |tag|
63
- tag.attribute('data-premailer') == 'ignore'
64
- }
65
-
66
- if tags
67
- tags.each do |tag|
68
- if tag.to_s.strip =~ /^\<link/i && tag.attribute('href') && media_type_ok?(tag.attribute('media')) && @options[:include_link_tags]
69
- # A user might want to <link /> to a local css file that is also mirrored on the site
70
- # but the local one is different (e.g. newer) than the live file, premailer will now choose the local file
71
-
72
- if tag.attribute('href').to_s.include? @base_url.to_s and @html_file.kind_of?(String)
73
- if @options[:with_html_string]
74
- link_uri = tag.attribute('href').to_s.sub(@base_url.to_s, '')
75
- else
76
- link_uri = File.join(File.dirname(@html_file), tag.attribute('href').to_s.sub!(@base_url.to_s, ''))
77
- # if the file does not exist locally, try to grab the remote reference
78
- unless File.exists?(link_uri)
79
- link_uri = Premailer.resolve_link(tag.attribute('href').to_s, @html_file)
80
- end
81
- end
82
- else
83
- link_uri = tag.attribute('href').to_s
84
- end
85
-
86
- if Premailer.local_data?(link_uri)
87
- $stderr.puts "Loading css from local file: " + link_uri if @options[:verbose]
88
- load_css_from_local_file!(link_uri)
89
- else
90
- $stderr.puts "Loading css from uri: " + link_uri if @options[:verbose]
91
- @css_parser.load_uri!(link_uri, {:only_media_types => [:screen, :handheld]})
92
- end
93
-
94
- elsif tag.to_s.strip =~ /^\<style/i && @options[:include_style_tags]
95
- @css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
96
- end
97
- end
98
- unless @options[:preserve_styles]
99
- tags.each do |tag|
100
- tag.remove
101
- end
102
- end
103
- end
104
- end
105
-
106
- module Adapter
107
- # Oga adapter
108
- module Oga
109
-
110
- # Merge CSS into the HTML document.
111
- #
112
- # @return [String] an HTML.
113
- def to_inline_css
114
- doc = @processed_doc
115
- @unmergable_rules = CssParser::Parser.new
116
-
117
- # Give all styles already in style attributes a specificity of 1000
118
- # per http://www.w3.org/TR/CSS21/cascade.html#specificity
119
- doc.css("*[style]").each do |el|
120
- el['style'] = '[SPEC=1000[' + el.attribute('style') + ']]'
121
- end
122
- # Iterate through the rules and merge them into the HTML
123
- @css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
124
- # Save un-mergable rules separately
125
- selector.gsub!(/:link([\s]*)+/i) { |m| $1 }
126
-
127
- # Convert element names to lower case
128
- selector.gsub!(/([\s]|^)([\w]+)/) { |m| $1.to_s + $2.to_s.downcase }
129
-
130
- if Premailer.is_media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
131
- @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
132
- else
133
- begin
134
- if selector =~ Premailer::RE_RESET_SELECTORS
135
- # this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
136
- # however, this doesn't mean for testing pur
137
- @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
138
- end
139
-
140
- # Change single ID CSS selectors into xpath so that we can match more
141
- # than one element. Added to work around dodgy generated code.
142
- selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
143
-
144
- doc.css(selector).each do |el|
145
- if el.elem? and (el.name != 'head' and el.parent.name != 'head')
146
- # Add a style attribute or append to the existing one
147
- block = "[SPEC=#{specificity}[#{declaration}]]"
148
- el['style'] = (el.attribute('style').to_s ||= '') + ' ' + block
149
- end
150
- end
151
- rescue RuntimeError, ArgumentError
152
- $stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
153
- next
154
- end
155
- end
156
- end
157
-
158
- # Remove script tags
159
- if @options[:remove_scripts]
160
- doc.css("script").remove
161
- end
162
-
163
- # Read STYLE attributes and perform folding
164
- doc.css("*[style]").each do |el|
165
- style = el.attribute('style').to_s
166
-
167
- declarations = []
168
- style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
169
- rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
170
- declarations << rs
171
- end
172
-
173
- # Perform style folding
174
- merged = CssParser.merge(declarations)
175
- merged.expand_shorthand!
176
-
177
- # Duplicate CSS attributes as HTML attributes
178
- if Premailer::RELATED_ATTRIBUTES.has_key?(el.name) && @options[:css_to_attributes]
179
- Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
180
- el[html_att] = merged[css_att].gsub(/url\(['|"](.*)['|"]\)/, '\1').gsub(/;$|\s*!important/, '').strip if el[html_att].nil? and not merged[css_att].empty?
181
- merged.instance_variable_get("@declarations").tap do |declarations|
182
- declarations.delete(css_att)
183
- end
184
- end
185
- end
186
- # Collapse multiple rules into one as much as possible.
187
- merged.create_shorthand! if @options[:create_shorthands]
188
-
189
- # write the inline STYLE attribute
190
- attributes = Premailer.escape_string(merged.declarations_to_s).split(';').map(&:strip)
191
- attributes = attributes.map { |attr| [attr.split(':').first, attr] }.sort_by { |pair| pair.first }.map { |pair| pair[1] }
192
- el['style'] = attributes.join('; ')
193
- end
194
-
195
- doc = write_unmergable_css_rules(doc, @unmergable_rules)
196
-
197
- if @options[:remove_classes] or @options[:remove_comments]
198
- doc.traverse do |el|
199
- if el.comment? and @options[:remove_comments]
200
- el.remove
201
- elsif el.element?
202
- el.unset('class') if @options[:remove_classes]
203
- end
204
- end
205
- end
206
-
207
- if @options[:remove_ids]
208
- # find all anchor's targets and hash them
209
- targets = []
210
- doc.css("a[@href^='#']").each do |el|
211
- target = el.get_attribute('href')[1..-1]
212
- targets << target
213
- el.set('href', "#" + Digest::MD5.hexdigest(target))
214
- end
215
- # hash ids that are links target, delete others
216
- doc.css("*[@id]").each do |el|
217
- id = el.get_attribute('id')
218
- if targets.include?(id)
219
- el.set('id', Digest::MD5.hexdigest(id))
220
- else
221
- el.unset('id')
222
- end
223
- end
224
- end
225
-
226
- if @options[:reset_contenteditable]
227
- doc.css('*[contenteditable]').each do |el|
228
- el.unset('contenteditable')
229
- end
230
- end
231
-
232
- @processed_doc = doc
233
- @processed_doc.to_xml
234
- end
235
-
236
- # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
237
- # and write it into the <tt>body</tt>.
238
- #
239
- # <tt>doc</tt> is an Oga document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
240
- #
241
- # @return [::Oga::XML::Document] a document.
242
- def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
243
- styles = unmergable_rules.to_s
244
-
245
- unless styles.empty?
246
- style_tag = "<style type=\"text/css\">\n#{styles}</style>"
247
- unless (body = doc.css('body')).empty?
248
- if doc.at_css('body').children && !doc.at_css('body').children.empty?
249
- doc.at_css('body').children.before(::Oga.parse_html(style_tag))
250
- else
251
- doc.at_css('body').add_child(::Oga.parse_html(style_tag))
252
- end
253
- else
254
- doc.inner_html = style_tag += doc.inner_html
255
- end
256
- end
257
- doc
258
- end
259
-
260
-
261
- # Converts the HTML document to a format suitable for plain-text e-mail.
262
- #
263
- # If present, uses the <body> element as its base; otherwise uses the whole document.
264
- #
265
- # @return [String] a plain text.
266
- def to_plain_text
267
- html_src = ''
268
- begin
269
- html_src = @doc.at("body").inner_html
270
- rescue;
271
- end
272
-
273
- html_src = @doc.to_xml unless html_src and not html_src.empty?
274
- convert_to_text(html_src, @options[:line_length], @html_encoding)
275
- end
276
-
277
- # Gets the original HTML as a string.
278
- # @return [String] HTML.
279
- def to_s
280
- @doc.to_xml
281
- end
282
-
283
- # Load the HTML file and convert it into an Oga document.
284
- #
285
- # @return [::Oga::XML::Document] a document.
286
- def load_html(input) # :nodoc:
287
- thing = nil
288
-
289
- # TODO: duplicate options
290
- if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
291
- thing = input
292
- elsif @is_local_file
293
- @base_dir = File.dirname(input)
294
- thing = File.open(input, 'r')
295
- else
296
- thing = open(input)
297
- end
298
-
299
- ::Oga.parse_html(thing)
300
- end
301
- end
302
- end
303
- end
@@ -1,7 +0,0 @@
1
- module Pakyow
2
- module Helpers
3
- def mailer(view_path)
4
- Mailer.from_store(view_path, @presenter.store, self)
5
- end
6
- end
7
- end
@@ -1,76 +0,0 @@
1
- module Pakyow
2
- class Mailer
3
- attr_accessor :view, :message, :processed
4
-
5
- def self.from_store(view_path, view_store, context = nil)
6
- view = view_store.view(view_path)
7
- new(view: Pakyow::Presenter::ViewContext.new(view, context))
8
- end
9
-
10
- def initialize(view: nil, content: nil)
11
- @view = view
12
- @content = content
13
-
14
- @message = Mail.new
15
- @message.from = Config.mailer.default_sender
16
- @message.content_type = Config.mailer.default_content_type
17
- @message.delivery_method(Config.mailer.delivery_method, Config.mailer.delivery_options)
18
- end
19
-
20
- def deliver_to(recipient, subject = nil)
21
- html = content :html
22
- text = content :text
23
-
24
- if html.nil?
25
- @message.body = text
26
- else
27
- @message.html_part do
28
- content_type 'text/html; charset=' + Config.mailer.encoding
29
- body html
30
- end
31
-
32
- @message.text_part do
33
- body text
34
- end
35
- end
36
-
37
- @message.subject = subject if subject
38
-
39
- Array(recipient).each {|r| deliver(r)}
40
- end
41
-
42
- def content(type = :html)
43
- return process.fetch(type, nil)
44
- end
45
-
46
- protected
47
-
48
- def process
49
- unless @processed
50
- @processed_content = {}
51
-
52
- if @view
53
- @premailer = Premailer.new(view.to_html, with_html_string: true, input_encoding: Config.mailer.encoding)
54
-
55
- @premailer.warnings.each do |w|
56
- Pakyow.logger.warn "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
57
- end
58
-
59
- @processed_content[:text] = @content || @premailer.to_plain_text
60
- @processed_content[:html] = @premailer.to_inline_css
61
- else
62
- @processed_content[:text] = @content
63
- end
64
-
65
- @processed = true
66
- end
67
-
68
- return @processed_content
69
- end
70
-
71
- def deliver(recipient)
72
- @message.to = recipient
73
- @message.deliver
74
- end
75
- end
76
- end
@@ -1,9 +0,0 @@
1
- require 'mail'
2
- require 'premailer'
3
-
4
- require 'pakyow/mailer/mailer'
5
- require 'pakyow/mailer/config/mailer'
6
- require 'pakyow/mailer/helpers'
7
- require 'pakyow/mailer/ext/premailer/adapter/oga'
8
-
9
- Premailer::Adapter.use = :oga
@@ -1 +0,0 @@
1
- require 'pakyow/mailer'