pakyow-mailer 0.11.3 → 1.0.0.rc1

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
- 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'