courrier 0.5.0
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 +7 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +109 -0
- data/README.md +243 -0
- data/Rakefile +9 -0
- data/bin/console +11 -0
- data/bin/release +19 -0
- data/bin/setup +8 -0
- data/courrier.gemspec +25 -0
- data/lib/courrier/configuration/preview.rb +23 -0
- data/lib/courrier/configuration/providers.rb +41 -0
- data/lib/courrier/configuration.rb +53 -0
- data/lib/courrier/email/address.rb +37 -0
- data/lib/courrier/email/layouts.rb +40 -0
- data/lib/courrier/email/options.rb +55 -0
- data/lib/courrier/email/provider.rb +68 -0
- data/lib/courrier/email/providers/base.rb +39 -0
- data/lib/courrier/email/providers/logger.rb +41 -0
- data/lib/courrier/email/providers/loops.rb +36 -0
- data/lib/courrier/email/providers/mailgun.rb +36 -0
- data/lib/courrier/email/providers/mailjet.rb +50 -0
- data/lib/courrier/email/providers/mailpace.rb +32 -0
- data/lib/courrier/email/providers/postmark.rb +34 -0
- data/lib/courrier/email/providers/preview/default.html.erb +119 -0
- data/lib/courrier/email/providers/preview.rb +51 -0
- data/lib/courrier/email/providers/sendgrid.rb +57 -0
- data/lib/courrier/email/providers/sparkpost.rb +38 -0
- data/lib/courrier/email/request.rb +73 -0
- data/lib/courrier/email/result.rb +43 -0
- data/lib/courrier/email/transformer.rb +46 -0
- data/lib/courrier/email.rb +97 -0
- data/lib/courrier/errors.rb +11 -0
- data/lib/courrier/railtie.rb +23 -0
- data/lib/courrier/tasks/courrier.rake +13 -0
- data/lib/courrier/version.rb +3 -0
- data/lib/courrier.rb +10 -0
- data/lib/generators/courrier/email_generator.rb +19 -0
- data/lib/generators/courrier/install_generator.rb +11 -0
- data/lib/generators/courrier/templates/email.rb.tt +15 -0
- data/lib/generators/courrier/templates/initializer.rb.tt +38 -0
- metadata +125 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "courrier/email/transformer"
|
4
|
+
|
5
|
+
module Courrier
|
6
|
+
class Email
|
7
|
+
class Options
|
8
|
+
attr_reader :from, :to, :reply_to, :cc, :bcc, :subject
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@from = options.fetch(:from, nil)
|
12
|
+
|
13
|
+
@to = options.fetch(:to, nil)
|
14
|
+
@reply_to = options.fetch(:reply_to, nil)
|
15
|
+
@cc = options.fetch(:cc, nil)
|
16
|
+
@bcc = options.fetch(:bcc, nil)
|
17
|
+
|
18
|
+
@subject = options.fetch(:subject, "")
|
19
|
+
@text = options.fetch(:text, "")
|
20
|
+
@html = options.fetch(:html, "")
|
21
|
+
|
22
|
+
@auto_generate_text = options.fetch(:auto_generate_text, false)
|
23
|
+
|
24
|
+
@layouts = Array(options[:layouts])
|
25
|
+
|
26
|
+
raise Courrier::ArgumentError, "Recipient (`to`) is required" unless @to
|
27
|
+
raise Courrier::ArgumentError, "Sender (`from`) is required" unless @from
|
28
|
+
end
|
29
|
+
|
30
|
+
def text = wrap(transformed_text, with_layout: :text)
|
31
|
+
|
32
|
+
def html = wrap(@html, with_layout: :html)
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def wrap(content, with_layout:)
|
37
|
+
return content if content.nil? || content.empty?
|
38
|
+
|
39
|
+
@layouts.reduce(content) do |wrapped, layout_options|
|
40
|
+
layout = layout_options[with_layout]
|
41
|
+
|
42
|
+
next wrapped if !layout
|
43
|
+
|
44
|
+
layout % {content: wrapped}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def transformed_text
|
49
|
+
return Courrier::Email::Transformer.new(@html).to_text if @auto_generate_text
|
50
|
+
|
51
|
+
@text
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "courrier/email/providers/base"
|
4
|
+
require "courrier/email/providers/logger"
|
5
|
+
require "courrier/email/providers/loops"
|
6
|
+
require "courrier/email/providers/mailgun"
|
7
|
+
require "courrier/email/providers/mailjet"
|
8
|
+
require "courrier/email/providers/mailpace"
|
9
|
+
require "courrier/email/providers/postmark"
|
10
|
+
require "courrier/email/providers/preview"
|
11
|
+
require "courrier/email/providers/sendgrid"
|
12
|
+
require "courrier/email/providers/sparkpost"
|
13
|
+
|
14
|
+
module Courrier
|
15
|
+
class Email
|
16
|
+
class Provider
|
17
|
+
def initialize(provider: nil, api_key: nil, options: {}, provider_options: {})
|
18
|
+
@provider = provider
|
19
|
+
@api_key = api_key
|
20
|
+
@options = options
|
21
|
+
@provider_options = provider_options
|
22
|
+
end
|
23
|
+
|
24
|
+
def deliver
|
25
|
+
raise Courrier::ConfigurationError, "`provider` and `api_key` must be configured for production environment" if configuration_missing_in_production?
|
26
|
+
raise Courrier::ConfigurationError, "Unknown provider. Choose one of `#{comma_separated_providers}` or provide your own." if @provider.empty? || @provider.nil?
|
27
|
+
|
28
|
+
provider_class.new(
|
29
|
+
api_key: @api_key,
|
30
|
+
options: @options,
|
31
|
+
provider_options: @provider_options
|
32
|
+
).deliver
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
PROVIDERS = {
|
38
|
+
logger: Courrier::Email::Providers::Logger,
|
39
|
+
loops: Courrier::Email::Providers::Loops,
|
40
|
+
mailgun: Courrier::Email::Providers::Mailgun,
|
41
|
+
mailjet: Courrier::Email::Providers::Mailjet,
|
42
|
+
mailpace: Courrier::Email::Providers::Mailpace,
|
43
|
+
postmark: Courrier::Email::Providers::Postmark,
|
44
|
+
preview: Courrier::Email::Providers::Preview,
|
45
|
+
sendgrid: Courrier::Email::Providers::Sendgrid,
|
46
|
+
sparkpost: Courrier::Email::Providers::Sparkpost
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
def configuration_missing_in_production?
|
50
|
+
production? && required_attributes_blank?
|
51
|
+
end
|
52
|
+
|
53
|
+
def comma_separated_providers = PROVIDERS.keys.join(", ")
|
54
|
+
|
55
|
+
def provider_class
|
56
|
+
return PROVIDERS[@provider.to_sym] if PROVIDERS.key?(@provider.to_sym)
|
57
|
+
|
58
|
+
Object.const_get(@provider)
|
59
|
+
end
|
60
|
+
|
61
|
+
def required_attributes_blank? = @api_key.empty?
|
62
|
+
|
63
|
+
def production?
|
64
|
+
defined?(Rails) && Rails.env.production?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "courrier/email/request"
|
4
|
+
|
5
|
+
module Courrier
|
6
|
+
class Email
|
7
|
+
module Providers
|
8
|
+
class Base
|
9
|
+
def initialize(api_key: nil, options: {}, provider_options: {})
|
10
|
+
@api_key = api_key
|
11
|
+
@options = options
|
12
|
+
@provider_options = provider_options
|
13
|
+
end
|
14
|
+
|
15
|
+
def deliver
|
16
|
+
Request.new(
|
17
|
+
endpoint_url: endpoint_url,
|
18
|
+
body: body,
|
19
|
+
provider: provider,
|
20
|
+
headers: headers,
|
21
|
+
content_type: content_type
|
22
|
+
).post
|
23
|
+
end
|
24
|
+
|
25
|
+
def body = raise Courrier::NotImplementedError, "Provider class must implement `body`"
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def endpoint_url = self.class::ENDPOINT_URL
|
30
|
+
|
31
|
+
def content_type = "application/json"
|
32
|
+
|
33
|
+
def headers = {}
|
34
|
+
|
35
|
+
def provider = self.class.name.split("::").last
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module Courrier
|
6
|
+
class Email
|
7
|
+
module Providers
|
8
|
+
class Logger < Base
|
9
|
+
def deliver
|
10
|
+
logger = Courrier.configuration.logger
|
11
|
+
logger.formatter = proc do |_severity, _datetime, _progname, message|
|
12
|
+
"#{format_email_using(message)}\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
logger.info(@options)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def format_email_using(options)
|
21
|
+
<<~EMAIL
|
22
|
+
#{separator}
|
23
|
+
Timestamp: #{Time.now.strftime("%Y-%m-%d %H:%M:%S %z")}
|
24
|
+
From: #{@options.from}
|
25
|
+
To: #{@options.to}
|
26
|
+
Subject: #{@options.subject}
|
27
|
+
|
28
|
+
Text:
|
29
|
+
#{@options.text || "(empty)"}
|
30
|
+
|
31
|
+
HTML:
|
32
|
+
#{@options.html || "(empty)"}
|
33
|
+
#{separator}
|
34
|
+
EMAIL
|
35
|
+
end
|
36
|
+
|
37
|
+
def separator = "-" * 80
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Loops < Base
|
7
|
+
ENDPOINT_URL = "https://app.loops.so/api/v1/transactional"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"email" => @options.to,
|
12
|
+
"transactionalId" => @provider_options.transactional_id || raise(Courrier::ArgumentError, "Loops requires a `transactionalId`"),
|
13
|
+
"dataVariables" => data_variables
|
14
|
+
}.compact
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def headers
|
20
|
+
{
|
21
|
+
"Authorization" => "Bearer #{@api_key}"
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def data_variables
|
26
|
+
{
|
27
|
+
"from" => @options.from,
|
28
|
+
"subject" => @options.subject,
|
29
|
+
"text_content" => @options.text,
|
30
|
+
"html_content" => @options.html
|
31
|
+
}.compact
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Mailgun < Base
|
7
|
+
ENDPOINT_URL = "https://api.mailgun.net/v3/%{domain}/messages"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"from" => @options.from,
|
12
|
+
|
13
|
+
"to" => @options.to,
|
14
|
+
"h:Reply-To" => @options.reply_to,
|
15
|
+
|
16
|
+
"subject" => @options.subject,
|
17
|
+
"text" => @options.text,
|
18
|
+
"html" => @options.html
|
19
|
+
}.compact
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def endpoint_url
|
25
|
+
domain = @provider_options.domain || raise(Courrier::ArgumentError, "Mailgun requires a `domain`")
|
26
|
+
|
27
|
+
ENDPOINT_URL % {domain: domain}
|
28
|
+
end
|
29
|
+
|
30
|
+
def content_type = "multipart/form-data"
|
31
|
+
|
32
|
+
def authentication_for(request) = request.basic_auth("api", @api_key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Mailjet < Base
|
7
|
+
ENDPOINT_URL = "https://api.mailjet.com/v3.1/send"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"Messages" => [
|
12
|
+
{
|
13
|
+
"From" => {
|
14
|
+
"Email" => @options.from
|
15
|
+
},
|
16
|
+
|
17
|
+
"To" => [
|
18
|
+
{
|
19
|
+
"Email" => @options.to
|
20
|
+
}
|
21
|
+
],
|
22
|
+
"ReplyTo" => reply_to_object,
|
23
|
+
|
24
|
+
"Subject" => @options.subject,
|
25
|
+
"TextPart" => @options.text,
|
26
|
+
"HTMLPart" => @options.html
|
27
|
+
}.compact
|
28
|
+
]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def headers
|
35
|
+
{
|
36
|
+
"Authorization" => "Basic " + Base64.strict_encode64("#{@api_key}:#{@provider_options.api_secret}")
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def reply_to_object
|
41
|
+
return if @options.reply_to.nil?
|
42
|
+
|
43
|
+
{
|
44
|
+
"Email" => @options.reply_to
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Mailpace < Base
|
7
|
+
ENDPOINT_URL = "https://app.mailpace.com/api/v1/send"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"from" => @options.from,
|
12
|
+
|
13
|
+
"to" => @options.to,
|
14
|
+
"replyto" => @options.reply_to,
|
15
|
+
|
16
|
+
"subject" => @options.subject,
|
17
|
+
"textbody" => @options.text,
|
18
|
+
"htmlbody" => @options.html
|
19
|
+
}.compact
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def headers
|
25
|
+
{
|
26
|
+
"MailPace-Server-Token" => @api_key
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Postmark < Base
|
7
|
+
ENDPOINT_URL = "https://api.postmarkapp.com/email"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"From" => @options.from,
|
12
|
+
|
13
|
+
"To" => @options.to,
|
14
|
+
"ReplyTo" => @options.reply_to,
|
15
|
+
|
16
|
+
"Subject" => @options.subject,
|
17
|
+
"TextBody" => @options.text,
|
18
|
+
"HtmlBody" => @options.html,
|
19
|
+
|
20
|
+
"MessageStream" => @provider_options.message_stream || "outbound"
|
21
|
+
}.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def headers
|
27
|
+
{
|
28
|
+
"X-Postmark-Server-Token" => @api_key
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title><%= @options.subject %></title>
|
6
|
+
<style>
|
7
|
+
*, *::before, *::after { box-sizing: border-box; }
|
8
|
+
* { margin: 0; padding: 0; }
|
9
|
+
body { margin: 0; padding: .5rem; line-height: 1.5; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: rgb(241, 245, 249); }
|
10
|
+
pre { margin: 0; white-space: pre-wrap; }
|
11
|
+
.email {
|
12
|
+
width: 100%;
|
13
|
+
max-width: 65rem;
|
14
|
+
margin-left: auto; margin-right: auto;
|
15
|
+
background-color: #fff;
|
16
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
17
|
+
border-radius: 1rem;
|
18
|
+
}
|
19
|
+
|
20
|
+
.email__header { display: flex; align-items: center; padding: 1rem .75rem; -moz-column-gap: .75rem; column-gap: .75rem; }
|
21
|
+
|
22
|
+
.email__avatar {
|
23
|
+
flex-shrink: 0;
|
24
|
+
padding: .25rem;
|
25
|
+
width: 2.5rem; height: 2.5rem;
|
26
|
+
background-color: rgb(203, 213, 225);
|
27
|
+
color: rgb(51, 65, 85);
|
28
|
+
border-radius: 9999px;
|
29
|
+
}
|
30
|
+
|
31
|
+
.email__recipient { flex-grow: 1; font-size: .875rem; line-height: 1.25rem; }
|
32
|
+
|
33
|
+
.email__recipient-name { font-weight: 600; color: rgb(51, 65, 85); }
|
34
|
+
|
35
|
+
.email__metadata { display: flex; align-items: center; justify-content: space-between; }
|
36
|
+
|
37
|
+
.email__recipient-email { color: rgb(100, 116, 139); }
|
38
|
+
|
39
|
+
.email__datetime { font-size: .75rem; line-height: 1rem; color: rgb(148, 163, 184); }
|
40
|
+
|
41
|
+
.email__subject {
|
42
|
+
margin-left: 4rem;
|
43
|
+
font-size: 1.25rem; line-height: 1.75rem;
|
44
|
+
font-weight: 700;
|
45
|
+
letter-spacing: -.025em;
|
46
|
+
color: rgb(30, 41, 59)
|
47
|
+
}
|
48
|
+
|
49
|
+
.email__preview { margin-top: .75rem; margin-left: 4rem; padding: .5rem .25rem; }
|
50
|
+
|
51
|
+
.preview-toggle { display: none; }
|
52
|
+
.preview-toggle-label {
|
53
|
+
display: inline-block;
|
54
|
+
padding: .25rem .875rem;
|
55
|
+
font-size: .75rem; line-height: 1rem;
|
56
|
+
font-weight: 600;
|
57
|
+
color: rgb(30, 41, 59);
|
58
|
+
background-color: rgb(255, 255, 255);
|
59
|
+
border: 1px solid rgb(226, 232, 240);
|
60
|
+
border-radius: .375rem;
|
61
|
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
62
|
+
cursor: pointer;
|
63
|
+
|
64
|
+
&:hover { border-color: rgb(203, 213, 225); }
|
65
|
+
&:active { transform: scale(.98); }
|
66
|
+
}
|
67
|
+
.preview-toggle[disabled] + .preview-toggle-label { opacity: 0.5; cursor: not-allowed; }
|
68
|
+
|
69
|
+
.email__preview-html, .email__preview-text { margin-top: 1.5rem;}
|
70
|
+
.email__preview-html { display: none; }
|
71
|
+
.preview-toggle:checked ~ .email__preview-text { display: none; }
|
72
|
+
.preview-toggle:checked ~ .email__preview-html { display: block; }
|
73
|
+
</style>
|
74
|
+
</head>
|
75
|
+
<body>
|
76
|
+
<article class="email">
|
77
|
+
<header class="email__header">
|
78
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" data-slot="icon" class="email__avatar">
|
79
|
+
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd"/>
|
80
|
+
</svg>
|
81
|
+
|
82
|
+
<div class="email__recipient">
|
83
|
+
<% if name %>
|
84
|
+
<p class="email__recipient-name">
|
85
|
+
<%= name %>
|
86
|
+
</p>
|
87
|
+
<% end %>
|
88
|
+
|
89
|
+
<div class="email__metadata">
|
90
|
+
<p class="email__recipient-email">
|
91
|
+
<%= email %>
|
92
|
+
</p>
|
93
|
+
|
94
|
+
<time class="email__datetime" datetime="#{Time.now.strftime("%Y-%m-%d %H:%M:%S %z")}">
|
95
|
+
<%= Time.now.strftime("%Y-%m-%d %H:%M:%S %z") %>
|
96
|
+
</time>
|
97
|
+
</div>
|
98
|
+
</div>
|
99
|
+
</header>
|
100
|
+
|
101
|
+
<p class="email__subject">
|
102
|
+
<%= @options.subject %>
|
103
|
+
</p>
|
104
|
+
|
105
|
+
<div class="email__preview">
|
106
|
+
<input type="checkbox" id="preview-toggle" class="preview-toggle" <%= html ? "" : "disabled" %>>
|
107
|
+
<label for="preview-toggle" class="preview-toggle-label">Toggle HTML/Text</label>
|
108
|
+
|
109
|
+
<div class="email__preview-text">
|
110
|
+
<pre><%= text || "(no text content)" %></pre>
|
111
|
+
</div>
|
112
|
+
|
113
|
+
<div class="email__preview-html">
|
114
|
+
<%= html || "<p>(no HTML content)</p>" %>
|
115
|
+
</div>
|
116
|
+
</div>
|
117
|
+
</article>
|
118
|
+
</body>
|
119
|
+
</html>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tmpdir"
|
4
|
+
require "fileutils"
|
5
|
+
require "launchy"
|
6
|
+
|
7
|
+
module Courrier
|
8
|
+
class Email
|
9
|
+
module Providers
|
10
|
+
class Preview < Base
|
11
|
+
def deliver
|
12
|
+
FileUtils.mkdir_p(config.destination)
|
13
|
+
|
14
|
+
file_path = File.join(config.destination, "#{Time.now.to_i}.html")
|
15
|
+
|
16
|
+
File.write(file_path, ERB.new(File.read(config.template_path)).result(binding))
|
17
|
+
|
18
|
+
Launchy.open(file_path)
|
19
|
+
|
20
|
+
"Preview email saved to #{file_path}#{config.auto_open ? " and opened in browser" : ""}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def name = extract(@options.to)[:name]
|
24
|
+
|
25
|
+
def email = extract(@options.to)[:email]
|
26
|
+
|
27
|
+
def text = prepare(@options.text)
|
28
|
+
|
29
|
+
def html = prepare(@options.html)
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def extract(to)
|
34
|
+
if to.to_s =~ /(.*?)\s*<(.+?)>/
|
35
|
+
{name: $1.strip, email: $2.strip}
|
36
|
+
else
|
37
|
+
{name: nil, email: to.to_s.strip}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def prepare(content)
|
42
|
+
content.to_s.gsub(URI::DEFAULT_PARSER.make_regexp(%w[http https])) do |url|
|
43
|
+
%(<a href="#{url}">#{url}</a>)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def config = @config ||= Courrier.configuration.preview
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Sendgrid < Base
|
7
|
+
ENDPOINT_URL = "https://api.sendgrid.com/v3/mail/send"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"from" => {
|
12
|
+
"email" => @options.from
|
13
|
+
},
|
14
|
+
"personalizations" => [
|
15
|
+
{
|
16
|
+
"to" => [
|
17
|
+
{
|
18
|
+
"email" => @options.to
|
19
|
+
}
|
20
|
+
]
|
21
|
+
}
|
22
|
+
],
|
23
|
+
"reply_to" => reply_to_object,
|
24
|
+
|
25
|
+
"subject" => @options.subject,
|
26
|
+
"content" => [
|
27
|
+
{
|
28
|
+
"type" => "text/plain",
|
29
|
+
"value" => @options.text
|
30
|
+
},
|
31
|
+
{
|
32
|
+
"type" => "text/html",
|
33
|
+
"value" => @options.html
|
34
|
+
}
|
35
|
+
].compact
|
36
|
+
}.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def headers
|
42
|
+
{
|
43
|
+
"Authorization" => "Bearer #{@api_key}"
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def reply_to_object
|
48
|
+
return if @options.reply_to.nil?
|
49
|
+
|
50
|
+
{
|
51
|
+
"email" => @options.reply_to
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Sparkpost < Base
|
7
|
+
ENDPOINT_URL = "https://api.sparkpost.com/api/v1/transmissions"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"content" => {
|
12
|
+
"reply_to" => @options.reply_to,
|
13
|
+
"from" => @options.from,
|
14
|
+
"subject" => @options.subject,
|
15
|
+
"text" => @options.text,
|
16
|
+
"html" => @options.html
|
17
|
+
}.compact,
|
18
|
+
"recipients" => [
|
19
|
+
{
|
20
|
+
"address" => {
|
21
|
+
"email" => @options.to
|
22
|
+
}
|
23
|
+
}
|
24
|
+
]
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def headers
|
31
|
+
{
|
32
|
+
"Authorization" => @api_key
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|