[cover image: Photo by Maksim Goncharenok from Pexels]
Goal
As explained in part one, we want to import all Postmark templates into our local system to be able to change them in bulk and export them to Postmark later.
In this section, we will look at importing e-mails from Postmark.
Data Structure
E-mails in Postmark are not only composed of HTML. They also have:
- A text body
- A layout id
- A name
- A subject
- An alias
- An associated server-id
- An active attribute
- A layout template
To both allow changing easily the HTML of the e-mails and the data associated with our e-mail, we decided to structure the data in two ways:
- an HTML file per template named with the following pattern: #{templatename}#{template_id}.
- a global metadata.yml file with the rest of the templates information. Each e-mail has one entry under its id and then an entry per attribute + the DateTime of the import for that template.
1234:
:name: template_name
:subject: Some Subject
:associated_server_id: 1564321
:active: true
:text_body: The text body
:alias: some-alias
:layout_template: yago_layout_nl
:imported_at: "2022-02-09T15:53"
Code
Code Structure
We decided to implement an ExternalTemplate
model that represents the data on Postmark. We felt that it would have different responsibilities than the InternalTemplate
model, which would focus on representing the mail on our DB.
To bulk import the e-mails, we also decided to implement a TemplateGetter
service. The TemplateGetter
service will make use of the ExternalTemplate
.
ExternalTemplate
Its responsibilities are to get the data from Postmark and instantiate itself as an object (initialize
), and save
itself on the DB. For that, it will user write_html_body
and write_metadata
.
class ExternalTemplate
def initialize(id, server_key)
postmark = Postmark::ApiClient.new(server_key)
postmark.get_template(id).each do |key, value|
instance_variable_set("@#{key}", value)
self.class.send(:attr_reader, key)
end
end
def save
print '.'
write_html_body
write_metadata
end
private
def write_html_body
File.open("imported_templates/#{name}_#{template_id}.html", 'w+') do |file|
file << html_body
end
end
def write_metadata
require 'yaml'
data = YAML.load_file('imported_templates/metadata.yml') || {}
data[template_id] = yaml_attributes.merge(imported_at: Time.now.strftime('%Y-%m-%dT%H:%M'))
File.open('imported_templates/metadata.yml', 'w+') do |file|
YAML.dump(data, file)
end
end
def yaml_attributes
{
name: name,
subject: subject,
associated_server_id: associated_server_id,
active: active,
text_body: text_body,
alias: self.alias,
layout_template: layout_template
}
end
end
TemplateGetter
Its responsibilities are:
- Iterate through the postmark servers (limited to 100 templates, so we currently have two servers)
- Get the ids of the server's templates
- Initialize an
InternalTemplate
for each id - Tell the
InternalTemplate
to save
require 'rubygems'
require 'bundler/setup'
require 'yaml'
require 'postmark'
require_relative '../models/external_template'
class TemplateGetter
def self.import_templates
postmark_api_servers.each do |server_key|
templates_ids(server_key).each { |id| download_template(id, server_key) }
end
end
def self.postmark_api_servers
secrets = YAML.load_file('secrets.yml')
[secrets['postmark_api_server_1'], secrets['postmark_api_server_2']]
end
def self.templates_ids(server_key)
postmark = Postmark::ApiClient.new(server_key)
postmark.get_templates(count: 100)[1].map { |template| template[:template_id] }
end
def self.download_template(id, server_key)
template = ExternalTemplate.new(id, server_key)
template.save
end
end
Conclusion
Building a tool to import templates from Postmark is relatively easy (thanks to their gem. If you find yourself having to make changes in bulk or even just wanting to have a copy of all your e-mail data on Postmark, this system is relatively easy to implement.
In our case, we added a CLI tool to be able to interact easily with the code:
require_relative 'services/template_getter'
puts 'welcome to the Postmark cli tool'
puts 'would you like to: ?'
puts '1. import all e-mails'
input = gets
case input.strip
when '1'
TemplateGetter.new.get_all_templates
else
puts 'invalid input'
end
In Part 3 we will discuss the strategies we implemented to test our changes.