inkcite 1.14.0 → 1.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/assets/init/config.yml +2 -0
- data/assets/social/facebook.png +0 -0
- data/assets/social/instagram.png +0 -0
- data/assets/social/pintrest.png +0 -0
- data/assets/social/twitter.png +0 -0
- data/inkcite.gemspec +2 -2
- data/lib/inkcite.rb +1 -0
- data/lib/inkcite/cli/base.rb +32 -6
- data/lib/inkcite/cli/preview.rb +24 -28
- data/lib/inkcite/cli/server.rb +22 -19
- data/lib/inkcite/cli/test.rb +1 -1
- data/lib/inkcite/email.rb +8 -4
- data/lib/inkcite/facade/animation.rb +4 -0
- data/lib/inkcite/facade/keyframe.rb +26 -3
- data/lib/inkcite/image/base.rb +38 -0
- data/lib/inkcite/image/guetzli_minifier.rb +62 -0
- data/lib/inkcite/image/image_minifier.rb +143 -0
- data/lib/inkcite/image/image_optim_minifier.rb +90 -0
- data/lib/inkcite/image/mozjpeg_minifier.rb +92 -0
- data/lib/inkcite/mailer.rb +201 -112
- data/lib/inkcite/minifier.rb +2 -146
- data/lib/inkcite/post_processor.rb +13 -0
- data/lib/inkcite/renderer.rb +19 -0
- data/lib/inkcite/renderer/background.rb +53 -14
- data/lib/inkcite/renderer/base.rb +29 -15
- data/lib/inkcite/renderer/button.rb +1 -1
- data/lib/inkcite/renderer/carousel.rb +245 -0
- data/lib/inkcite/renderer/container_base.rb +10 -0
- data/lib/inkcite/renderer/div.rb +1 -3
- data/lib/inkcite/renderer/fireworks.rb +54 -40
- data/lib/inkcite/renderer/footnote.rb +22 -2
- data/lib/inkcite/renderer/image.rb +11 -0
- data/lib/inkcite/renderer/image_base.rb +3 -6
- data/lib/inkcite/renderer/in_browser.rb +4 -0
- data/lib/inkcite/renderer/link.rb +39 -12
- data/lib/inkcite/renderer/mobile_image.rb +1 -1
- data/lib/inkcite/renderer/responsive.rb +9 -1
- data/lib/inkcite/renderer/social.rb +31 -3
- data/lib/inkcite/renderer/special_effect.rb +22 -13
- data/lib/inkcite/renderer/sup.rb +32 -0
- data/lib/inkcite/renderer/table_base.rb +3 -0
- data/lib/inkcite/renderer/topic.rb +76 -0
- data/lib/inkcite/renderer/trademark.rb +47 -0
- data/lib/inkcite/renderer/video_preview.rb +3 -2
- data/lib/inkcite/uploader.rb +2 -3
- data/lib/inkcite/util.rb +51 -0
- data/lib/inkcite/version.rb +1 -1
- data/lib/inkcite/view.rb +140 -54
- data/lib/inkcite/view/context.rb +1 -31
- data/lib/inkcite/view/media_query.rb +6 -0
- data/test/animation_spec.rb +7 -0
- data/test/parser_spec.rb +1 -1
- data/test/renderer/background_spec.rb +16 -12
- data/test/renderer/div_spec.rb +11 -0
- data/test/renderer/footnote_spec.rb +5 -1
- data/test/renderer/image_spec.rb +51 -28
- data/test/renderer/link_spec.rb +20 -8
- data/test/renderer/lorem_spec.rb +2 -2
- data/test/renderer/mobile_image_spec.rb +6 -0
- data/test/renderer/mobile_style_spec.rb +3 -3
- data/test/renderer/redacted_spec.rb +2 -2
- data/test/renderer/social_spec.rb +6 -6
- data/test/renderer/table_spec.rb +4 -0
- data/test/renderer/topic_spec.rb +28 -0
- data/test/renderer/trademark_spec.rb +40 -0
- data/test/renderer/video_preview_spec.rb +1 -1
- data/test/test_helper.rb +14 -0
- data/test/view_spec.rb +4 -0
- metadata +26 -12
- data/assets/init/image_optim.yml +0 -37
@@ -0,0 +1,143 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative 'guetzli_minifier'
|
3
|
+
require_relative 'image_optim_minifier'
|
4
|
+
require_relative 'mozjpeg_minifier'
|
5
|
+
|
6
|
+
module Inkcite
|
7
|
+
module Image
|
8
|
+
class ImageMinifier
|
9
|
+
|
10
|
+
# Directory of optimized images
|
11
|
+
IMAGE_CACHE = 'images-optim'
|
12
|
+
|
13
|
+
# Common extensions
|
14
|
+
GIF = 'gif'
|
15
|
+
JPG = 'jpg'
|
16
|
+
PNG = 'png'
|
17
|
+
|
18
|
+
def self.minify email, img_name, force=false
|
19
|
+
|
20
|
+
# Original, unoptimized source image
|
21
|
+
source_img = File.join(email.image_dir, img_name)
|
22
|
+
|
23
|
+
# Cached, optimized path for this image.
|
24
|
+
cache_path = email.project_file(IMAGE_CACHE)
|
25
|
+
cached_img = File.join(cache_path, File.basename(img_name))
|
26
|
+
|
27
|
+
# Grab the first file that exists for this project.
|
28
|
+
config_path = email.config_file
|
29
|
+
|
30
|
+
unless force
|
31
|
+
|
32
|
+
# Get the last-modified date of the image optimization config
|
33
|
+
# file - if that file is newer than the image, re-optimization
|
34
|
+
# is necessary because the settings have changed.
|
35
|
+
config_last_modified = Util.last_modified(config_path)
|
36
|
+
|
37
|
+
# Get the last-modified date of the actual image. If the source
|
38
|
+
# image is newer than the cached version, we'll need to run it
|
39
|
+
# through optimization again, too.
|
40
|
+
cache_last_modified = Util.last_modified(cached_img)
|
41
|
+
source_last_modified = Util.last_modified(source_img)
|
42
|
+
|
43
|
+
# Nothing to do unless the image in the cache is older than the
|
44
|
+
# source or the config file.
|
45
|
+
return unless config_last_modified > cache_last_modified || source_last_modified > cache_last_modified
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
# Make sure the image cache directory exists
|
50
|
+
FileUtils.mkpath(cache_path)
|
51
|
+
|
52
|
+
# Copy the original image to the cache where it can be processed.
|
53
|
+
FileUtils.copy(source_img, cached_img)
|
54
|
+
|
55
|
+
# Get the file format (e.g. gif) of the file being optimized.
|
56
|
+
source_ext = Util::file_extension(source_img)
|
57
|
+
|
58
|
+
# This will hold the list of minifiers that will be applied to the
|
59
|
+
# image, based on its extension.
|
60
|
+
pipeline = []
|
61
|
+
|
62
|
+
# True if ImageOptim is allowed to make lossy optimizations to the
|
63
|
+
# images. When false, even if the quality settings allow it, ImageOptim
|
64
|
+
# won't make lossy optimizations.
|
65
|
+
allow_lossy = true
|
66
|
+
|
67
|
+
if source_ext == JPG
|
68
|
+
pipeline << MozjpegMinifier.new
|
69
|
+
pipeline << GuetzliMinifier.new
|
70
|
+
allow_lossy = false
|
71
|
+
end
|
72
|
+
|
73
|
+
# Always optimize with ImageOptim although for JPGs additional
|
74
|
+
# lossy compression is force disabled.
|
75
|
+
pipeline << ImageOptimMinifier.new(allow_lossy)
|
76
|
+
|
77
|
+
original_size = File.size(source_img)
|
78
|
+
|
79
|
+
msg = "Compressing #{img_name} #{Util.pretty_file_size(original_size)}"
|
80
|
+
|
81
|
+
# Process the image
|
82
|
+
pipeline.each do |p|
|
83
|
+
|
84
|
+
# Minifiers don't work well when the source and destination images are the
|
85
|
+
# same files - so move the image to a temporary file so the minifier can
|
86
|
+
# optimize it back into place.
|
87
|
+
temp_img = "#{cached_img}.tmp"
|
88
|
+
FileUtils.move(cached_img, temp_img)
|
89
|
+
|
90
|
+
temp_size = File.size(temp_img)
|
91
|
+
|
92
|
+
# Let the processor compress the image
|
93
|
+
p.minify!(email, temp_img, cached_img)
|
94
|
+
|
95
|
+
compressed_size = File.size(cached_img)
|
96
|
+
if compressed_size < temp_size
|
97
|
+
msg << " >> #{p.name} #{Util.pretty_file_size(compressed_size)}"
|
98
|
+
|
99
|
+
else
|
100
|
+
|
101
|
+
# Occassionally the compressor does the wrong thing and
|
102
|
+
# makes the image bigger (particularly ImageOptim after
|
103
|
+
# Guetzli) so in that case, revert the image to its
|
104
|
+
# smaller pre-optimization form.
|
105
|
+
FileUtils.copy(temp_img, cached_img)
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
# Now remove the temp file.
|
110
|
+
FileUtils.remove(temp_img) if File.exists?(temp_img)
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get the final compressed size of the image so we can print the
|
115
|
+
# resulting compression ratio.
|
116
|
+
compressed_size = File.size(cached_img)
|
117
|
+
msg << " (#{self.compressed_percent(original_size, compressed_size)}%)"
|
118
|
+
|
119
|
+
Util.log msg
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
# Minifies all of the images in the provided email's project directory.
|
124
|
+
def self.minify_all email, force=false
|
125
|
+
|
126
|
+
images_path = File.join(email.image_dir, '*.*')
|
127
|
+
|
128
|
+
# Iterate through all of the images in the project and optimize them
|
129
|
+
# if necessary.
|
130
|
+
Dir.glob(images_path).each do |img|
|
131
|
+
self.minify(email, File.basename(img), force)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.compressed_percent original_size, compressed_size
|
137
|
+
((1.0 - (compressed_size / original_size.to_f)) * 100).round(1)
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Image
|
3
|
+
class ImageOptimMinifier < ImageMinifier::Base
|
4
|
+
|
5
|
+
def initialize allow_lossy=true
|
6
|
+
super('ImageOptim')
|
7
|
+
@allow_lossy = allow_lossy
|
8
|
+
end
|
9
|
+
|
10
|
+
def minify! email, source_img, cache_img
|
11
|
+
|
12
|
+
config = email.config
|
13
|
+
|
14
|
+
img_type = Util::file_extension(cache_img)
|
15
|
+
|
16
|
+
# This will hold the settings that control how imageoptim
|
17
|
+
# compresses the image, based on extension.
|
18
|
+
optim_opt = {
|
19
|
+
:verbose => false,
|
20
|
+
:svgo => false
|
21
|
+
}
|
22
|
+
|
23
|
+
FileUtils.copy(source_img, cache_img)
|
24
|
+
|
25
|
+
case img_type
|
26
|
+
when ImageMinifier::GIF
|
27
|
+
mix_gif_options email, config, optim_opt
|
28
|
+
when ImageMinifier::JPG
|
29
|
+
mix_jpg_options email, config, optim_opt
|
30
|
+
when ImageMinifier::PNG
|
31
|
+
mix_png_options email, config, optim_opt
|
32
|
+
else
|
33
|
+
# Don't know how to compress this type of image so
|
34
|
+
# just leave it alone
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
ImageOptim.new(optim_opt).optimize_image!(cache_img)
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Name of the configuration field that controls ImageOptim's JPG quality
|
45
|
+
IMAGEOPTIM_JPG_QUALITY = :'imageopt-jpg-quality'
|
46
|
+
|
47
|
+
# Default JPG image quality
|
48
|
+
DEFAULT_JPG_QUALITY = 85
|
49
|
+
|
50
|
+
# Quality constraints
|
51
|
+
MIN_QUALITY = 0
|
52
|
+
MAX_QUALITY = 100
|
53
|
+
|
54
|
+
def mix_gif_options email, config, opts
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
def mix_jpg_options email, config, opts
|
59
|
+
|
60
|
+
max_quality = get_jpg_quality(config, IMAGEOPTIM_JPG_QUALITY, DEFAULT_JPG_QUALITY)
|
61
|
+
|
62
|
+
# Anything less than 100% quality means lossy is enabled.
|
63
|
+
lossy = @allow_lossy && max_quality < 100
|
64
|
+
opts[:allow_lossy] = lossy
|
65
|
+
|
66
|
+
# Additional configuration necessary only if lossy compression
|
67
|
+
# is enabled. Otherwise, the defaults are acceptable.
|
68
|
+
if lossy
|
69
|
+
opts[:jpegoptim] = {
|
70
|
+
:allow_lossy => lossy,
|
71
|
+
:max_quality => max_quality
|
72
|
+
}
|
73
|
+
|
74
|
+
opts[:jpegrecompress] = {
|
75
|
+
:allow_lossy => lossy,
|
76
|
+
:quality => (max_quality / 100.0 * 3).round(0)
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def mix_png_options email, config, opts
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'mozjpeg'
|
2
|
+
|
3
|
+
module Inkcite
|
4
|
+
module Image
|
5
|
+
class MozjpegMinifier < ImageMinifier::Base
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super('Mozjpeg')
|
9
|
+
end
|
10
|
+
|
11
|
+
def minify! email, source_img, cache_img
|
12
|
+
|
13
|
+
config = email.config
|
14
|
+
|
15
|
+
cmd = []
|
16
|
+
|
17
|
+
# mozjpeg usage documentation available
|
18
|
+
# https://github.com/mozilla/mozjpeg/blob/master/usage.txt
|
19
|
+
cmd << Mozjpeg.cjpeg_path
|
20
|
+
|
21
|
+
chrominance_quality = get_jpg_quality(config, MOZJPEG_CHROMINANCE_QUALITY, DEFAULT_QUALITY)
|
22
|
+
|
23
|
+
# The human eye is more sensitive to spatial changes in brightness than
|
24
|
+
# spatial changes in color, the chrominance components can be quantized more
|
25
|
+
# than the luminance components without incurring any visible image quality loss.
|
26
|
+
luminance_quality = (config[MOZJPEG_LUMINANCE_QUALITY] || DEFAULT_QUALITY).to_i # Recommended default
|
27
|
+
luminance_quality = MAX_QUALITY if luminance_quality > MAX_QUALITY
|
28
|
+
|
29
|
+
cmd << "-quality #{luminance_quality},#{chrominance_quality}"
|
30
|
+
|
31
|
+
# Perform optimization of entropy encoding parameters.
|
32
|
+
# -optimize usually makes the JPEG file a little smaller,
|
33
|
+
# but cjpeg runs somewhat slower and needs much more
|
34
|
+
# memory. Image quality and speed of decompression are
|
35
|
+
# unaffected by -optimize.
|
36
|
+
cmd << '-optimize'
|
37
|
+
|
38
|
+
# Produce progressive JPEGs
|
39
|
+
cmd << '-progressive'
|
40
|
+
|
41
|
+
subsampling = (config[MOZJPEG_SUBSAMPLING] || DEFAULT_SUBSAMPLING).to_i
|
42
|
+
cmd << "-sample #{subsampling}x#{subsampling}"
|
43
|
+
|
44
|
+
# Reduces posterization in lower-quality JPEGs
|
45
|
+
# https://calendar.perfplanet.com/2014/mozjpeg-3-0/
|
46
|
+
quant_table = (config[MOZJPEG_QUANT_TABLE] || MS_SSIM).to_i
|
47
|
+
cmd << "-quant-table #{quant_table}"
|
48
|
+
|
49
|
+
# Makes images sharper, at the cost of increased file size
|
50
|
+
# https://calendar.perfplanet.com/2014/mozjpeg-3-0/
|
51
|
+
cmd << '-notrellis'
|
52
|
+
|
53
|
+
cmd << %Q(-outfile "#{cache_img}")
|
54
|
+
cmd << %Q("#{source_img}")
|
55
|
+
|
56
|
+
Util::exec(cmd.join(' '))
|
57
|
+
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Name of the configuration attributes used to control
|
64
|
+
# JPEG compression.
|
65
|
+
MOZJPEG_LUMINANCE_QUALITY = :'mozjpeg-luminance-quality'
|
66
|
+
MOZJPEG_CHROMINANCE_QUALITY = :'mozjpeg-chrominance-quality'
|
67
|
+
MOZJPEG_QUANT_TABLE = :'mozjpeg-quant-table'
|
68
|
+
MOZJPEG_SUBSAMPLING = :'mozjpeg-subsampling'
|
69
|
+
|
70
|
+
# Default compression quality for images unless specified in the
|
71
|
+
# configuration file under 'mozjpeg-quality'
|
72
|
+
DEFAULT_QUALITY = 85
|
73
|
+
|
74
|
+
# Default subsampling
|
75
|
+
DEFAULT_SUBSAMPLING = 1
|
76
|
+
|
77
|
+
# Quality limits
|
78
|
+
MIN_QUALITY = 0
|
79
|
+
MAX_QUALITY = 100
|
80
|
+
|
81
|
+
# Quant table constants per the mozjpeg man page
|
82
|
+
JPEG_ANNEX_K = 0
|
83
|
+
FLAT = 1
|
84
|
+
MS_SSIM = 2
|
85
|
+
IMAGEMAGICK = 3
|
86
|
+
PSNR_HVS = 4
|
87
|
+
KLEIN_SILVERSTEIN_CARNEY = 5
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
data/lib/inkcite/mailer.rb
CHANGED
@@ -4,166 +4,258 @@ require 'mailgun'
|
|
4
4
|
module Inkcite
|
5
5
|
class Mailer
|
6
6
|
|
7
|
-
def self.
|
7
|
+
def self.send_test email, test_address, opts
|
8
8
|
|
9
|
-
# Determine
|
10
|
-
count =
|
9
|
+
# Determine the number of times we've emailed to this list
|
10
|
+
count = get_count(email, :test, opts)
|
11
11
|
|
12
|
-
#
|
13
|
-
|
12
|
+
# Check to see if a specific version is requested or if unspecified
|
13
|
+
# all versions of the email should be sent.
|
14
|
+
versions = get_versions(email, opts)
|
14
15
|
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
16
|
+
# Will hold the instance of the Mailer::Base that will handle the
|
17
|
+
# actual sending of the email.
|
18
|
+
mailer_base = get_mailer_base(email)
|
18
19
|
|
19
|
-
|
20
|
-
# additional set of recipients for this initial mailing.
|
21
|
-
if count == 1
|
22
|
-
also_to = recipients[FIRST_PREVIEW]
|
23
|
-
#to = [* to] + [* also_to] unless also_to.blank?
|
24
|
-
to = [* also_to] unless also_to.blank?
|
25
|
-
end
|
20
|
+
versions.each do |version|
|
26
21
|
|
27
|
-
|
28
|
-
|
22
|
+
# The version of the email we will be sending.
|
23
|
+
view = email.view(:preview, :email, version)
|
29
24
|
|
30
|
-
|
31
|
-
|
32
|
-
:cc => cc,
|
33
|
-
:bcc => true,
|
34
|
-
:tag => "Preview ##{count}"
|
35
|
-
}))
|
25
|
+
subject = "#{view.subject} (Test ##{count})"
|
26
|
+
print "Sending '#{subject}' ... "
|
36
27
|
|
37
|
-
|
28
|
+
mailer_base.send!({ :to => test_address }, subject, view.render!)
|
38
29
|
|
39
|
-
|
30
|
+
puts 'Sent!'
|
40
31
|
|
41
|
-
|
32
|
+
end
|
42
33
|
|
43
|
-
|
44
|
-
|
45
|
-
}))
|
34
|
+
# Increment the count now that we've successfully emailed this list
|
35
|
+
increment email, :test
|
46
36
|
|
47
37
|
end
|
48
38
|
|
49
|
-
def self.
|
39
|
+
def self.send_to_list email, list, opts
|
40
|
+
|
41
|
+
# Determine the number of times we've emailed to this list
|
42
|
+
count = get_count(email, list, opts)
|
43
|
+
|
44
|
+
# Check to see if a specific version is requested or if unspecified
|
45
|
+
# all versions of the email should be sent.
|
46
|
+
versions = get_versions(email, opts)
|
47
|
+
|
48
|
+
# Will hold the instance of the Mailer::Base that will handle the
|
49
|
+
# actual sending of the email.
|
50
|
+
mailer_base = get_mailer_base(email)
|
51
|
+
|
52
|
+
# Get the email address from which the previews will be sent.
|
53
|
+
from = mailer_base.get_from_address
|
54
|
+
|
55
|
+
versions.each do |version|
|
56
|
+
|
57
|
+
# The version of the email we will be sending.
|
58
|
+
view = email.view(:preview, :email, version)
|
59
|
+
|
60
|
+
# Subject line tag such as "Preview #3"
|
61
|
+
tag = "#{opts[:tag]} ##{count}"
|
62
|
+
|
63
|
+
subject = view.subject
|
64
|
+
subject = "#{subject} (#{tag})" unless tag.blank?
|
65
|
+
|
66
|
+
# Get the list of recipients for this version
|
67
|
+
recipients = get_recipients(list, view, count, from)
|
50
68
|
|
51
|
-
|
69
|
+
# Get the total number of recipients for this version
|
70
|
+
total_recipients = recipients.inject(0) { |total, (k, v)| total + v.length }
|
52
71
|
|
53
|
-
|
54
|
-
count = increment(email, :internal)
|
72
|
+
print "Sending '#{subject}' to #{total_recipients} recipient#{'s' if total_recipients > 1} ... "
|
55
73
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
74
|
+
mailer_base.send! recipients, subject, view.render!
|
75
|
+
|
76
|
+
puts 'Sent!'
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
# Increment the count now that we've successfully emailed this list
|
81
|
+
increment email, list
|
61
82
|
|
62
83
|
end
|
63
84
|
|
64
|
-
|
65
|
-
def self.send email, opts
|
85
|
+
private
|
66
86
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
87
|
+
# Name of the distribution list used on the first preview. For one
|
88
|
+
# client, they wanted the first preview sent to additional people
|
89
|
+
# but subsequent previews went to a shorter list.
|
90
|
+
FIRST_PREVIEW = :'first-preview'
|
71
91
|
|
72
|
-
|
73
|
-
|
74
|
-
|
92
|
+
def self.comma_set_includes? _set, value
|
93
|
+
_set.blank? || _set.split(',').collect(&:to_sym).include?(value.to_sym)
|
94
|
+
end
|
75
95
|
|
76
|
-
|
77
|
-
|
96
|
+
def self.get_count email, sym, opts
|
97
|
+
opts[:count] || email.meta(sym).to_i + 1
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.get_mailer_base email
|
78
101
|
mailer_base = nil
|
79
102
|
|
80
103
|
# Check to see if
|
81
104
|
if config = email.config[:mailgun]
|
82
|
-
mailer_base = MailgunMailer.new
|
105
|
+
mailer_base = MailgunMailer.new(config)
|
83
106
|
elsif config = email.config[:smtp]
|
84
|
-
mailer_base = SmtpMailer.new
|
107
|
+
mailer_base = SmtpMailer.new(config)
|
85
108
|
else
|
86
109
|
abort <<-USAGE.strip_heredoc
|
87
110
|
|
88
|
-
|
89
|
-
|
111
|
+
Oops! Inkcite can't send this email because of a configuration problem.
|
112
|
+
Please update the mailgun or smtp sections of your config.yml file.
|
90
113
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
114
|
+
smtp:
|
115
|
+
host: 'smtp.gmail.com'
|
116
|
+
port: 587
|
117
|
+
domain: 'yourdomain.com'
|
118
|
+
username: ''
|
119
|
+
password: ''
|
120
|
+
from: 'Your Name <email@domain.com>'
|
98
121
|
|
99
|
-
|
122
|
+
Or send via Mailgun:
|
100
123
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
124
|
+
mailgun:
|
125
|
+
api-key: 'key-your-api-key'
|
126
|
+
domain: 'mg.sending-domain.com'
|
127
|
+
from: 'Your Name <email@domain.com>'
|
105
128
|
|
106
129
|
USAGE
|
107
130
|
end
|
108
131
|
|
109
|
-
|
132
|
+
mailer_base
|
133
|
+
end
|
110
134
|
|
111
|
-
|
112
|
-
view = email.view(:preview, :email, version)
|
135
|
+
def self.get_recipients list, view, count, from
|
113
136
|
|
114
|
-
|
115
|
-
tag = opts[:tag]
|
137
|
+
recipients = { :to => [], :cc => [], :bcc => [] }
|
116
138
|
|
117
|
-
|
118
|
-
|
139
|
+
# Developer list always only sends to the original from address.
|
140
|
+
if list == :developer
|
141
|
+
recipients[:to] << from
|
142
|
+
|
143
|
+
else
|
144
|
+
|
145
|
+
# Always bcc the developer of the email
|
146
|
+
recipients[:bcc] << from
|
147
|
+
|
148
|
+
# Check to see if there is a TSV file which allows for maximum
|
149
|
+
# configurability of the recipient list.
|
150
|
+
recipients_file = view.email.project_file('recipients.tsv')
|
151
|
+
if File.exist?(recipients_file)
|
152
|
+
|
153
|
+
# Iterate through the recipients file and determine which entries match the
|
154
|
+
# list, version and preview count...
|
155
|
+
Util.each_line(recipients_file, false) do |line|
|
156
|
+
|
157
|
+
# Skip comments
|
158
|
+
next if line.start_with?('#')
|
119
159
|
|
120
|
-
|
160
|
+
name, email, _list, delivery, min_preview, max_preview, versions = line.split("\t")
|
161
|
+
next if name.blank? || email.blank?
|
121
162
|
|
122
|
-
|
163
|
+
# Skip this recipient unless the distribution list matches the one
|
164
|
+
# we're looking for.
|
165
|
+
next unless _list.to_sym == list
|
166
|
+
|
167
|
+
# Skip this recipient unless we've reached the minimum number of
|
168
|
+
# earlier previews for this recipient - e.g. they only receive the
|
169
|
+
# 2nd previews and beyond
|
170
|
+
min_preview = min_preview.to_i
|
171
|
+
next if min_preview > 0 && count < min_preview
|
172
|
+
|
173
|
+
# Skip this recipient if we've already delivered the maximum number
|
174
|
+
# of previews they should receive - e.g. they only receive the
|
175
|
+
# first preview, no additional previews.
|
176
|
+
max_preview = max_preview.to_i
|
177
|
+
next if max_preview > 0 && count > max_preview
|
178
|
+
|
179
|
+
# Skip this recipient unless
|
180
|
+
next unless comma_set_includes?(versions, view.version)
|
181
|
+
|
182
|
+
delivery = delivery.blank? ? :to : delivery.to_sym
|
183
|
+
recipient = "#{name} <#{email}>"
|
184
|
+
recipients[delivery] << recipient
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
else
|
189
|
+
|
190
|
+
# Grab the array of recipients from the config.yml
|
191
|
+
recipient_yml = view[:recipients]
|
192
|
+
|
193
|
+
case list
|
194
|
+
when :client
|
195
|
+
first_preview = count == 1 ? recipient_yml[FIRST_PREVIEW] : nil
|
196
|
+
clients = recipient_yml[:clients] || recipient_yml[:client]
|
197
|
+
recipients[:to] = first_preview || clients
|
198
|
+
recipients[:cc] = recipient_yml[:internal]
|
199
|
+
when :internal
|
200
|
+
recipients[:to] = recipient_yml[:internal]
|
201
|
+
when :developer
|
202
|
+
recipients[:to] = from
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
123
206
|
|
124
207
|
end
|
125
208
|
|
209
|
+
return recipients
|
126
210
|
end
|
127
211
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
# client, they wanted the first preview sent to additional people
|
132
|
-
# but subsequent previews went to a shorter list.
|
133
|
-
FIRST_PREVIEW = :'first-preview'
|
212
|
+
def self.get_versions email, opts
|
213
|
+
Array(opts[:version] || email.versions)
|
214
|
+
end
|
134
215
|
|
135
216
|
def self.increment email, sym
|
136
|
-
|
137
|
-
email.set_meta sym, count
|
217
|
+
email.set_meta sym, get_count(email, sym, {})
|
138
218
|
end
|
139
219
|
|
140
220
|
# Abstract base class for the workhorses of the Mailer class.
|
141
221
|
# Instantiated based on the config.yml settings.
|
142
222
|
class Base
|
143
|
-
|
223
|
+
|
224
|
+
attr_accessor :config
|
225
|
+
|
226
|
+
def initialize config
|
227
|
+
@config = config
|
228
|
+
end
|
229
|
+
|
230
|
+
def get_from_address
|
231
|
+
@config[:from]
|
232
|
+
end
|
233
|
+
|
234
|
+
def send! recipients, subject, content
|
144
235
|
raise NotImplementedError
|
145
236
|
end
|
146
237
|
end
|
147
238
|
|
148
239
|
class MailgunMailer < Base
|
149
|
-
def
|
240
|
+
def initialize config
|
241
|
+
super(config)
|
242
|
+
end
|
150
243
|
|
151
|
-
|
152
|
-
from = config[:from]
|
244
|
+
def send! recipients, subject, content
|
153
245
|
|
154
246
|
# First, instantiate the Mailgun Client with your API key
|
155
247
|
mg_client = Mailgun::Client.new config[:'api-key']
|
156
248
|
|
157
249
|
# Define your message parameters
|
158
250
|
message_params = {
|
159
|
-
:from =>
|
160
|
-
:to =>
|
251
|
+
:from => get_from_address,
|
252
|
+
:to => recipients[:to],
|
161
253
|
:subject => subject,
|
162
|
-
:html =>
|
254
|
+
:html => content
|
163
255
|
}
|
164
256
|
|
165
|
-
message_params[:cc] =
|
166
|
-
message_params[:bcc] =
|
257
|
+
message_params[:cc] = recipients[:cc] unless recipients[:cc].blank?
|
258
|
+
message_params[:bcc] = recipients[:bcc] unless recipients[:bcc].blank?
|
167
259
|
|
168
260
|
# Send your message through the client
|
169
261
|
mg_client.send_message config[:domain], message_params
|
@@ -172,41 +264,38 @@ module Inkcite
|
|
172
264
|
end
|
173
265
|
|
174
266
|
class SmtpMailer < Base
|
175
|
-
def
|
267
|
+
def initialize config
|
268
|
+
super(config)
|
269
|
+
end
|
270
|
+
|
271
|
+
def send! recipients, subject, content
|
272
|
+
|
273
|
+
_config = config
|
176
274
|
|
177
275
|
Mail.defaults do
|
178
276
|
delivery_method :smtp, {
|
179
|
-
:address =>
|
180
|
-
:port =>
|
181
|
-
:user_name =>
|
182
|
-
:password =>
|
277
|
+
:address => _config[:host],
|
278
|
+
:port => _config[:port],
|
279
|
+
:user_name => _config[:username],
|
280
|
+
:password => _config[:password],
|
183
281
|
:authentication => :plain,
|
184
282
|
:enable_starttls_auto => true
|
185
283
|
}
|
186
284
|
end
|
187
285
|
|
188
|
-
# The address of the developer
|
189
|
-
_from = config[:from]
|
190
|
-
|
191
|
-
# True if the developer should be bcc'd.
|
192
|
-
_bcc = !!opt[:bcc]
|
193
|
-
|
194
286
|
mail = Mail.new do
|
195
|
-
|
196
|
-
to opt[:to] || _from
|
197
|
-
cc opt[:cc]
|
198
|
-
from _from
|
199
|
-
subject _subject
|
200
|
-
|
201
|
-
bcc(_from) if _bcc
|
202
|
-
|
203
287
|
html_part do
|
204
288
|
content_type 'text/html; charset=UTF-8'
|
205
|
-
body
|
289
|
+
body content
|
206
290
|
end
|
207
|
-
|
208
291
|
end
|
209
292
|
|
293
|
+
mail[:to] = recipients[:to]
|
294
|
+
mail[:cc] = recipients[:cc]
|
295
|
+
mail[:bcc] = recipients[:bcc]
|
296
|
+
mail[:from] = get_from_address
|
297
|
+
mail[:subject] = subject
|
298
|
+
|
210
299
|
mail.deliver!
|
211
300
|
|
212
301
|
end
|