laneful-ruby 1.1.0 → 1.2.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 +4 -4
- data/lib/laneful/client.rb +138 -17
- data/lib/laneful/models.rb +90 -15
- data/lib/laneful/org_models.rb +452 -0
- data/lib/laneful/version.rb +1 -1
- data/lib/laneful/webhooks.rb +1 -1
- data/lib/laneful.rb +2 -1
- metadata +3 -11
- data/.rubocop.yml +0 -31
- data/LICENSE +0 -21
- data/Makefile +0 -49
- data/README.md +0 -343
- data/Rakefile +0 -8
- data/examples/Gemfile +0 -5
- data/examples/simple_example.rb +0 -123
- data/laneful-ruby.gemspec +0 -38
- data/scripts/publish.sh +0 -169
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed5ff94086f558d6ea4bad6c914ec22cbc7b8d012cc27fc3d385d3cbc05c5c49
|
|
4
|
+
data.tar.gz: 02a6ba4d5c4dbee58809d4e15214623fc273770b3779d73fe6ae25b61de75252
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc30d8a4112104938f451247b7c24654365f4bb6f8efb5dbeacff4947b00841115563bc7e93d4e36b2eb17e3f115a3616d91a99ebff081d0f322fc9ebdca2060
|
|
7
|
+
data.tar.gz: 560628e6bd7d5cefa3b9e3229c4be1806d8703d6a7c61e919b94d386b9b3e4a27f1ea95602096e5783236489f4c234233676ebeadf42591eb3db767ec4cf6875
|
data/lib/laneful/client.rb
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
3
6
|
module Laneful
|
|
4
|
-
# Main client for communicating with the Laneful
|
|
7
|
+
# Main client for communicating with the Laneful API.
|
|
8
|
+
# Email sending uses a send host (https://your-endpoint.send.laneful.net).
|
|
9
|
+
# Domain, unsubscribe-group, and analytics endpoints use the organization
|
|
10
|
+
# API host (https://api.laneful.net).
|
|
5
11
|
class Client
|
|
6
12
|
include HTTParty
|
|
7
13
|
|
|
8
|
-
# Default timeout in seconds
|
|
9
14
|
DEFAULT_TIMEOUT = 30
|
|
10
15
|
|
|
16
|
+
# Repeat array keys as workspace_ids=1&workspace_ids=2 (not workspace_ids[]=).
|
|
17
|
+
query_string_normalizer HTTParty::Request::NON_RAILS_QUERY_STRING_NORMALIZER
|
|
18
|
+
|
|
11
19
|
attr_reader :base_url, :auth_token, :timeout
|
|
12
20
|
|
|
13
21
|
def initialize(base_url, auth_token, timeout: DEFAULT_TIMEOUT)
|
|
@@ -16,26 +24,112 @@ module Laneful
|
|
|
16
24
|
@timeout = timeout
|
|
17
25
|
|
|
18
26
|
validate_configuration!
|
|
19
|
-
|
|
20
|
-
# Configure HTTParty
|
|
21
|
-
self.class.base_uri @base_url
|
|
22
|
-
self.class.default_timeout @timeout
|
|
23
|
-
self.class.headers default_headers
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Sends a single email
|
|
27
|
-
def send_email(email)
|
|
28
|
-
send_emails([email])
|
|
30
|
+
def send_email(email, mail_settings = nil)
|
|
31
|
+
send_emails([email], mail_settings)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
# Sends multiple emails
|
|
32
|
-
def send_emails(emails)
|
|
35
|
+
def send_emails(emails, mail_settings = nil)
|
|
33
36
|
validate_emails!(emails)
|
|
34
37
|
|
|
35
38
|
request_data = { 'emails' => emails.map(&:to_hash) }
|
|
36
|
-
|
|
39
|
+
request_data['mail_settings'] = mail_settings.to_hash if mail_settings
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
request('POST', '/email/send', request_data)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# List unsubscribe groups for a workspace.
|
|
45
|
+
# Uses the organization API host (https://api.laneful.net).
|
|
46
|
+
def list_unsubscribe_groups(workspace_id, params = nil)
|
|
47
|
+
ListUnsubscribeGroupsResponse.from_hash(
|
|
48
|
+
request('GET', "/workspaces/#{workspace_id}/unsubscribe-groups", nil, params&.to_query)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create an unsubscribe group in a workspace.
|
|
53
|
+
def create_unsubscribe_group(workspace_id, name)
|
|
54
|
+
data = request('POST', "/workspaces/#{workspace_id}/unsubscribe-groups", { 'name' => name })
|
|
55
|
+
UnsubscribeGroup.from_hash(data['unsubscribe_group'] || data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Update an unsubscribe group.
|
|
59
|
+
def update_unsubscribe_group(workspace_id, unsubscribe_group_id, name)
|
|
60
|
+
data = request(
|
|
61
|
+
'PATCH',
|
|
62
|
+
"/workspaces/#{workspace_id}/unsubscribe-groups/#{unsubscribe_group_id}",
|
|
63
|
+
{ 'name' => name }
|
|
64
|
+
)
|
|
65
|
+
UnsubscribeGroup.from_hash(data['unsubscribe_group'] || data)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# List sending domains for a workspace.
|
|
69
|
+
def list_domains(workspace_id, params = nil)
|
|
70
|
+
ListDomainsResponse.from_hash(
|
|
71
|
+
request('GET', "/workspaces/#{workspace_id}/domains", nil, params&.to_query)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get a single sending domain by name.
|
|
76
|
+
def get_domain(workspace_id, domain)
|
|
77
|
+
Domain.from_hash(
|
|
78
|
+
request('GET', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}")
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Create a sending domain in a workspace.
|
|
83
|
+
def create_domain(workspace_id, create_request)
|
|
84
|
+
Domain.from_hash(
|
|
85
|
+
request('POST', "/workspaces/#{workspace_id}/domains", create_request.to_hash)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Update a domain's mutable settings (currently the email track).
|
|
90
|
+
def update_domain(workspace_id, domain, update_request)
|
|
91
|
+
Domain.from_hash(
|
|
92
|
+
request(
|
|
93
|
+
'PATCH',
|
|
94
|
+
"/workspaces/#{workspace_id}/domains/#{encode_path(domain)}",
|
|
95
|
+
update_request.to_hash
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Trigger DNS verification for a domain.
|
|
101
|
+
def verify_domain(workspace_id, domain)
|
|
102
|
+
Domain.from_hash(
|
|
103
|
+
request('POST', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}/verify")
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Delete a sending domain from a workspace.
|
|
108
|
+
def delete_domain(workspace_id, domain)
|
|
109
|
+
SuccessResponse.from_hash(
|
|
110
|
+
request('DELETE', "/workspaces/#{workspace_id}/domains/#{encode_path(domain)}")
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# List domains whose spam complaint ratio reached a critical level.
|
|
115
|
+
def list_domain_spam_ratio_radar(params = nil)
|
|
116
|
+
ListDomainSpamRatioRadarResponse.from_hash(
|
|
117
|
+
request('GET', '/analytics/radar/domain-spam-ratio', nil, params&.to_query)
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# List daily Google Postmaster Tools spam-rate reports.
|
|
122
|
+
def list_google_postmaster_spam_reports(params = nil)
|
|
123
|
+
ListGooglePostmasterSpamReportsResponse.from_hash(
|
|
124
|
+
request('GET', '/analytics/google-postmaster/spam-reports', nil, params&.to_query)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# List daily Microsoft SNDS reports for the organization's sending IPs.
|
|
129
|
+
def list_snds_reports(params = nil)
|
|
130
|
+
ListSndsReportsResponse.from_hash(
|
|
131
|
+
request('GET', '/analytics/microsoft-snds/reports', nil, params&.to_query)
|
|
132
|
+
)
|
|
39
133
|
end
|
|
40
134
|
|
|
41
135
|
private
|
|
@@ -45,7 +139,6 @@ module Laneful
|
|
|
45
139
|
|
|
46
140
|
raise ValidationException, 'Auth token cannot be empty' if auth_token.nil? || auth_token.empty?
|
|
47
141
|
|
|
48
|
-
# Basic URL validation
|
|
49
142
|
return if base_url.match?(%r{^https?://})
|
|
50
143
|
|
|
51
144
|
raise ValidationException, 'Base URL must be a valid HTTP/HTTPS URL'
|
|
@@ -59,18 +152,46 @@ module Laneful
|
|
|
59
152
|
end
|
|
60
153
|
end
|
|
61
154
|
|
|
62
|
-
def default_headers
|
|
63
|
-
{
|
|
155
|
+
def default_headers(with_json: true)
|
|
156
|
+
headers = {
|
|
64
157
|
'Authorization' => "Bearer #{auth_token}",
|
|
65
|
-
'Content-Type' => 'application/json',
|
|
66
158
|
'Accept' => 'application/json',
|
|
67
159
|
'User-Agent' => USER_AGENT
|
|
68
160
|
}
|
|
161
|
+
headers['Content-Type'] = 'application/json' if with_json
|
|
162
|
+
headers
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def build_url(path)
|
|
166
|
+
"#{base_url.chomp('/')}/#{API_VERSION}/#{path.sub(%r{\A/}, '')}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def encode_path(value)
|
|
170
|
+
CGI.escape(value.to_s).gsub('+', '%20')
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def request(method, path, json = nil, query = nil)
|
|
174
|
+
url = build_url(path)
|
|
175
|
+
|
|
176
|
+
options = {
|
|
177
|
+
headers: default_headers(with_json: !json.nil?),
|
|
178
|
+
timeout: timeout
|
|
179
|
+
}
|
|
180
|
+
options[:query] = query unless query.nil? || query.empty?
|
|
181
|
+
options[:body] = json.to_json unless json.nil?
|
|
182
|
+
|
|
183
|
+
begin
|
|
184
|
+
response = self.class.send(method.downcase, url, options)
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
raise HttpException.new("HTTP request failed: #{e.message}", 0, e)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
handle_response(response)
|
|
69
190
|
end
|
|
70
191
|
|
|
71
192
|
def handle_response(response)
|
|
72
193
|
case response.code
|
|
73
|
-
when 200, 201, 202
|
|
194
|
+
when 200, 201, 202, 204
|
|
74
195
|
parse_success_response(response)
|
|
75
196
|
when 404
|
|
76
197
|
raise HttpException.new(
|
data/lib/laneful/models.rb
CHANGED
|
@@ -59,12 +59,15 @@ module Laneful
|
|
|
59
59
|
|
|
60
60
|
# Configuration for email tracking settings
|
|
61
61
|
class TrackingSettings
|
|
62
|
-
attr_reader :opens, :clicks, :unsubscribes
|
|
62
|
+
attr_reader :opens, :clicks, :unsubscribes, :unsubscribe_group_id, :unsubscribe_group_name
|
|
63
63
|
|
|
64
|
-
def initialize(opens: false, clicks: false, unsubscribes: false
|
|
64
|
+
def initialize(opens: false, clicks: false, unsubscribes: false,
|
|
65
|
+
unsubscribe_group_id: nil, unsubscribe_group_name: nil)
|
|
65
66
|
@opens = opens
|
|
66
67
|
@clicks = clicks
|
|
67
68
|
@unsubscribes = unsubscribes
|
|
69
|
+
@unsubscribe_group_id = unsubscribe_group_id
|
|
70
|
+
@unsubscribe_group_name = unsubscribe_group_name
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
# Creates tracking settings from a hash representation
|
|
@@ -72,22 +75,31 @@ module Laneful
|
|
|
72
75
|
new(
|
|
73
76
|
opens: data['opens'] || false,
|
|
74
77
|
clicks: data['clicks'] || false,
|
|
75
|
-
unsubscribes: data['unsubscribes'] || false
|
|
78
|
+
unsubscribes: data['unsubscribes'] || false,
|
|
79
|
+
unsubscribe_group_id: data['unsubscribe_group_id'],
|
|
80
|
+
unsubscribe_group_name: data['unsubscribe_group_name']
|
|
76
81
|
)
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def to_hash
|
|
80
|
-
{
|
|
85
|
+
hash = {
|
|
81
86
|
'opens' => opens,
|
|
82
87
|
'clicks' => clicks,
|
|
83
88
|
'unsubscribes' => unsubscribes
|
|
84
89
|
}
|
|
90
|
+
hash['unsubscribe_group_id'] = unsubscribe_group_id unless unsubscribe_group_id.nil?
|
|
91
|
+
hash['unsubscribe_group_name'] = unsubscribe_group_name unless unsubscribe_group_name.nil?
|
|
92
|
+
hash
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
def ==(other)
|
|
88
96
|
return false unless other.is_a?(TrackingSettings)
|
|
89
97
|
|
|
90
|
-
opens == other.opens &&
|
|
98
|
+
opens == other.opens &&
|
|
99
|
+
clicks == other.clicks &&
|
|
100
|
+
unsubscribes == other.unsubscribes &&
|
|
101
|
+
unsubscribe_group_id == other.unsubscribe_group_id &&
|
|
102
|
+
unsubscribe_group_name == other.unsubscribe_group_name
|
|
91
103
|
end
|
|
92
104
|
|
|
93
105
|
def eql?(other)
|
|
@@ -95,7 +107,7 @@ module Laneful
|
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def hash
|
|
98
|
-
[opens, clicks, unsubscribes].hash
|
|
110
|
+
[opens, clicks, unsubscribes, unsubscribe_group_id, unsubscribe_group_name].hash
|
|
99
111
|
end
|
|
100
112
|
|
|
101
113
|
def to_s
|
|
@@ -103,14 +115,53 @@ module Laneful
|
|
|
103
115
|
end
|
|
104
116
|
end
|
|
105
117
|
|
|
118
|
+
# Request-level mail settings (sandbox mode, return message IDs)
|
|
119
|
+
class MailSettings
|
|
120
|
+
attr_reader :sandbox_mode, :return_message_ids
|
|
121
|
+
|
|
122
|
+
def initialize(sandbox_mode: nil, return_message_ids: nil)
|
|
123
|
+
@sandbox_mode = sandbox_mode
|
|
124
|
+
@return_message_ids = return_message_ids
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.from_hash(data)
|
|
128
|
+
new(
|
|
129
|
+
sandbox_mode: data['sandbox_mode'],
|
|
130
|
+
return_message_ids: data['return_message_ids']
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def to_hash
|
|
135
|
+
hash = {}
|
|
136
|
+
hash['sandbox_mode'] = sandbox_mode unless sandbox_mode.nil?
|
|
137
|
+
hash['return_message_ids'] = return_message_ids unless return_message_ids.nil?
|
|
138
|
+
hash
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def ==(other)
|
|
142
|
+
return false unless other.is_a?(MailSettings)
|
|
143
|
+
|
|
144
|
+
sandbox_mode == other.sandbox_mode && return_message_ids == other.return_message_ids
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def eql?(other)
|
|
148
|
+
self == other
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def hash
|
|
152
|
+
[sandbox_mode, return_message_ids].hash
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
106
156
|
# Represents a file attachment for an email
|
|
107
157
|
class Attachment
|
|
108
|
-
attr_reader :filename, :content_type, :content
|
|
158
|
+
attr_reader :filename, :content_type, :content, :inline_id
|
|
109
159
|
|
|
110
|
-
def initialize(filename, content_type, content)
|
|
160
|
+
def initialize(filename, content_type, content, inline_id = nil)
|
|
111
161
|
@filename = filename
|
|
112
162
|
@content_type = content_type
|
|
113
163
|
@content = content
|
|
164
|
+
@inline_id = inline_id
|
|
114
165
|
validate!
|
|
115
166
|
end
|
|
116
167
|
|
|
@@ -124,21 +175,27 @@ module Laneful
|
|
|
124
175
|
|
|
125
176
|
# Creates an attachment from a hash representation
|
|
126
177
|
def self.from_hash(data)
|
|
127
|
-
|
|
178
|
+
filename = data['file_name'] || data['filename']
|
|
179
|
+
new(filename, data['content_type'], data['content'], data['inline_id'])
|
|
128
180
|
end
|
|
129
181
|
|
|
130
182
|
def to_hash
|
|
131
|
-
{
|
|
132
|
-
'
|
|
183
|
+
hash = {
|
|
184
|
+
'file_name' => filename,
|
|
133
185
|
'content_type' => content_type,
|
|
134
186
|
'content' => content
|
|
135
187
|
}
|
|
188
|
+
hash['inline_id'] = inline_id if inline_id && !inline_id.empty?
|
|
189
|
+
hash
|
|
136
190
|
end
|
|
137
191
|
|
|
138
192
|
def ==(other)
|
|
139
193
|
return false unless other.is_a?(Attachment)
|
|
140
194
|
|
|
141
|
-
filename == other.filename &&
|
|
195
|
+
filename == other.filename &&
|
|
196
|
+
content_type == other.content_type &&
|
|
197
|
+
content == other.content &&
|
|
198
|
+
inline_id == other.inline_id
|
|
142
199
|
end
|
|
143
200
|
|
|
144
201
|
def eql?(other)
|
|
@@ -146,7 +203,7 @@ module Laneful
|
|
|
146
203
|
end
|
|
147
204
|
|
|
148
205
|
def hash
|
|
149
|
-
[filename, content_type, content].hash
|
|
206
|
+
[filename, content_type, content, inline_id].hash
|
|
150
207
|
end
|
|
151
208
|
|
|
152
209
|
def to_s
|
|
@@ -192,12 +249,13 @@ module Laneful
|
|
|
192
249
|
|
|
193
250
|
# Represents a single email to be sent
|
|
194
251
|
class Email
|
|
195
|
-
attr_reader :from, :to, :cc, :bcc, :subject, :text_content, :html_content,
|
|
252
|
+
attr_reader :from, :from_header, :to, :cc, :bcc, :subject, :text_content, :html_content,
|
|
196
253
|
:template_id, :template_data, :attachments, :headers, :reply_to,
|
|
197
254
|
:send_time, :webhook_data, :tag, :tracking
|
|
198
255
|
|
|
199
256
|
def initialize(builder)
|
|
200
257
|
@from = builder.instance_variable_get(:@from)
|
|
258
|
+
@from_header = builder.instance_variable_get(:@from_header)
|
|
201
259
|
@to = builder.instance_variable_get(:@to).dup.freeze
|
|
202
260
|
@cc = builder.instance_variable_get(:@cc).dup.freeze
|
|
203
261
|
@bcc = builder.instance_variable_get(:@bcc).dup.freeze
|
|
@@ -221,6 +279,7 @@ module Laneful
|
|
|
221
279
|
builder = Builder.new
|
|
222
280
|
|
|
223
281
|
builder.from(Address.from_hash(data['from'])) if data && data['from']
|
|
282
|
+
builder.from_header(Address.from_hash(data['from_header'])) if data && data['from_header']
|
|
224
283
|
|
|
225
284
|
data['to'].each { |to_data| builder.to(Address.from_hash(to_data)) } if data && data['to']
|
|
226
285
|
|
|
@@ -252,6 +311,7 @@ module Laneful
|
|
|
252
311
|
|
|
253
312
|
# Required fields
|
|
254
313
|
hash['from'] = from.to_hash
|
|
314
|
+
hash['from_header'] = from_header.to_hash if from_header
|
|
255
315
|
hash['to'] = to.map(&:to_hash)
|
|
256
316
|
hash['subject'] = subject if subject
|
|
257
317
|
|
|
@@ -277,6 +337,7 @@ module Laneful
|
|
|
277
337
|
return false unless other.is_a?(Email)
|
|
278
338
|
|
|
279
339
|
from == other.from &&
|
|
340
|
+
from_header == other.from_header &&
|
|
280
341
|
to == other.to &&
|
|
281
342
|
cc == other.cc &&
|
|
282
343
|
bcc == other.bcc &&
|
|
@@ -299,7 +360,7 @@ module Laneful
|
|
|
299
360
|
end
|
|
300
361
|
|
|
301
362
|
def hash
|
|
302
|
-
[from, to, cc, bcc, subject, text_content, html_content, template_id,
|
|
363
|
+
[from, from_header, to, cc, bcc, subject, text_content, html_content, template_id,
|
|
303
364
|
template_data, attachments, headers, reply_to, send_time, webhook_data, tag, tracking].hash
|
|
304
365
|
end
|
|
305
366
|
|
|
@@ -339,6 +400,15 @@ module Laneful
|
|
|
339
400
|
raise ValidationException, 'Email must have either content (text/HTML) or a template ID'
|
|
340
401
|
end
|
|
341
402
|
|
|
403
|
+
if webhook_data
|
|
404
|
+
raise ValidationException, 'Webhook data cannot have more than 20 keys' if webhook_data.size > 20
|
|
405
|
+
|
|
406
|
+
webhook_data.each do |key, value|
|
|
407
|
+
raise ValidationException, 'Webhook data keys cannot exceed 50 characters' if key.to_s.length > 50
|
|
408
|
+
raise ValidationException, 'Webhook data values cannot exceed 100 characters' if value.to_s.length > 100
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
342
412
|
# Validate send time
|
|
343
413
|
return unless send_time && send_time <= Time.now.to_i
|
|
344
414
|
|
|
@@ -359,6 +429,11 @@ module Laneful
|
|
|
359
429
|
self
|
|
360
430
|
end
|
|
361
431
|
|
|
432
|
+
def from_header(address)
|
|
433
|
+
@from_header = address
|
|
434
|
+
self
|
|
435
|
+
end
|
|
436
|
+
|
|
362
437
|
def to(address)
|
|
363
438
|
@to << address
|
|
364
439
|
self
|