├── VERSION ├── .ruby-version ├── init.rb ├── .ruby-gemset ├── .rspec ├── lib ├── postmark │ ├── version.rb │ ├── response_parsers │ │ ├── json.rb │ │ ├── yajl.rb │ │ └── active_support.rb │ ├── json.rb │ ├── inflector.rb │ ├── inbound.rb │ ├── handlers │ │ └── mail.rb │ ├── bounce.rb │ ├── helpers │ │ ├── hash_helper.rb │ │ └── message_helper.rb │ ├── mail_message_converter.rb │ ├── client.rb │ ├── http_client.rb │ ├── error.rb │ ├── account_api_client.rb │ ├── message_extensions │ │ └── mail.rb │ └── api_client.rb └── postmark.rb ├── postmark.png ├── .document ├── Rakefile ├── spec ├── data │ └── empty.gif ├── support │ ├── helpers.rb │ ├── custom_matchers.rb │ └── shared_examples.rb ├── unit │ ├── postmark │ │ ├── json_spec.rb │ │ ├── inflector_spec.rb │ │ ├── client_spec.rb │ │ ├── handlers │ │ │ └── mail_spec.rb │ │ ├── helpers │ │ │ ├── hash_helper_spec.rb │ │ │ └── message_helper_spec.rb │ │ ├── bounce_spec.rb │ │ ├── inbound_spec.rb │ │ ├── error_spec.rb │ │ ├── http_client_spec.rb │ │ ├── message_extensions │ │ │ └── mail_spec.rb │ │ ├── mail_message_converter_spec.rb │ │ └── account_api_client_spec.rb │ └── postmark_spec.rb ├── spec_helper.rb └── integration │ ├── api_client_resources_spec.rb │ ├── mail_delivery_method_spec.rb │ ├── api_client_hashes_spec.rb │ ├── account_api_client_spec.rb │ └── api_client_messages_spec.rb ├── .gitignore ├── .rake_tasks ├── Gemfile ├── gemfiles └── Gemfile.legacy ├── CONTRIBUTING.md ├── LICENSE ├── RELEASE.md ├── postmark.gemspec ├── .circleci └── config.yml ├── README.md └── CHANGELOG.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 1.22.1 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.6 2 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'postmark' -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | postmark-gem 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | -------------------------------------------------------------------------------- /lib/postmark/version.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | VERSION = '1.22.1' 3 | end 4 | -------------------------------------------------------------------------------- /postmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/postmark-gem/main/postmark.png -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | RSpec::Core::RakeTask.new -------------------------------------------------------------------------------- /spec/data/empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/postmark-gem/main/spec/data/empty.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .env 4 | Gemfile.lock 5 | pkg/* 6 | .rvmrc 7 | .idea 8 | bin/* 9 | *.swp 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /lib/postmark/response_parsers/json.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | module Postmark 3 | module ResponseParsers 4 | module Json 5 | def self.decode(data) 6 | JSON.parse(data) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/postmark/response_parsers/yajl.rb: -------------------------------------------------------------------------------- 1 | require 'yajl' 2 | Yajl::Encoder.enable_json_gem_compatability 3 | module Postmark 4 | module ResponseParsers 5 | module Yajl 6 | def self.decode(data) 7 | ::Yajl::Parser.parse(data) 8 | end 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/postmark/response_parsers/active_support.rb: -------------------------------------------------------------------------------- 1 | # assume activesupport is already loaded 2 | module Postmark 3 | module ResponseParsers 4 | module ActiveSupport 5 | def self.decode(data) 6 | ::ActiveSupport::JSON.decode(data) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/helpers.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | module RSpecHelpers 3 | def empty_gif_path 4 | File.join(File.dirname(__FILE__), '..', 'data', 'empty.gif') 5 | end 6 | 7 | def encoded_empty_gif_data 8 | Postmark::MessageHelper.encode_in_base64(File.read(empty_gif_path)) 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /.rake_tasks: -------------------------------------------------------------------------------- 1 | build 2 | check_dependencies 3 | check_dependencies:development 4 | check_dependencies:runtime 5 | clobber_rcov 6 | clobber_rdoc 7 | features 8 | gemcutter:release 9 | gemspec 10 | gemspec:generate 11 | gemspec:validate 12 | install 13 | rcov 14 | rdoc 15 | release 16 | rerdoc 17 | spec 18 | version 19 | version:bump:major 20 | version:bump:minor 21 | version:bump:patch 22 | version:write 23 | -------------------------------------------------------------------------------- /lib/postmark/json.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | module Json 3 | 4 | class << self 5 | def encode(data) 6 | json_parser 7 | data.to_json 8 | end 9 | 10 | def decode(data) 11 | json_parser.decode(data) 12 | end 13 | 14 | private 15 | 16 | def json_parser 17 | ResponseParsers.const_get(Postmark.response_parser_class) 18 | end 19 | 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in postmark.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'rspec', '~> 3.7' 8 | gem 'rspec-its', '~> 1.2' 9 | gem 'fakeweb', :git => 'https://github.com/chrisk/fakeweb.git' 10 | gem 'fakeweb-matcher' 11 | gem 'mime-types' 12 | gem 'activesupport' 13 | gem 'i18n', '~> 0.6.0' 14 | gem 'yajl-ruby', '~> 1.0', :platforms => [:mingw, :mswin, :ruby] 15 | end 16 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.legacy: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => '../' 4 | 5 | gem 'rake', '< 11.0.0' 6 | gem 'json', '< 2.0.0' 7 | 8 | group :test do 9 | gem 'rspec', '~> 3.7' 10 | gem 'rspec-its', '~> 1.2' 11 | gem 'fakeweb', :git => 'https://github.com/chrisk/fakeweb.git' 12 | gem 'fakeweb-matcher' 13 | gem 'mime-types', '~> 1.25.1' 14 | gem 'activesupport', '~> 3.2.0' 15 | gem 'i18n', '~> 0.6.0' 16 | gem 'yajl-ruby', '~> 1.0', '< 1.4.0', :platforms => [:mingw, :mswin, :ruby] 17 | end 18 | -------------------------------------------------------------------------------- /lib/postmark/inflector.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | module Inflector 3 | 4 | extend self 5 | 6 | def to_postmark(name) 7 | name.to_s.split('_').map { |part| capitalize_first_letter(part) }.join('') 8 | end 9 | 10 | def to_ruby(name) 11 | name.to_s.scan(camel_case_regexp).join('_').downcase.to_sym 12 | end 13 | 14 | def camel_case_regexp 15 | /(?:[A-Z](?:(?:[A-Z]+(?![a-z\d]))|[a-z\d]*))|[a-z\d\_]+/ 16 | end 17 | 18 | protected 19 | 20 | def capitalize_first_letter(str) 21 | if str.length > 0 22 | str.slice(0..0).capitalize + str.slice(1..-1) 23 | else 24 | str 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/postmark/inbound.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | module Inbound 3 | extend self 4 | 5 | def to_ruby_hash(inbound) 6 | inbound = Json.decode(inbound) if inbound.is_a?(String) 7 | ret = HashHelper.to_ruby(inbound) 8 | ret[:from_full] ||= {} 9 | ret[:to_full] ||= [] 10 | ret[:cc_full] ||= [] 11 | ret[:headers] ||= [] 12 | ret[:attachments] ||= [] 13 | ret[:from_full] = HashHelper.to_ruby(ret[:from_full]) 14 | ret[:to_full] = ret[:to_full].map { |to| HashHelper.to_ruby(to) } 15 | ret[:cc_full] = ret[:cc_full].map { |cc| HashHelper.to_ruby(cc) } 16 | ret[:headers] = ret[:headers].map { |h| HashHelper.to_ruby(h) } 17 | ret[:attachments] = ret[:attachments].map { |a| HashHelper.to_ruby(a) } 18 | ret 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/postmark/handlers/mail.rb: -------------------------------------------------------------------------------- 1 | module Mail 2 | class Postmark 3 | 4 | attr_accessor :settings 5 | 6 | def initialize(values) 7 | self.settings = { :api_token => ENV['POSTMARK_API_TOKEN'] }.merge(values) 8 | end 9 | 10 | def deliver!(mail) 11 | response = if mail.templated? 12 | api_client.deliver_message_with_template(mail) 13 | else 14 | api_client.deliver_message(mail) 15 | end 16 | 17 | if settings[:return_response] 18 | response 19 | else 20 | self 21 | end 22 | end 23 | 24 | def api_client 25 | settings = self.settings.dup 26 | api_token = settings.delete(:api_token) || settings.delete(:api_key) 27 | ::Postmark::ApiClient.new(api_token, settings) 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Before you report an issue or submit a pull request 2 | 3 | *If you are blocked or need help with Postmark, please [contact 4 | Postmark Support](https://postmarkapp.com/contact)*. For other, non-urgent 5 | cases you’re welcome to report a bug and/or contribute to this project. We will 6 | make our best effort to review your contributions and triage any bug reports in 7 | a timely fashion. 8 | 9 | If you’d like to submit a pull request: 10 | 11 | * Fork the project. 12 | * Make your feature addition or bug fix. 13 | * Add tests for it. This is important to prevent future regressions. 14 | * Do not mess with rakefile, version, or history. 15 | * Update the CHANGELOG, list your changes under Unreleased. 16 | * Update the README if necessary. 17 | * Write short, descriptive commit messages, following the format used in therepo. 18 | * Send a pull request. Bonus points for topic branches. 19 | -------------------------------------------------------------------------------- /spec/unit/postmark/json_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::Json do 4 | let(:data) { {"bar" => "foo", "foo" => "bar"} } 5 | 6 | shared_examples "json parser" do 7 | it 'encodes and decodes data correctly' do 8 | hash = Postmark::Json.decode(Postmark::Json.encode(data)) 9 | expect(hash).to have_key("bar") 10 | expect(hash).to have_key("foo") 11 | end 12 | end 13 | 14 | context "given response parser is JSON" do 15 | before do 16 | Postmark.response_parser_class = :Json 17 | end 18 | 19 | it_behaves_like "json parser" 20 | end 21 | 22 | context "given response parser is ActiveSupport::JSON" do 23 | before do 24 | Postmark.response_parser_class = :ActiveSupport 25 | end 26 | 27 | it_behaves_like "json parser" 28 | end 29 | 30 | context "given response parser is Yajl", :skip_for_platform => 'java' do 31 | before do 32 | Postmark.response_parser_class = :Yajl 33 | end 34 | 35 | it_behaves_like "json parser" 36 | end 37 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 ActiveCampaign LLC. 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. 21 | -------------------------------------------------------------------------------- /spec/support/custom_matchers.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :a_postmark_json do |string| 2 | def postmark_key?(key) 3 | key == ::Postmark::Inflector.to_postmark(key) 4 | end 5 | 6 | def postmark_object?(obj) 7 | case obj 8 | when Hash 9 | return false unless obj.keys.all? { |k| postmark_key?(k) } 10 | return false unless obj.values.all? { |v| postmark_object?(v) } 11 | when Array 12 | return false unless obj.all? { |v| postmark_object?(v) } 13 | end 14 | 15 | true 16 | end 17 | 18 | def postmark_json?(str) 19 | return false unless str.is_a?(String) 20 | 21 | json = Postmark::Json.decode(str) 22 | postmark_object?(json) 23 | rescue 24 | false 25 | end 26 | 27 | match do |actual| 28 | postmark_json?(actual) 29 | end 30 | end 31 | 32 | RSpec::Matchers.define :json_representation_of do |x| 33 | match { |actual| Postmark::Json.decode(actual) == x } 34 | end 35 | 36 | RSpec::Matchers.define :match_json do |x| 37 | match { |actual| Postmark::Json.encode(x) == actual } 38 | end 39 | 40 | RSpec::Matchers.define :be_serialized_to do |json| 41 | match do |mail_message| 42 | Postmark.convert_message_to_options_hash(mail_message) == JSON.parse(json) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | New versions of the gem are cut by the Postmark team, this is a quick guide to ensuring a smooth release. 2 | 3 | 1. Determine the next version of the gem by following the [SemVer](https://semver.org/) guidelines. 4 | 1. Verify all builds are passing on CircleCI for your branch. 5 | 1. Merge in your branch to main. 6 | 1. Update VERSION and lib/postmark/version.rb with the new version. 7 | 1. Update CHANGELOG.rdoc with a brief description of the changes. 8 | 1. Commit to git with a comment of "Bump version to x.y.z". 9 | 1. run `rake release` - This will push to github(with the version tag) and rubygems with the version in lib/postmark/version.rb. 10 | *Note that if you're on Bundler 1.17 there's a bug that hides the prompt for your OTP. If it hangs after adding the tag then it's asking for your OTP, enter your OTP and press Enter. Bundler 2.x and beyond resolved this issue. * 11 | 1. Verify the new version is on [github](https://github.com/ActiveCampaign/postmark-gem) and [rubygems](https://rubygems.org/gems/postmark). 12 | 1. Create a new release for the version on [Github releases](https://github.com/ActiveCampaign/postmark-gem/releases). 13 | 1. Add or update any related content to the [wiki](https://github.com/ActiveCampaign/postmark-gem/wiki). 14 | -------------------------------------------------------------------------------- /spec/unit/postmark/inflector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::Inflector do 4 | describe ".to_postmark" do 5 | it 'converts rubyish underscored format to camel cased symbols accepted by the Postmark API' do 6 | expect(subject.to_postmark(:foo_bar)).to eq 'FooBar' 7 | expect(subject.to_postmark(:_bar)).to eq 'Bar' 8 | expect(subject.to_postmark(:really_long_long_long_long_symbol)).to eq 'ReallyLongLongLongLongSymbol' 9 | expect(subject.to_postmark(:foo_bar_1)).to eq 'FooBar1' 10 | end 11 | 12 | it 'accepts strings as well' do 13 | expect(subject.to_postmark('foo_bar')).to eq 'FooBar' 14 | end 15 | 16 | it 'acts idempotentely' do 17 | expect(subject.to_postmark('FooBar')).to eq 'FooBar' 18 | end 19 | end 20 | 21 | describe ".to_ruby" do 22 | it 'converts camel cased symbols returned by the Postmark API to underscored Ruby symbols' do 23 | expect(subject.to_ruby('FooBar')).to eq :foo_bar 24 | expect(subject.to_ruby('LongTimeAgoInAFarFarGalaxy')).to eq :long_time_ago_in_a_far_far_galaxy 25 | expect(subject.to_ruby('MessageID')).to eq :message_id 26 | end 27 | 28 | it 'acts idempotentely' do 29 | expect(subject.to_ruby(:foo_bar)).to eq :foo_bar 30 | expect(subject.to_ruby(:foo_bar_1)).to eq :foo_bar_1 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /postmark.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "postmark/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "postmark" 7 | s.version = Postmark::VERSION 8 | s.homepage = "http://postmarkapp.com" 9 | s.platform = Gem::Platform::RUBY 10 | s.license = 'MIT' 11 | 12 | s.authors = ["Tomek Maszkowski", "Igor Balos", "Artem Chistyakov", "Nick Hammond", "Petyo Ivanov", "Ilya Sabanin"] 13 | s.extra_rdoc_files = ["LICENSE", "README.md"] 14 | s.rdoc_options = ["--charset=UTF-8"] 15 | 16 | s.summary = "Official Postmark API wrapper." 17 | s.description = "Use this gem to send emails through Postmark HTTP API and retrieve info about bounces." 18 | 19 | s.files = `git ls-files`.split("\n") 20 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 22 | s.require_paths = ["lib"] 23 | 24 | s.post_install_message = %q{ 25 | ================== 26 | Thanks for installing the postmark gem. If you don't have an account, please 27 | sign up at http://postmarkapp.com/. 28 | 29 | Review the README.md for implementation details and examples. 30 | ================== 31 | } 32 | 33 | s.required_rubygems_version = ">= 1.3.7" 34 | 35 | s.add_dependency "json" 36 | 37 | s.add_development_dependency "mail" 38 | s.add_development_dependency "rake" 39 | end 40 | -------------------------------------------------------------------------------- /lib/postmark/bounce.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module Postmark 4 | class Bounce 5 | 6 | attr_reader :email, :bounced_at, :type, :description, :details, :name, :id, :server_id, :tag, :message_id, :subject 7 | 8 | def initialize(values = {}) 9 | values = Postmark::HashHelper.to_ruby(values) 10 | @id = values[:id] 11 | @email = values[:email] 12 | @bounced_at = Time.parse(values[:bounced_at]) 13 | @type = values[:type] 14 | @name = values[:name] 15 | @description = values[:description] 16 | @details = values[:details] 17 | @tag = values[:tag] 18 | @dump_available = values[:dump_available] 19 | @inactive = values[:inactive] 20 | @can_activate = values[:can_activate] 21 | @message_id = values[:message_id] 22 | @subject = values[:subject] 23 | end 24 | 25 | def inactive? 26 | !!@inactive 27 | end 28 | 29 | def can_activate? 30 | !!@can_activate 31 | end 32 | 33 | def dump 34 | Postmark.api_client.dump_bounce(id)[:body] 35 | end 36 | 37 | def activate 38 | Bounce.new(Postmark.api_client.activate_bounce(id)) 39 | end 40 | 41 | def dump_available? 42 | !!@dump_available 43 | end 44 | 45 | class << self 46 | def find(id) 47 | Bounce.new(Postmark.api_client.get_bounce(id)) 48 | end 49 | 50 | def all(options = {}) 51 | options[:count] ||= 30 52 | options[:offset] ||= 0 53 | Postmark.api_client.get_bounces(options).map do |bounce_json| 54 | Bounce.new(bounce_json) 55 | end 56 | end 57 | end 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/unit/postmark/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::Client do 4 | subject { Postmark::Client.new('abcd-efgh') } 5 | 6 | describe 'instance' do 7 | describe '#find_each' do 8 | let(:path) { 'resources' } 9 | let(:name) { 'Resources' } 10 | let(:response) { 11 | { 12 | 'TotalCount' => 10, 13 | name => [{'Foo' => 'bar'}, {'Bar' => 'foo'}] 14 | } 15 | } 16 | 17 | it 'returns an enumerator' do 18 | expect(subject.find_each(path, name)).to be_kind_of(Enumerable) 19 | end 20 | 21 | it 'can be iterated' do 22 | collection = [{:foo => 'bar'}, {:bar => 'foo'}].cycle(5) 23 | allow(subject.http_client). 24 | to receive(:get).with(path, an_instance_of(Hash)). 25 | exactly(5).times.and_return(response) 26 | expect { |b| subject.find_each(path, name, :count => 2).each(&b) }. 27 | to yield_successive_args(*collection) 28 | end 29 | 30 | # Only Ruby >= 2.0.0 supports Enumerator#size 31 | it 'lazily calculates the collection size', 32 | :skip_ruby_version => ['1.8.7', '1.9'] do 33 | allow(subject.http_client). 34 | to receive(:get).exactly(1).times.and_return(response) 35 | collection = subject.find_each(path, name, :count => 2) 36 | expect(collection.size).to eq(10) 37 | end 38 | 39 | it 'iterates over the collection to count it' do 40 | allow(subject.http_client). 41 | to receive(:get).exactly(5).times.and_return(response) 42 | expect(subject.find_each(path, name, :count => 2).count).to eq(10) 43 | end 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /lib/postmark/helpers/hash_helper.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | module HashHelper 3 | 4 | extend self 5 | 6 | def to_postmark(object, options = {}) 7 | deep = options.fetch(:deep, false) 8 | 9 | case object 10 | when Hash 11 | object.reduce({}) do |m, (k, v)| 12 | m.tap do |h| 13 | h[Inflector.to_postmark(k)] = deep ? to_postmark(v, options) : v 14 | end 15 | end 16 | when Array 17 | deep ? object.map { |v| to_postmark(v, options) } : object 18 | else 19 | object 20 | end 21 | end 22 | 23 | def to_ruby(object, options = {}) 24 | compatible = options.fetch(:compatible, false) 25 | deep = options.fetch(:deep, false) 26 | 27 | case object 28 | when Hash 29 | object.reduce({}) do |m, (k, v)| 30 | m.tap do |h| 31 | h[Inflector.to_ruby(k)] = deep ? to_ruby(v, options) : v 32 | end 33 | end.tap do |result| 34 | if compatible 35 | result.merge!(object) 36 | enhance_with_compatibility_warning(result) 37 | end 38 | end 39 | when Array 40 | deep ? object.map { |v| to_ruby(v, options) } : object 41 | else 42 | object 43 | end 44 | end 45 | 46 | private 47 | 48 | def enhance_with_compatibility_warning(hash) 49 | def hash.[](key) 50 | if key.is_a? String 51 | Kernel.warn("Postmark: the CamelCased String keys of response are " \ 52 | "deprecated in favor of underscored symbols. The " \ 53 | "support will be dropped in the future.") 54 | end 55 | super 56 | end 57 | end 58 | 59 | end 60 | end -------------------------------------------------------------------------------- /lib/postmark/helpers/message_helper.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | module MessageHelper 3 | 4 | extend self 5 | 6 | def to_postmark(message = {}) 7 | message = message.dup 8 | 9 | %w(to reply_to cc bcc).each do |field| 10 | message[field.to_sym] = Array[*message[field.to_sym]].join(", ") 11 | end 12 | 13 | if message[:headers] 14 | message[:headers] = headers_to_postmark(message[:headers]) 15 | end 16 | 17 | if message[:attachments] 18 | message[:attachments] = attachments_to_postmark(message[:attachments]) 19 | end 20 | 21 | if message[:track_links] 22 | message[:track_links] = ::Postmark::Inflector.to_postmark(message[:track_links]) 23 | end 24 | 25 | HashHelper.to_postmark(message) 26 | end 27 | 28 | def headers_to_postmark(headers) 29 | wrap_in_array(headers).map do |item| 30 | HashHelper.to_postmark(item) 31 | end 32 | end 33 | 34 | def attachments_to_postmark(attachments) 35 | wrap_in_array(attachments).map do |item| 36 | if item.is_a?(Hash) 37 | HashHelper.to_postmark(item) 38 | elsif item.is_a?(File) 39 | { 40 | "Name" => item.path.split("/")[-1], 41 | "Content" => encode_in_base64(IO.read(item.path)), 42 | "ContentType" => "application/octet-stream" 43 | } 44 | end 45 | end 46 | end 47 | 48 | def encode_in_base64(data) 49 | [data].pack('m') 50 | end 51 | 52 | protected 53 | 54 | # From ActiveSupport (Array#wrap) 55 | def wrap_in_array(object) 56 | if object.nil? 57 | [] 58 | elsif object.respond_to?(:to_ary) 59 | object.to_ary || [object] 60 | else 61 | [object] 62 | end 63 | end 64 | 65 | end 66 | end -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # In order for builds to pass, in CircleCI you must have following environment variables setub: 2 | # POSTMARK_API_KEY,POSTMARK_ACCOUNT_API_KEY,POSTMARK_CI_RECIPIENT,POSTMARK_CI_SENDER 3 | 4 | version: 2.1 5 | 6 | workflows: 7 | ruby-tests: 8 | jobs: 9 | - unit-tests: 10 | matrix: 11 | parameters: 12 | version: ["2", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7"] 13 | - unit-tests-legacy: 14 | matrix: 15 | parameters: 16 | version: ["kneip/ree-1.8.7-2012.02","ruby:1.9.3","circleci/jruby:9"] 17 | 18 | orbs: 19 | ruby: circleci/ruby@0.1.2 20 | 21 | jobs: 22 | unit-tests: 23 | parallelism: 1 24 | parameters: 25 | version: 26 | type: string 27 | docker: 28 | - image: circleci/ruby:<< parameters.version >> 29 | steps: 30 | - checkout 31 | - run: 32 | name: Versions 33 | command: | 34 | echo "ruby: $(ruby --version)" 35 | 36 | - run: 37 | name: Install dependencies 38 | command: bundle install 39 | 40 | - run: 41 | name: Run tests 42 | command: bundle exec rake spec 43 | 44 | unit-tests-legacy: 45 | parallelism: 1 46 | environment: 47 | BUNDLE_GEMFILE: ./gemfiles/Gemfile.legacy 48 | parameters: 49 | version: 50 | type: string 51 | docker: 52 | - image: << parameters.version >> 53 | steps: 54 | - checkout 55 | - run: 56 | name: Versions 57 | command: | 58 | echo "ruby: $(ruby --version)" 59 | 60 | - run: 61 | name: Install dependencies 62 | command: | 63 | gem install bundler --version 1.17.3 64 | bundle install 65 | 66 | - run: 67 | name: Run tests 68 | command: bundle exec rake spec 69 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'rubygems' 4 | require 'bundler' 5 | Bundler.setup(:development) 6 | require 'mail' 7 | require 'postmark' 8 | require 'active_support' 9 | require 'json' 10 | require 'fakeweb' 11 | require 'fakeweb_matcher' 12 | require 'rspec' 13 | require 'rspec/its' 14 | require File.join(File.expand_path(File.dirname(__FILE__)), 'support', 'shared_examples.rb') 15 | require File.join(File.expand_path(File.dirname(__FILE__)), 'support', 'custom_matchers.rb') 16 | require File.join(File.expand_path(File.dirname(__FILE__)), 'support', 'helpers.rb') 17 | 18 | if ENV['JSONGEM'] 19 | # `JSONGEM=Yajl rake spec` 20 | Postmark.response_parser_class = ENV['JSONGEM'].to_sym 21 | puts "Setting ResponseParser class to #{Postmark::ResponseParsers.const_get Postmark.response_parser_class}" 22 | end 23 | 24 | RSpec.configure do |config| 25 | include Postmark::RSpecHelpers 26 | 27 | config.expect_with(:rspec) { |c| c.syntax = :expect } 28 | 29 | config.filter_run_excluding :skip_for_platform => lambda { |platform| 30 | RUBY_PLATFORM.to_s =~ /^#{platform.to_s}/ 31 | } 32 | 33 | config.filter_run_excluding :skip_ruby_version => lambda { |version| 34 | versions = [*version] 35 | versions.any? { |v| RUBY_VERSION.to_s =~ /^#{v.to_s}/ } 36 | } 37 | 38 | config.filter_run_excluding :exclusive_for_ruby_version => lambda { |version| 39 | versions = [*version] 40 | versions.all? { |v| !(RUBY_VERSION.to_s =~ /^#{v.to_s}/) } 41 | } 42 | 43 | config.before(:each) do 44 | %w(api_client response_parser_class secure api_token proxy_host proxy_port 45 | proxy_user proxy_pass host port path_prefix http_open_timeout 46 | http_read_timeout max_retries).each do |var| 47 | Postmark.instance_variable_set(:"@#{var}", nil) 48 | end 49 | Postmark.response_parser_class = nil 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/integration/api_client_resources_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Accessing server resources using the API' do 4 | let(:api_client) {Postmark::ApiClient.new(ENV['POSTMARK_API_KEY'], :http_open_timeout => 15)} 5 | let(:recipient) {ENV['POSTMARK_CI_RECIPIENT']} 6 | let(:message) { 7 | { 8 | :from => ENV['POSTMARK_CI_SENDER'], 9 | :to => recipient, 10 | :subject => "Mail::Message object", 11 | :text_body => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " \ 12 | "sed do eiusmod tempor incididunt ut labore et dolore " \ 13 | "magna aliqua." 14 | } 15 | } 16 | 17 | context 'Messages API' do 18 | def with_retries(max_retries = 20, wait_seconds = 3) 19 | yield 20 | rescue => e 21 | retries = retries ? retries + 1 : 1 22 | if retries < max_retries 23 | sleep wait_seconds 24 | retry 25 | else 26 | raise e 27 | end 28 | end 29 | 30 | it 'is possible to send a message and access its details via the Messages API' do 31 | response = api_client.deliver(message) 32 | message = with_retries {api_client.get_message(response[:message_id])} 33 | expect(message[:recipients]).to include(recipient) 34 | end 35 | 36 | it 'is possible to send a message and dump it via the Messages API' do 37 | response = api_client.deliver(message) 38 | dump = with_retries {api_client.dump_message(response[:message_id])} 39 | expect(dump[:body]).to include('Mail::Message object') 40 | end 41 | 42 | it 'is possible to send a message and find it via the Messages API' do 43 | response = api_client.deliver(message) 44 | expect { 45 | with_retries { 46 | messages = api_client.get_messages(:recipient => recipient, 47 | :fromemail => message[:from], 48 | :subject => message[:subject]) 49 | unless messages.map {|m| m[:message_id]}.include?(response[:message_id]) 50 | raise 'Message not found' 51 | end 52 | } 53 | }.not_to raise_error 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/postmark/mail_message_converter.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | 3 | class MailMessageConverter 4 | 5 | def initialize(message) 6 | @message = message 7 | end 8 | 9 | def run 10 | delete_blank_fields(pick_fields(convert, @message.templated?)) 11 | end 12 | 13 | private 14 | 15 | def convert 16 | headers_part.merge(content_part) 17 | end 18 | 19 | def delete_blank_fields(message_hash) 20 | message_hash.delete_if { |k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) } 21 | end 22 | 23 | def headers_part 24 | { 25 | 'From' => @message['from'].to_s, 26 | 'To' => @message['to'].to_s, 27 | 'ReplyTo' => @message['reply_to'].to_s, 28 | 'Cc' => @message['cc'].to_s, 29 | 'Bcc' => @message['bcc'].to_s, 30 | 'Subject' => @message.subject, 31 | 'Headers' => @message.export_headers, 32 | 'Tag' => @message.tag.to_s, 33 | 'TrackOpens' => (cast_to_bool(@message.track_opens) unless @message.track_opens.empty?), 34 | 'TrackLinks' => (::Postmark::Inflector.to_postmark(@message.track_links) unless @message.track_links.empty?), 35 | 'Metadata' => @message.metadata, 36 | 'TemplateAlias' => @message.template_alias, 37 | 'TemplateModel' => @message.template_model, 38 | 'MessageStream' => @message.message_stream 39 | } 40 | end 41 | 42 | def pick_fields(message_hash, templated = false) 43 | fields = if templated 44 | %w(Subject HtmlBody TextBody) 45 | else 46 | %w(TemplateAlias TemplateModel) 47 | end 48 | fields.each { |key| message_hash.delete(key) } 49 | message_hash 50 | end 51 | 52 | def content_part 53 | { 54 | 'Attachments' => @message.export_attachments, 55 | 'HtmlBody' => @message.body_html, 56 | 'TextBody' => @message.body_text 57 | } 58 | end 59 | 60 | def cast_to_bool(val) 61 | if val.is_a?(TrueClass) || val.is_a?(FalseClass) 62 | val 63 | elsif val.is_a?(String) && val.downcase == "true" 64 | true 65 | else 66 | false 67 | end 68 | end 69 | 70 | end 71 | 72 | end -------------------------------------------------------------------------------- /spec/support/shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples :mail do 2 | it "set text body for plain message" do 3 | expect(Postmark.send(:convert_message_to_options_hash, subject)['TextBody']).not_to be_nil 4 | end 5 | 6 | it "encode from properly when name is used" do 7 | subject.from = "Sheldon Lee Cooper " 8 | expect(subject).to be_serialized_to %q[{"Subject":"Hello!", "From":"Sheldon Lee Cooper ", "To":"lenard@bigbangtheory.com", "TextBody":"Hello Sheldon!"}] 9 | end 10 | 11 | it "encode reply to" do 12 | subject.reply_to = ['a@a.com', 'b@b.com'] 13 | expect(subject).to be_serialized_to %q[{"Subject":"Hello!", "From":"sheldon@bigbangtheory.com", "ReplyTo":"a@a.com, b@b.com", "To":"lenard@bigbangtheory.com", "TextBody":"Hello Sheldon!"}] 14 | end 15 | 16 | it "encode tag" do 17 | subject.tag = "invite" 18 | expect(subject).to be_serialized_to %q[{"Subject":"Hello!", "From":"sheldon@bigbangtheory.com", "Tag":"invite", "To":"lenard@bigbangtheory.com", "TextBody":"Hello Sheldon!"}] 19 | end 20 | 21 | it "encode multiple recepients (TO)" do 22 | subject.to = ['a@a.com', 'b@b.com'] 23 | expect(subject).to be_serialized_to %q[{"Subject":"Hello!", "From":"sheldon@bigbangtheory.com", "To":"a@a.com, b@b.com", "TextBody":"Hello Sheldon!"}] 24 | end 25 | 26 | it "encode multiple recepients (CC)" do 27 | subject.cc = ['a@a.com', 'b@b.com'] 28 | expect(subject).to be_serialized_to %q[{"Cc":"a@a.com, b@b.com", "Subject":"Hello!", "From":"sheldon@bigbangtheory.com", "To":"lenard@bigbangtheory.com", "TextBody":"Hello Sheldon!"}] 29 | end 30 | 31 | it "encode multiple recepients (BCC)" do 32 | subject.bcc = ['a@a.com', 'b@b.com'] 33 | expect(subject).to be_serialized_to %q[{"Bcc":"a@a.com, b@b.com", "Subject":"Hello!", "From":"sheldon@bigbangtheory.com", "To":"lenard@bigbangtheory.com", "TextBody":"Hello Sheldon!"}] 34 | end 35 | 36 | it "accept string as reply_to field" do 37 | subject.reply_to = ['Anton Astashov '] 38 | expect(subject).to be_serialized_to %q[{"From": "sheldon@bigbangtheory.com", "ReplyTo": "b@b.com", "To": "lenard@bigbangtheory.com", "Subject": "Hello!", "TextBody": "Hello Sheldon!"}] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/postmark.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'net/https' 3 | require 'thread' unless defined? Mutex # For Ruby 1.8.7 4 | 5 | require 'postmark/version' 6 | require 'postmark/inflector' 7 | require 'postmark/helpers/hash_helper' 8 | require 'postmark/helpers/message_helper' 9 | require 'postmark/mail_message_converter' 10 | require 'postmark/bounce' 11 | require 'postmark/inbound' 12 | require 'postmark/json' 13 | require 'postmark/error' 14 | require 'postmark/http_client' 15 | require 'postmark/client' 16 | require 'postmark/api_client' 17 | require 'postmark/account_api_client' 18 | require 'postmark/message_extensions/mail' 19 | require 'postmark/handlers/mail' 20 | 21 | module Postmark 22 | module ResponseParsers 23 | autoload :Json, 'postmark/response_parsers/json' 24 | autoload :ActiveSupport, 'postmark/response_parsers/active_support' 25 | autoload :Yajl, 'postmark/response_parsers/yajl' 26 | end 27 | 28 | HEADERS = { 29 | 'User-Agent' => "Postmark Ruby Gem v#{VERSION}", 30 | 'Content-type' => 'application/json', 31 | 'Accept' => 'application/json' 32 | } 33 | 34 | extend self 35 | 36 | @@api_client_mutex = Mutex.new 37 | 38 | attr_accessor :secure, :api_token, :proxy_host, :proxy_port, :proxy_user, 39 | :proxy_pass, :host, :port, :path_prefix, 40 | :http_open_timeout, :http_read_timeout, :max_retries 41 | 42 | alias_method :api_key, :api_token 43 | alias_method :api_key=, :api_token= 44 | 45 | attr_writer :response_parser_class, :api_client 46 | 47 | def response_parser_class 48 | @response_parser_class ||= defined?(ActiveSupport::JSON) ? :ActiveSupport : :Json 49 | end 50 | 51 | def configure 52 | yield self 53 | end 54 | 55 | def api_client 56 | return @api_client if @api_client 57 | 58 | @@api_client_mutex.synchronize do 59 | @api_client ||= Postmark::ApiClient.new( 60 | self.api_token, 61 | :secure => self.secure, 62 | :proxy_host => self.proxy_host, 63 | :proxy_port => self.proxy_port, 64 | :proxy_user => self.proxy_user, 65 | :proxy_pass => self.proxy_pass, 66 | :host => self.host, 67 | :port => self.port, 68 | :path_prefix => self.path_prefix, 69 | :max_retries => self.max_retries 70 | ) 71 | end 72 | end 73 | 74 | def deliver_message(*args) 75 | api_client.deliver_message(*args) 76 | end 77 | alias_method :send_through_postmark, :deliver_message 78 | 79 | def deliver_messages(*args) 80 | api_client.deliver_messages(*args) 81 | end 82 | 83 | def delivery_stats(*args) 84 | api_client.delivery_stats(*args) 85 | end 86 | 87 | end 88 | 89 | Postmark.response_parser_class = nil -------------------------------------------------------------------------------- /lib/postmark/client.rb: -------------------------------------------------------------------------------- 1 | require 'enumerator' 2 | 3 | module Postmark 4 | class Client 5 | attr_reader :http_client, :max_retries 6 | 7 | def initialize(api_token, options = {}) 8 | options = options.dup 9 | @max_retries = options.delete(:max_retries) || 0 10 | @http_client = HttpClient.new(api_token, options) 11 | end 12 | 13 | def api_token=(api_token) 14 | http_client.api_token = api_token 15 | end 16 | alias_method :api_key=, :api_token= 17 | 18 | def find_each(path, name, options = {}) 19 | if block_given? 20 | options = options.dup 21 | i, total_count = [0, 1] 22 | 23 | while i < total_count 24 | options[:offset] = i 25 | total_count, collection = load_batch(path, name, options) 26 | collection.each { |e| yield e } 27 | i += collection.size 28 | end 29 | else 30 | enum_for(:find_each, path, name, options) do 31 | get_resource_count(path, options) 32 | end 33 | end 34 | end 35 | 36 | protected 37 | 38 | def with_retries 39 | yield 40 | rescue HttpServerError, HttpClientError, TimeoutError, Errno::EINVAL, 41 | Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, 42 | Net::ProtocolError, SocketError => e 43 | retries = retries ? retries + 1 : 1 44 | retriable = !e.respond_to?(:retry?) || e.retry? 45 | 46 | if retriable && retries < self.max_retries 47 | retry 48 | else 49 | raise e 50 | end 51 | end 52 | 53 | def serialize(data) 54 | Postmark::Json.encode(data) 55 | end 56 | 57 | def take_response_of 58 | [yield, nil] 59 | rescue HttpServerError => e 60 | [e.full_response || {}, e] 61 | end 62 | 63 | def format_response(response, options = {}) 64 | return {} unless response 65 | 66 | compatible = options.fetch(:compatible, false) 67 | deep = options.fetch(:deep, false) 68 | 69 | if response.kind_of? Array 70 | response.map { |entry| Postmark::HashHelper.to_ruby(entry, :compatible => compatible, :deep => deep) } 71 | else 72 | Postmark::HashHelper.to_ruby(response, :compatible => compatible, :deep => deep) 73 | end 74 | end 75 | 76 | def get_resource_count(path, options = {}) 77 | # At this point Postmark API returns 0 as total if you request 0 documents 78 | total_count, _ = load_batch(path, nil, options.merge(:count => 1)) 79 | total_count 80 | end 81 | 82 | def load_batch(path, name, options) 83 | options[:offset] ||= 0 84 | options[:count] ||= 30 85 | response = http_client.get(path, options) 86 | format_batch_response(response, name) 87 | end 88 | 89 | def format_batch_response(response, name) 90 | [response['TotalCount'], format_response(response[name])] 91 | end 92 | 93 | end 94 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Postmark Logo 3 | 4 | 5 | # Postmark Ruby Gem 6 | 7 | [![Build Status](https://circleci.com/gh/ActiveCampaign/postmark-gem.svg?style=shield)](https://circleci.com/gh/ActiveCampaign/postmark-gem) 8 | [![Code Climate](https://codeclimate.com/github/ActiveCampaign/postmark-gem/badges/gpa.svg)](https://codeclimate.com/github/ActiveCampaign/postmark-gem) 9 | [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://www.opensource.org/licenses/MIT) 10 | [![Gem Version](https://badge.fury.io/rb/postmark.svg)](https://badge.fury.io/rb/postmark) 11 | 12 | Postmark allows you to send your emails with high delivery rates. It also includes detailed statistics. In addition, Postmark can parse incoming emails which are forwarded back to your application. 13 | 14 | This gem is the official wrapper for the [Postmark HTTP API](http://postmarkapp.com). 15 | 16 | ## Usage 17 | 18 | Please see the [wiki](https://github.com/ActiveCampaign/postmark-gem/wiki) for detailed instructions about sending email, using the bounce api and other Postmark API features. 19 | For details about Postmark API in general, please check out [Postmark developer docs](https://postmarkapp.com/developer). 20 | 21 | ## Requirements 22 | 23 | You will need a Postmark account, server and sender signature (or verified domain) set up to use it. For details about setup, check out [wiki pages](https://github.com/ActiveCampaign/postmark-gem/wiki/Getting-Started). 24 | 25 | If you plan using the library in a Rails project, check out the [postmark-rails](https://github.com/ActiveCampaign/postmark-rails/) gem, which 26 | is meant to integrate with ActionMailer. The plugin will try to use ActiveSupport JSon if it is already included. If not, 27 | it will attempt to use the built-in Ruby JSon library. 28 | 29 | You can also explicitly specify which one to be used, using following code: 30 | 31 | ```ruby 32 | Postmark.response_parser_class = :Json # :ActiveSupport or :Yajl are also supported. 33 | ``` 34 | 35 | ## Installation 36 | 37 | You can use the library with or without a Bundler. 38 | 39 | With Bundler: 40 | 41 | ```ruby 42 | gem 'postmark' 43 | ``` 44 | 45 | Without Bundler: 46 | 47 | ```bash 48 | gem install postmark 49 | ``` 50 | 51 | ## Note on Patches/Pull Requests 52 | 53 | See [CONTRIBUTING.md](CONTRIBUTING.md) file for details. 54 | 55 | ## Issues & Comments 56 | 57 | Feel free to contact us if you encounter any issues with the library or Postmark API. 58 | Please leave all comments, bugs, requests and issues on the Issues page. 59 | 60 | ## License 61 | 62 | The Postmark Ruby library is licensed under the [MIT](http://www.opensource.org/licenses/mit-license.php) license. 63 | Refer to the [LICENSE](https://github.com/ActiveCampaign/postmark-gem/blob/main/LICENSE) file for more information. 64 | 65 | ## Copyright 66 | 67 | Copyright © 2022 ActiveCampaign LLC. 68 | -------------------------------------------------------------------------------- /spec/unit/postmark/handlers/mail_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mail::Postmark do 4 | let(:message) do 5 | Mail.new do 6 | from "sheldon@bigbangtheory.com" 7 | to "lenard@bigbangtheory.com" 8 | subject "Hello!" 9 | body "Hello Sheldon!" 10 | end 11 | end 12 | 13 | before do 14 | message.delivery_method Mail::Postmark 15 | end 16 | 17 | subject(:handler) { message.delivery_method } 18 | 19 | it "can be set as delivery_method" do 20 | message.delivery_method Mail::Postmark 21 | 22 | is_expected.to be_a(Mail::Postmark) 23 | end 24 | 25 | describe '#deliver!' do 26 | it "returns self by default" do 27 | expect_any_instance_of(Postmark::ApiClient).to receive(:deliver_message).with(message) 28 | expect(message.deliver).to eq message 29 | end 30 | 31 | it "returns the actual response if :return_response setting is present" do 32 | expect_any_instance_of(Postmark::ApiClient).to receive(:deliver_message).with(message) 33 | message.delivery_method Mail::Postmark, :return_response => true 34 | expect(message.deliver).to eq message 35 | end 36 | 37 | it "allows setting the api token" do 38 | message.delivery_method Mail::Postmark, :api_token => 'api-token' 39 | expect(message.delivery_method.settings[:api_token]).to eq 'api-token' 40 | end 41 | 42 | it 'uses provided API token' do 43 | message.delivery_method Mail::Postmark, :api_token => 'api-token' 44 | expect(Postmark::ApiClient).to receive(:new).with('api-token', {}).and_return(double(:deliver_message => true)) 45 | message.deliver 46 | end 47 | 48 | it 'uses API token provided as legacy api_key' do 49 | message.delivery_method Mail::Postmark, :api_key => 'api-token' 50 | expect(Postmark::ApiClient).to receive(:new).with('api-token', {}).and_return(double(:deliver_message => true)) 51 | message.deliver 52 | end 53 | 54 | context 'when sending a pre-rendered message' do 55 | it "uses ApiClient#deliver_message to send the message" do 56 | expect_any_instance_of(Postmark::ApiClient).to receive(:deliver_message).with(message) 57 | message.deliver 58 | end 59 | end 60 | 61 | context 'when sending a Postmark template' do 62 | let(:message) do 63 | Mail.new do 64 | from "sheldon@bigbangtheory.com" 65 | to "lenard@bigbangtheory.com" 66 | template_alias "hello" 67 | template_model :name => "Sheldon" 68 | end 69 | end 70 | 71 | it 'uses ApiClient#deliver_message_with_template to send the message' do 72 | expect_any_instance_of(Postmark::ApiClient).to receive(:deliver_message_with_template).with(message) 73 | message.deliver 74 | end 75 | end 76 | end 77 | 78 | describe '#api_client' do 79 | subject { handler.api_client } 80 | 81 | it { is_expected.to be_a(Postmark::ApiClient) } 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/integration/mail_delivery_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Sending Mail::Messages with delivery_method Mail::Postmark" do 4 | let(:postmark_message_id_format) { /\w{8}\-\w{4}-\w{4}-\w{4}-\w{12}/ } 5 | 6 | let(:message) { 7 | Mail.new do 8 | from "sender@postmarkapp.com" 9 | to "recipient@postmarkapp.com" 10 | subject "Mail::Message object" 11 | body "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " 12 | "eiusmod tempor incididunt ut labore et dolore magna aliqua." 13 | delivery_method Mail::Postmark, :api_token => "POSTMARK_API_TEST", 14 | :http_open_timeout => 15, 15 | :http_read_timeout => 15 16 | 17 | end 18 | } 19 | 20 | let(:tagged_message) { message.tap { |m| m.tag "postmark-gem" } } 21 | 22 | let(:message_with_no_body) { 23 | Mail.new do 24 | from "sender@postmarkapp.com" 25 | to "recipient@postmarkapp.com" 26 | delivery_method Mail::Postmark, :api_token => "POSTMARK_API_TEST", 27 | :http_open_timeout => 15, 28 | :http_read_timeout => 15 29 | end 30 | } 31 | 32 | let(:message_with_attachment) { 33 | message.tap do |msg| 34 | msg.attachments["test.gif"] = File.read(File.join(File.dirname(__FILE__), '..', 'data', 'empty.gif')) 35 | end 36 | } 37 | 38 | let(:message_with_invalid_to) { 39 | Mail.new do 40 | from "sender@postmarkapp.com" 41 | to "@postmarkapp.com" 42 | delivery_method Mail::Postmark, :api_token => "POSTMARK_API_TEST", 43 | :http_open_timeout => 15, 44 | :http_read_timeout => 15 45 | end 46 | } 47 | 48 | it 'delivers a plain text message' do 49 | expect { message.deliver }.to change{message.delivered?}.to(true) 50 | end 51 | 52 | it 'updates a message object with X-PM-Message-Id' do 53 | expect { message.deliver }.to change{message['X-PM-Message-Id'].to_s}.to(postmark_message_id_format) 54 | end 55 | 56 | it 'updates a message object with full postmark response' do 57 | expect { message.deliver }.to change{message.postmark_response}.from(nil) 58 | end 59 | 60 | it 'delivers a tagged message' do 61 | expect { tagged_message.deliver }.to change{message.delivered?}.to(true) 62 | end 63 | 64 | it 'delivers a message with attachment' do 65 | expect { message_with_attachment.deliver }.to change{message_with_attachment.delivered?}.to(true) 66 | end 67 | 68 | context 'fails to deliver a message' do 69 | it ' without body - raise error' do 70 | expect { message_with_no_body.deliver! }.to raise_error(Postmark::InvalidMessageError) 71 | end 72 | 73 | it 'without body - do not deliver' do 74 | expect(message_with_no_body).not_to be_delivered 75 | end 76 | 77 | it 'with invalid To address - raise error' do 78 | expect { message_with_invalid_to.deliver! }.to raise_error(Postmark::InvalidMessageError) 79 | end 80 | 81 | it 'with invalid To address - do not deliver' do 82 | expect(message_with_invalid_to).not_to be_delivered 83 | end 84 | end 85 | end -------------------------------------------------------------------------------- /spec/unit/postmark/helpers/hash_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::HashHelper do 4 | describe ".to_postmark" do 5 | let(:source) do 6 | { 7 | :level_one => { 8 | :level_two => { 9 | :level_three => [{ :array_item => 1 }] 10 | } 11 | } 12 | } 13 | end 14 | 15 | describe 'default behaviour' do 16 | let(:target) do 17 | { 18 | 'LevelOne' => { 19 | :level_two => { 20 | :level_three => [{ :array_item => 1 }] 21 | } 22 | } 23 | } 24 | end 25 | 26 | it 'does not convert nested elements' do 27 | expect(subject.to_postmark(source)).to eq(target) 28 | end 29 | end 30 | 31 | describe 'deep conversion' do 32 | let(:target) do 33 | { 34 | 'LevelOne' => { 35 | 'LevelTwo' => { 36 | 'LevelThree' => [{ 'ArrayItem' => 1 }] 37 | } 38 | } 39 | } 40 | end 41 | 42 | it 'converts nested elements when requested' do 43 | expect(subject.to_postmark(source, :deep => true)).to eq(target) 44 | end 45 | end 46 | 47 | it 'leaves CamelCase keys untouched' do 48 | expect(subject.to_postmark('ReplyTo' => 'alice@example.com')).to eq('ReplyTo' => 'alice@example.com') 49 | end 50 | end 51 | 52 | describe ".to_ruby" do 53 | let(:source) do 54 | { 55 | 'LevelOne' => { 56 | 'LevelTwo' => { 57 | 'LevelThree' => [{ 'ArrayItem' => 1 }] 58 | } 59 | } 60 | } 61 | end 62 | 63 | describe 'default behaviour' do 64 | let(:target) do 65 | { 66 | :level_one => { 67 | 'LevelTwo' => { 68 | 'LevelThree' => [{ 'ArrayItem' => 1 }] 69 | } 70 | } 71 | } 72 | end 73 | 74 | it 'does not convert nested elements' do 75 | expect(subject.to_ruby(source)).to eq(target) 76 | end 77 | end 78 | 79 | describe 'deep conversion' do 80 | let(:target) do 81 | { 82 | :level_one => { 83 | :level_two => { 84 | :level_three => [{ :array_item => 1 }] 85 | } 86 | } 87 | } 88 | end 89 | 90 | it 'converts nested elements when requested' do 91 | expect(subject.to_ruby(source, :deep => true)).to eq(target) 92 | end 93 | end 94 | 95 | describe 'compatibility mode' do 96 | let(:target) do 97 | { 98 | :level_one => { 99 | 'LevelTwo' => { 100 | 'LevelThree' => [{ 'ArrayItem' => 1 }] 101 | } 102 | }, 103 | 'LevelOne' => { 104 | 'LevelTwo' => { 105 | 'LevelThree' => [{ 'ArrayItem' => 1 }] 106 | } 107 | } 108 | } 109 | end 110 | 111 | it 'preserves the original structure' do 112 | expect(subject.to_ruby(source, :compatible => true)).to eq target 113 | end 114 | end 115 | 116 | it 'leaves symbol keys untouched' do 117 | expect(subject.to_ruby(:reply_to => 'alice@example.com')).to eq(:reply_to => 'alice@example.com') 118 | end 119 | end 120 | end -------------------------------------------------------------------------------- /spec/integration/api_client_hashes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Sending messages as Ruby hashes with Postmark::ApiClient" do 4 | let(:message_id_format) {/<.+@.+>/} 5 | let(:postmark_message_id_format) {/\w{8}\-\w{4}-\w{4}-\w{4}-\w{12}/} 6 | let(:api_client) {Postmark::ApiClient.new('POSTMARK_API_TEST', :http_open_timeout => 15, :http_read_timeout => 15)} 7 | 8 | let(:message) { 9 | { 10 | :from => "sender@postmarkapp.com", 11 | :to => "recipient@postmarkapp.com", 12 | :subject => "Mail::Message object", 13 | :text_body => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " \ 14 | "sed do eiusmod tempor incididunt ut labore et dolore " \ 15 | "magna aliqua." 16 | } 17 | } 18 | 19 | let(:message_with_no_body) { 20 | { 21 | :from => "sender@postmarkapp.com", 22 | :to => "recipient@postmarkapp.com", 23 | } 24 | } 25 | 26 | let(:message_with_attachment) { 27 | message.tap do |m| 28 | m[:attachments] = [File.open(empty_gif_path)] 29 | end 30 | } 31 | 32 | let(:message_with_invalid_to) {{:from => "sender@postmarkapp.com", :to => "@postmarkapp.com"}} 33 | let(:valid_messages) {[message, message.dup]} 34 | let(:partially_valid_messages) {[message, message.dup, message_with_no_body]} 35 | let(:invalid_messages) {[message_with_no_body, message_with_no_body.dup]} 36 | 37 | context "single message" do 38 | it 'plain text message' do 39 | expect(api_client.deliver(message)).to have_key(:message_id) 40 | end 41 | 42 | it 'message with attachment' do 43 | expect(api_client.deliver(message_with_attachment)).to have_key(:message_id) 44 | end 45 | 46 | it 'response Message-ID' do 47 | expect(api_client.deliver(message)[:message_id]).to be =~ postmark_message_id_format 48 | end 49 | 50 | it 'response is Hash' do 51 | expect(api_client.deliver(message)).to be_a Hash 52 | end 53 | 54 | it 'fails to deliver a message without body' do 55 | expect {api_client.deliver(message_with_no_body)}.to raise_error(Postmark::InvalidMessageError) 56 | end 57 | 58 | it 'fails to deliver a message with invalid To address' do 59 | expect {api_client.deliver(message_with_invalid_to)}.to raise_error(Postmark::InvalidMessageError) 60 | end 61 | end 62 | 63 | context "batch message" do 64 | it 'response messages count' do 65 | expect(api_client.deliver_in_batches(valid_messages).count).to eq valid_messages.count 66 | end 67 | 68 | context "custom max_batch_size" do 69 | before do 70 | api_client.max_batch_size = 1 71 | end 72 | 73 | it 'response message count' do 74 | expect(api_client.deliver_in_batches(valid_messages).count).to eq valid_messages.count 75 | end 76 | end 77 | 78 | it 'partially delivers a batch of partially valid Mail::Message objects' do 79 | response = api_client.deliver_in_batches(partially_valid_messages) 80 | expect(response).to satisfy {|r| r.count {|mr| mr[:error_code].to_i.zero?} == 2} 81 | end 82 | 83 | it "doesn't deliver a batch of invalid Mail::Message objects" do 84 | response = api_client.deliver_in_batches(invalid_messages) 85 | expect(response).to satisfy {|r| r.all? {|mr| !!mr[:error_code]}} 86 | end 87 | end 88 | end -------------------------------------------------------------------------------- /lib/postmark/http_client.rb: -------------------------------------------------------------------------------- 1 | require 'thread' unless defined? Mutex # For Ruby 1.8.7 2 | require 'cgi' 3 | 4 | module Postmark 5 | class HttpClient 6 | attr_accessor :api_token 7 | attr_reader :http, :secure, :proxy_host, :proxy_port, :proxy_user, 8 | :proxy_pass, :host, :port, :path_prefix, 9 | :http_open_timeout, :http_read_timeout, :http_ssl_version, 10 | :auth_header_name 11 | 12 | alias_method :api_key, :api_token 13 | alias_method :api_key=, :api_token= 14 | 15 | DEFAULTS = { 16 | :auth_header_name => 'X-Postmark-Server-Token', 17 | :host => 'api.postmarkapp.com', 18 | :secure => true, 19 | :path_prefix => '/', 20 | :http_read_timeout => 15, 21 | :http_open_timeout => 5 22 | } 23 | 24 | def initialize(api_token, options = {}) 25 | @api_token = api_token 26 | @request_mutex = Mutex.new 27 | apply_options(options) 28 | @http = build_http 29 | end 30 | 31 | def post(path, data = '') 32 | do_request { |client| client.post(url_path(path), data, headers) } 33 | end 34 | 35 | def put(path, data = '') 36 | do_request { |client| client.put(url_path(path), data, headers) } 37 | end 38 | 39 | def patch(path, data = '') 40 | do_request { |client| client.patch(url_path(path), data, headers) } 41 | end 42 | 43 | def get(path, query = {}) 44 | do_request { |client| client.get(url_path(path + to_query_string(query)), headers) } 45 | end 46 | 47 | def delete(path, query = {}) 48 | do_request { |client| client.delete(url_path(path + to_query_string(query)), headers) } 49 | end 50 | 51 | def protocol 52 | self.secure ? 'https' : 'http' 53 | end 54 | 55 | protected 56 | 57 | def apply_options(options = {}) 58 | options = Hash[*options.select { |_, v| !v.nil? }.flatten] 59 | DEFAULTS.merge(options).each_pair do |name, value| 60 | instance_variable_set(:"@#{name}", value) 61 | end 62 | @port = options[:port] || (@secure ? 443 : 80) 63 | end 64 | 65 | def to_query_string(hash) 66 | return "" if hash.empty? 67 | "?" + hash.map { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" }.join("&") 68 | end 69 | 70 | def url 71 | URI.parse("#{protocol}://#{self.host}:#{self.port}/") 72 | end 73 | 74 | def handle_response(response) 75 | if response.code.to_i == 200 76 | Postmark::Json.decode(response.body) 77 | else 78 | raise HttpServerError.build(response.code, response.body) 79 | end 80 | end 81 | 82 | def headers 83 | HEADERS.merge(self.auth_header_name => self.api_token.to_s) 84 | end 85 | 86 | def url_path(path) 87 | self.path_prefix + path 88 | end 89 | 90 | def do_request 91 | @request_mutex.synchronize do 92 | handle_response(yield(http)) 93 | end 94 | rescue Timeout::Error => e 95 | raise TimeoutError.new(e) 96 | rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e 97 | raise HttpClientError.new(e.message) 98 | end 99 | 100 | def build_http 101 | http = Net::HTTP::Proxy(self.proxy_host, 102 | self.proxy_port, 103 | self.proxy_user, 104 | self.proxy_pass).new(url.host, url.port) 105 | 106 | http.read_timeout = self.http_read_timeout 107 | http.open_timeout = self.http_open_timeout 108 | http.use_ssl = !!self.secure 109 | http.ssl_version = self.http_ssl_version if self.http_ssl_version && http.respond_to?(:ssl_version=) 110 | http 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/postmark/error.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | class Error < ::StandardError; end 3 | 4 | class HttpClientError < Error 5 | def retry? 6 | true 7 | end 8 | end 9 | 10 | class HttpServerError < Error 11 | attr_accessor :status_code, :parsed_body, :body 12 | 13 | alias_method :full_response, :parsed_body 14 | 15 | def self.build(status_code, body) 16 | parsed_body = Postmark::Json.decode(body) rescue {} 17 | 18 | case status_code 19 | when '401' 20 | InvalidApiKeyError.new(401, body, parsed_body) 21 | when '422' 22 | ApiInputError.build(body, parsed_body) 23 | when '500' 24 | InternalServerError.new(500, body, parsed_body) 25 | else 26 | UnexpectedHttpResponseError.new(status_code, body, parsed_body) 27 | end 28 | end 29 | 30 | def initialize(status_code = 500, body = '', parsed_body = {}) 31 | self.parsed_body = parsed_body 32 | self.status_code = status_code.to_i 33 | self.body = body 34 | message = parsed_body.fetch('Message', "The Postmark API responded with HTTP status #{status_code}.") 35 | 36 | super(message) 37 | end 38 | 39 | def retry? 40 | 5 == status_code / 100 41 | end 42 | end 43 | 44 | class ApiInputError < HttpServerError 45 | INACTIVE_RECIPIENT = 406 46 | INVALID_EMAIL_ADDRESS = 300 47 | 48 | attr_accessor :error_code 49 | 50 | def self.build(body, parsed_body) 51 | error_code = parsed_body['ErrorCode'].to_i 52 | 53 | case error_code 54 | when INACTIVE_RECIPIENT 55 | InactiveRecipientError.new(error_code, body, parsed_body) 56 | when INVALID_EMAIL_ADDRESS 57 | InvalidEmailAddressError.new(error_code, body, parsed_body) 58 | else 59 | new(error_code, body, parsed_body) 60 | end 61 | end 62 | 63 | def initialize(error_code = nil, body = '', parsed_body = {}) 64 | self.error_code = error_code.to_i 65 | super(422, body, parsed_body) 66 | end 67 | 68 | def retry? 69 | false 70 | end 71 | end 72 | 73 | class InvalidEmailAddressError < ApiInputError; end 74 | 75 | class InactiveRecipientError < ApiInputError 76 | attr_reader :recipients 77 | 78 | PATTERNS = [/^Found inactive addresses: (.+?)\.$/.freeze, 79 | /these inactive addresses: (.+?)\. Inactive/.freeze, 80 | /these inactive addresses: (.+?)\.?$/].freeze 81 | 82 | def self.parse_recipients(message) 83 | PATTERNS.each do |p| 84 | _, recipients = p.match(message).to_a 85 | next unless recipients 86 | return recipients.split(', ') 87 | end 88 | 89 | [] 90 | end 91 | 92 | def initialize(*args) 93 | super 94 | @recipients = parse_recipients || [] 95 | end 96 | 97 | private 98 | 99 | def parse_recipients 100 | return unless parsed_body && !parsed_body.empty? 101 | 102 | self.class.parse_recipients(parsed_body['Message']) 103 | end 104 | end 105 | 106 | class InvalidTemplateError < Error 107 | attr_reader :postmark_response 108 | 109 | def initialize(response) 110 | @postmark_response = response 111 | super('Failed to render the template. Please check #postmark_response on this error for details.') 112 | end 113 | end 114 | 115 | class TimeoutError < Error 116 | def retry? 117 | true 118 | end 119 | end 120 | 121 | class MailAdapterError < Postmark::Error; end 122 | class UnknownMessageType < Error; end 123 | class InvalidApiKeyError < HttpServerError; end 124 | class InternalServerError < HttpServerError; end 125 | class UnexpectedHttpResponseError < HttpServerError; end 126 | 127 | # Backwards compatible aliases 128 | DeliveryError = Error 129 | InvalidMessageError = ApiInputError 130 | UnknownError = UnexpectedHttpResponseError 131 | end 132 | -------------------------------------------------------------------------------- /spec/integration/account_api_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Account API client usage' do 4 | 5 | subject { Postmark::AccountApiClient.new(ENV['POSTMARK_ACCOUNT_API_KEY'], 6 | :http_open_timeout => 15, 7 | :http_read_timeout => 15) } 8 | let(:unique_token) { rand(36**32).to_s(36) } 9 | let(:unique_from_email) { ENV['POSTMARK_CI_SENDER'].gsub(/(\+.+)?@/, "+#{unique_token}@") } 10 | 11 | it 'manage senders' do 12 | # create & count 13 | new_sender = subject.create_sender(:name => 'Integration Test', :from_email => unique_from_email) 14 | expect(subject.get_senders_count).to be > 0 15 | 16 | # get 17 | expect(subject.get_sender(new_sender[:id])[:id]).to eq(new_sender[:id]) 18 | 19 | # list 20 | senders = subject.get_senders(:count => 50) 21 | expect(senders.map { |s| s[:id] }).to include(new_sender[:id]) 22 | 23 | # collection 24 | expect(subject.senders.map { |s| s[:id] }).to include(new_sender[:id]) 25 | 26 | # update 27 | updated_sender = subject.update_sender(new_sender[:id], :name => 'New Name') 28 | expect(updated_sender[:name]).to eq('New Name') 29 | expect(updated_sender[:id]).to eq(new_sender[:id]) 30 | 31 | 32 | # spf 33 | expect(subject.verified_sender_spf?(new_sender[:id])).to be true 34 | 35 | # resend 36 | expect { subject.resend_sender_confirmation(new_sender[:id]) }.not_to raise_error 37 | 38 | # dkim 39 | expect { subject.request_new_sender_dkim(new_sender[:id]) }. 40 | to raise_error(Postmark::InvalidMessageError, 41 | 'This DKIM is already being renewed.') 42 | 43 | # delete 44 | expect { subject.delete_sender(new_sender[:id]) }.not_to raise_error 45 | end 46 | 47 | it 'manage domains' do 48 | domain_name = "#{unique_token}-gem-integration.test" 49 | return_path = "return.#{domain_name}" 50 | updated_return_path = "updated-return.#{domain_name}" 51 | 52 | # create & count 53 | new_domain = subject.create_domain(:name => domain_name, 54 | :return_path_domain => return_path) 55 | expect(subject.get_domains_count).to be > 0 56 | 57 | # get 58 | expect(subject.get_domain(new_domain[:id])[:id]).to eq(new_domain[:id]) 59 | 60 | # list 61 | domains = subject.get_domains(:count => 50) 62 | expect(domains.map { |d| d[:id] }).to include(new_domain[:id]) 63 | 64 | # collection 65 | expect(subject.domains.map { |d| d[:id] }).to include(new_domain[:id]) 66 | 67 | # update 68 | updated_domain = subject.update_domain(new_domain[:id], :return_path_domain => updated_return_path) 69 | expect(updated_domain[:return_path_domain]).to eq(updated_return_path) 70 | expect(updated_domain[:id]).to eq(new_domain[:id]) 71 | 72 | # spf 73 | expect(subject.verified_domain_spf?(new_domain[:id])).to be true 74 | 75 | # dkim 76 | expect { subject.rotate_domain_dkim(new_domain[:id]) }. 77 | to raise_error(Postmark::InvalidMessageError, 78 | 'This DKIM is already being renewed.') 79 | 80 | # delete 81 | expect { subject.delete_domain(new_domain[:id]) }.not_to raise_error 82 | end 83 | 84 | it 'manage servers' do 85 | # create & count 86 | new_server = subject.create_server(:name => "server-#{unique_token}", 87 | :color => 'red') 88 | expect(subject.get_servers_count).to be > 0 89 | 90 | # get 91 | expect(subject.get_server(new_server[:id])[:id]).to eq(new_server[:id]) 92 | 93 | # list 94 | servers = subject.get_servers(:count => 50) 95 | expect(servers.map { |s| s[:id] }).to include(new_server[:id]) 96 | 97 | # collection 98 | expect(subject.servers.map { |s| s[:id] }).to include(new_server[:id]) 99 | 100 | # update 101 | updated_server = subject.update_server(new_server[:id], :color => 'blue') 102 | expect(updated_server[:color]).to eq('blue') 103 | expect(updated_server[:id]).to eq(new_server[:id]) 104 | 105 | # delete 106 | expect { subject.delete_server(new_server[:id]) }.not_to raise_error 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/integration/api_client_messages_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Sending Mail::Messages with Postmark::ApiClient" do 4 | let(:postmark_message_id_format) { /\w{8}\-\w{4}-\w{4}-\w{4}-\w{12}/ } 5 | let(:api_client) { Postmark::ApiClient.new('POSTMARK_API_TEST', :http_open_timeout => 15, :http_read_timeout => 15) } 6 | 7 | let(:message) { 8 | Mail.new do 9 | from "sender@postmarkapp.com" 10 | to "recipient@postmarkapp.com" 11 | subject "Mail::Message object" 12 | body "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " 13 | "eiusmod tempor incididunt ut labore et dolore magna aliqua." 14 | end 15 | } 16 | 17 | let(:message_with_no_body) { 18 | Mail.new do 19 | from "sender@postmarkapp.com" 20 | to "recipient@postmarkapp.com" 21 | end 22 | } 23 | 24 | let(:message_with_attachment) { message.tap { |msg| msg.attachments["test.gif"] = File.read(empty_gif_path) } } 25 | 26 | let(:message_with_invalid_to) { 27 | Mail.new do 28 | from "sender@postmarkapp.com" 29 | to "@postmarkapp.com" 30 | end 31 | } 32 | 33 | let(:valid_messages) { [message, message.dup] } 34 | let(:partially_valid_messages) { [message, message.dup, message_with_no_body] } 35 | let(:invalid_messages) { [message_with_no_body, message_with_no_body.dup] } 36 | 37 | context 'invalid API code' do 38 | it "doesn't deliver messages" do 39 | expect { 40 | Postmark::ApiClient.new('INVALID').deliver_message(message) rescue Postmark::InvalidApiKeyError 41 | }.to change{message.delivered?}.to(false) 42 | end 43 | end 44 | 45 | context "single message" do 46 | it 'plain text message' do 47 | expect(api_client.deliver_message(message)).to have_key(:message_id) 48 | end 49 | 50 | it 'message with attachment' do 51 | expect(api_client.deliver_message(message_with_attachment)).to have_key(:message_id) 52 | end 53 | 54 | it 'response Message-ID' do 55 | expect(api_client.deliver_message(message)[:message_id]).to be =~ postmark_message_id_format 56 | end 57 | 58 | it 'response is Hash' do 59 | expect(api_client.deliver_message(message)).to be_a Hash 60 | end 61 | 62 | it 'fails to deliver a message without body' do 63 | expect { api_client.deliver_message(message_with_no_body) }.to raise_error(Postmark::InvalidMessageError) 64 | end 65 | 66 | it 'fails to deliver a message with invalid To address' do 67 | expect { api_client.deliver_message(message_with_invalid_to) }.to raise_error(Postmark::InvalidMessageError) 68 | end 69 | end 70 | 71 | context "batch message" do 72 | it 'response - valid Mail::Message objects' do 73 | expect { api_client.deliver_messages(valid_messages) }. 74 | to change{valid_messages.all? { |m| m.delivered? }}.to true 75 | end 76 | 77 | it 'response - valid X-PM-Message-Ids' do 78 | api_client.deliver_messages(valid_messages) 79 | expect(valid_messages.all? { |m| m['X-PM-Message-Id'].to_s =~ postmark_message_id_format }).to be true 80 | end 81 | 82 | it 'response - valid response objects' do 83 | api_client.deliver_messages(valid_messages) 84 | expect(valid_messages.all? { |m| m.postmark_response["To"] == m.to[0] }).to be true 85 | end 86 | 87 | it 'response - message responses count' do 88 | expect(api_client.deliver_messages(valid_messages).count).to eq valid_messages.count 89 | end 90 | 91 | context "custom max_batch_size" do 92 | before do 93 | api_client.max_batch_size = 1 94 | end 95 | 96 | it 'response - valid response objects' do 97 | api_client.deliver_messages(valid_messages) 98 | expect(valid_messages.all? { |m| m.postmark_response["To"] == m.to[0] }).to be true 99 | end 100 | 101 | it 'response - message responses count' do 102 | expect(api_client.deliver_messages(valid_messages).count).to eq valid_messages.count 103 | end 104 | end 105 | 106 | it 'partially delivers a batch of partially valid Mail::Message objects' do 107 | expect { api_client.deliver_messages(partially_valid_messages) }. 108 | to change{partially_valid_messages.select { |m| m.delivered? }.count}.to 2 109 | end 110 | 111 | it "doesn't deliver a batch of invalid Mail::Message objects" do 112 | aggregate_failures do 113 | expect { api_client.deliver_messages(invalid_messages) }. 114 | to change{invalid_messages.all? { |m| m.delivered? == false }}.to true 115 | 116 | expect(invalid_messages).to satisfy { |ms| ms.all? { |m| !!m.postmark_response }} 117 | end 118 | end 119 | end 120 | end -------------------------------------------------------------------------------- /lib/postmark/account_api_client.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | 3 | class AccountApiClient < Client 4 | 5 | def initialize(api_token, options = {}) 6 | options[:auth_header_name] = 'X-Postmark-Account-Token' 7 | super 8 | end 9 | 10 | def senders(options = {}) 11 | find_each('senders', 'SenderSignatures', options) 12 | end 13 | alias_method :signatures, :senders 14 | 15 | def get_senders(options = {}) 16 | load_batch('senders', 'SenderSignatures', options).last 17 | end 18 | alias_method :get_signatures, :get_senders 19 | 20 | def get_senders_count(options = {}) 21 | get_resource_count('senders', options) 22 | end 23 | alias_method :get_signatures_count, :get_senders_count 24 | 25 | def get_sender(id) 26 | format_response http_client.get("senders/#{id.to_i}") 27 | end 28 | alias_method :get_signature, :get_sender 29 | 30 | def create_sender(attributes = {}) 31 | data = serialize(HashHelper.to_postmark(attributes)) 32 | 33 | format_response http_client.post('senders', data) 34 | end 35 | alias_method :create_signature, :create_sender 36 | 37 | def update_sender(id, attributes = {}) 38 | data = serialize(HashHelper.to_postmark(attributes)) 39 | 40 | format_response http_client.put("senders/#{id.to_i}", data) 41 | end 42 | alias_method :update_signature, :update_sender 43 | 44 | def resend_sender_confirmation(id) 45 | format_response http_client.post("senders/#{id.to_i}/resend") 46 | end 47 | alias_method :resend_signature_confirmation, :resend_sender_confirmation 48 | 49 | def verified_sender_spf?(id) 50 | !!http_client.post("senders/#{id.to_i}/verifyspf")['SPFVerified'] 51 | end 52 | alias_method :verified_signature_spf?, :verified_sender_spf? 53 | 54 | def request_new_sender_dkim(id) 55 | format_response http_client.post("senders/#{id.to_i}/requestnewdkim") 56 | end 57 | alias_method :request_new_signature_dkim, :request_new_sender_dkim 58 | 59 | def delete_sender(id) 60 | format_response http_client.delete("senders/#{id.to_i}") 61 | end 62 | alias_method :delete_signature, :delete_sender 63 | 64 | def domains(options = {}) 65 | find_each('domains', 'Domains', options) 66 | end 67 | 68 | def get_domains(options = {}) 69 | load_batch('domains', 'Domains', options).last 70 | end 71 | 72 | def get_domains_count(options = {}) 73 | get_resource_count('domains', options) 74 | end 75 | 76 | def get_domain(id) 77 | format_response http_client.get("domains/#{id.to_i}") 78 | end 79 | 80 | def create_domain(attributes = {}) 81 | data = serialize(HashHelper.to_postmark(attributes)) 82 | 83 | format_response http_client.post('domains', data) 84 | end 85 | 86 | def update_domain(id, attributes = {}) 87 | data = serialize(HashHelper.to_postmark(attributes)) 88 | 89 | format_response http_client.put("domains/#{id.to_i}", data) 90 | end 91 | 92 | def verify_domain_dkim(id) 93 | format_response http_client.put("domains/#{id.to_i}/verifydkim") 94 | end 95 | 96 | def verify_domain_return_path(id) 97 | format_response http_client.put("domains/#{id.to_i}/verifyreturnpath") 98 | end 99 | 100 | def verified_domain_spf?(id) 101 | !!http_client.post("domains/#{id.to_i}/verifyspf")['SPFVerified'] 102 | end 103 | 104 | def rotate_domain_dkim(id) 105 | format_response http_client.post("domains/#{id.to_i}/rotatedkim") 106 | end 107 | 108 | def delete_domain(id) 109 | format_response http_client.delete("domains/#{id.to_i}") 110 | end 111 | 112 | def servers(options = {}) 113 | find_each('servers', 'Servers', options) 114 | end 115 | 116 | def get_servers(options = {}) 117 | load_batch('servers', 'Servers', options).last 118 | end 119 | 120 | def get_servers_count(options = {}) 121 | get_resource_count('servers', options) 122 | end 123 | 124 | def get_server(id) 125 | format_response http_client.get("servers/#{id.to_i}") 126 | end 127 | 128 | def create_server(attributes = {}) 129 | data = serialize(HashHelper.to_postmark(attributes)) 130 | format_response http_client.post('servers', data) 131 | end 132 | 133 | def update_server(id, attributes = {}) 134 | data = serialize(HashHelper.to_postmark(attributes)) 135 | format_response http_client.put("servers/#{id.to_i}", data) 136 | end 137 | 138 | def delete_server(id) 139 | format_response http_client.delete("servers/#{id.to_i}") 140 | end 141 | 142 | def push_templates(attributes = {}) 143 | data = serialize(HashHelper.to_postmark(attributes)) 144 | _, batch = format_batch_response(http_client.put('templates/push', data), "Templates") 145 | batch 146 | end 147 | 148 | end 149 | 150 | end 151 | -------------------------------------------------------------------------------- /spec/unit/postmark/bounce_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::Bounce do 4 | let(:bounce_data) { {:type => "HardBounce", 5 | :message_id => "d12c2f1c-60f3-4258-b163-d17052546ae4", 6 | :type_code => 1, 7 | :description => "The server was unable to deliver your message (ex: unknown user, mailbox not found).", 8 | :details => "test bounce", 9 | :email => "jim@test.com", 10 | :bounced_at => "2013-04-22 18:06:48 +0800", 11 | :dump_available => true, 12 | :inactive => false, 13 | :can_activate => true, 14 | :id => 42, 15 | :subject => "Hello from our app!"} } 16 | let(:bounce_data_postmark) { Postmark::HashHelper.to_postmark(bounce_data) } 17 | let(:bounces_data) { [bounce_data, bounce_data, bounce_data] } 18 | let(:bounce) { Postmark::Bounce.new(bounce_data) } 19 | 20 | subject { bounce } 21 | 22 | context "attr readers" do 23 | it { expect(subject).to respond_to(:email) } 24 | it { expect(subject).to respond_to(:bounced_at) } 25 | it { expect(subject).to respond_to(:type) } 26 | it { expect(subject).to respond_to(:description) } 27 | it { expect(subject).to respond_to(:details) } 28 | it { expect(subject).to respond_to(:name) } 29 | it { expect(subject).to respond_to(:id) } 30 | it { expect(subject).to respond_to(:server_id) } 31 | it { expect(subject).to respond_to(:tag) } 32 | it { expect(subject).to respond_to(:message_id) } 33 | it { expect(subject).to respond_to(:subject) } 34 | end 35 | 36 | context "given a bounce created from bounce_data" do 37 | 38 | it 'is not inactive' do 39 | expect(subject).not_to be_inactive 40 | end 41 | 42 | it 'allows to activate the bounce' do 43 | expect(subject.can_activate?).to be true 44 | end 45 | 46 | it 'has an available dump' do 47 | expect(subject.dump_available?).to be true 48 | end 49 | 50 | its(:type) { is_expected.to eq bounce_data[:type] } 51 | its(:message_id) { is_expected.to eq bounce_data[:message_id] } 52 | its(:description) { is_expected.to eq bounce_data[:description] } 53 | its(:details) { is_expected.to eq bounce_data[:details] } 54 | its(:email) { is_expected.to eq bounce_data[:email] } 55 | its(:bounced_at) { is_expected.to eq Time.parse(bounce_data[:bounced_at]) } 56 | its(:id) { is_expected.to eq bounce_data[:id] } 57 | its(:subject) { is_expected.to eq bounce_data[:subject] } 58 | 59 | end 60 | 61 | context "given a bounce created from bounce_data_postmark" do 62 | subject { Postmark::Bounce.new(bounce_data_postmark) } 63 | 64 | it 'is not inactive' do 65 | expect(subject).not_to be_inactive 66 | end 67 | 68 | it 'allows to activate the bounce' do 69 | expect(subject.can_activate?).to be true 70 | end 71 | 72 | it 'has an available dump' do 73 | expect(subject.dump_available?).to be true 74 | end 75 | 76 | its(:type) { is_expected.to eq bounce_data[:type] } 77 | its(:message_id) { is_expected.to eq bounce_data[:message_id] } 78 | its(:details) { is_expected.to eq bounce_data[:details] } 79 | its(:email) { is_expected.to eq bounce_data[:email] } 80 | its(:bounced_at) { is_expected.to eq Time.parse(bounce_data[:bounced_at]) } 81 | its(:id) { is_expected.to eq bounce_data[:id] } 82 | its(:subject) { is_expected.to eq bounce_data[:subject] } 83 | end 84 | 85 | describe "#dump" do 86 | let(:bounce_body) { double } 87 | let(:response) { {:body => bounce_body} } 88 | let(:api_client) { Postmark.api_client } 89 | 90 | it "calls #dump_bounce on shared api_client instance" do 91 | expect(Postmark.api_client).to receive(:dump_bounce).with(bounce.id) { response } 92 | expect(bounce.dump).to eq bounce_body 93 | end 94 | end 95 | 96 | describe "#activate" do 97 | let(:api_client) { Postmark.api_client } 98 | 99 | it "calls #activate_bounce on shared api_client instance" do 100 | expect(api_client).to receive(:activate_bounce).with(bounce.id) { bounce_data } 101 | expect(bounce.activate).to be_a Postmark::Bounce 102 | end 103 | end 104 | 105 | describe ".find" do 106 | let(:api_client) { Postmark.api_client } 107 | 108 | it "calls #get_bounce on shared api_client instance" do 109 | expect(api_client).to receive(:get_bounce).with(42) { bounce_data } 110 | expect(Postmark::Bounce.find(42)).to be_a Postmark::Bounce 111 | end 112 | end 113 | 114 | describe ".all" do 115 | let(:response) { bounces_data } 116 | let(:api_client) { Postmark.api_client } 117 | 118 | it "calls #get_bounces on shared api_client instance" do 119 | expect(api_client).to receive(:get_bounces) { response } 120 | expect(Postmark::Bounce.all.count).to eq(3) 121 | end 122 | end 123 | end -------------------------------------------------------------------------------- /spec/unit/postmark/inbound_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::Inbound do 4 | # http://developer.postmarkapp.com/developer-inbound-parse.html#example-hook 5 | let(:example_inbound) { '{"From":"myUser@theirDomain.com","FromFull":{"Email":"myUser@theirDomain.com","Name":"John Doe"},"To":"451d9b70cf9364d23ff6f9d51d870251569e+ahoy@inbound.postmarkapp.com","ToFull":[{"Email":"451d9b70cf9364d23ff6f9d51d870251569e+ahoy@inbound.postmarkapp.com","Name":""}],"Cc":"\"Full name\" , \"Another Cc\" ","CcFull":[{"Email":"sample.cc@emailDomain.com","Name":"Full name"},{"Email":"another.cc@emailDomain.com","Name":"Another Cc"}],"ReplyTo":"myUsersReplyAddress@theirDomain.com","Subject":"This is an inbound message","MessageID":"22c74902-a0c1-4511-804f2-341342852c90","Date":"Thu, 5 Apr 2012 16:59:01 +0200","MailboxHash":"ahoy","TextBody":"[ASCII]","HtmlBody":"[HTML(encoded)]","Tag":"","Headers":[{"Name":"X-Spam-Checker-Version","Value":"SpamAssassin 3.3.1 (2010-03-16) onrs-ord-pm-inbound1.postmarkapp.com"},{"Name":"X-Spam-Status","Value":"No"},{"Name":"X-Spam-Score","Value":"-0.1"},{"Name":"X-Spam-Tests","Value":"DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,SPF_PASS"},{"Name":"Received-SPF","Value":"Pass (sender SPF authorized) identity=mailfrom; client-ip=209.85.160.180; helo=mail-gy0-f180.google.com; envelope-from=myUser@theirDomain.com; receiver=451d9b70cf9364d23ff6f9d51d870251569e+ahoy@inbound.postmarkapp.com"},{"Name":"DKIM-Signature","Value":"v=1; a=rsa-sha256; c=relaxed\/relaxed; d=postmarkapp.com; s=google; h=mime-version:reply-to:date:message-id:subject:from:to:cc :content-type; bh=cYr\/+oQiklaYbBJOQU3CdAnyhCTuvemrU36WT7cPNt0=; b=QsegXXbTbC4CMirl7A3VjDHyXbEsbCUTPL5vEHa7hNkkUTxXOK+dQA0JwgBHq5C+1u iuAJMz+SNBoTqEDqte2ckDvG2SeFR+Edip10p80TFGLp5RucaYvkwJTyuwsA7xd78NKT Q9ou6L1hgy\/MbKChnp2kxHOtYNOrrszY3JfQM="},{"Name":"MIME-Version","Value":"1.0"},{"Name":"Message-ID","Value":""}],"Attachments":[{"Name":"myimage.png","Content":"[BASE64-ENCODED CONTENT]","ContentType":"image/png","ContentLength":4096},{"Name":"mypaper.doc","Content":"[BASE64-ENCODED CONTENT]","ContentType":"application/msword","ContentLength":16384}]}' } 6 | 7 | context "given a serialized inbound document" do 8 | subject { Postmark::Inbound.to_ruby_hash(example_inbound) } 9 | 10 | it { expect(subject).to have_key(:from) } 11 | it { expect(subject).to have_key(:from_full) } 12 | it { expect(subject).to have_key(:to) } 13 | it { expect(subject).to have_key(:to_full) } 14 | it { expect(subject).to have_key(:cc) } 15 | it { expect(subject).to have_key(:cc_full) } 16 | it { expect(subject).to have_key(:reply_to) } 17 | it { expect(subject).to have_key(:subject) } 18 | it { expect(subject).to have_key(:message_id) } 19 | it { expect(subject).to have_key(:date) } 20 | it { expect(subject).to have_key(:mailbox_hash) } 21 | it { expect(subject).to have_key(:text_body) } 22 | it { expect(subject).to have_key(:html_body) } 23 | it { expect(subject).to have_key(:tag) } 24 | it { expect(subject).to have_key(:headers) } 25 | it { expect(subject).to have_key(:attachments) } 26 | 27 | context "cc" do 28 | it 'has 2 CCs' do 29 | expect(subject[:cc_full].count).to eq 2 30 | end 31 | 32 | it 'stores CCs as an array of Ruby hashes' do 33 | cc = subject[:cc_full].last 34 | expect(cc).to have_key(:email) 35 | expect(cc).to have_key(:name) 36 | end 37 | end 38 | 39 | context "to" do 40 | it 'has 1 recipients' do 41 | expect(subject[:to_full].count).to eq 1 42 | end 43 | 44 | it 'stores TOs as an array of Ruby hashes' do 45 | cc = subject[:to_full].last 46 | expect(cc).to have_key(:email) 47 | expect(cc).to have_key(:name) 48 | end 49 | end 50 | 51 | context "from" do 52 | it 'is a hash' do 53 | expect(subject[:from_full]).to be_a Hash 54 | end 55 | 56 | it 'has all required fields' do 57 | expect(subject[:from_full]).to have_key(:email) 58 | expect(subject[:from_full]).to have_key(:name) 59 | end 60 | end 61 | 62 | context "headers" do 63 | it 'has 8 headers' do 64 | expect(subject[:headers].count).to eq 8 65 | end 66 | 67 | it 'stores headers as an array of Ruby hashes' do 68 | header = subject[:headers].last 69 | expect(header).to have_key(:name) 70 | expect(header).to have_key(:value) 71 | end 72 | end 73 | 74 | context "attachments" do 75 | it 'has 2 attachments' do 76 | expect(subject[:attachments].count).to eq 2 77 | end 78 | 79 | it 'stores attachemnts as an array of Ruby hashes' do 80 | attachment = subject[:attachments].last 81 | expect(attachment).to have_key(:name) 82 | expect(attachment).to have_key(:content) 83 | expect(attachment).to have_key(:content_type) 84 | expect(attachment).to have_key(:content_length) 85 | end 86 | end 87 | end 88 | end -------------------------------------------------------------------------------- /lib/postmark/message_extensions/mail.rb: -------------------------------------------------------------------------------- 1 | module Mail 2 | class Message 3 | 4 | attr_accessor :delivered, :postmark_response 5 | 6 | def delivered? 7 | self.delivered 8 | end 9 | 10 | def tag(val = nil) 11 | default 'TAG', val 12 | end 13 | 14 | def tag=(val) 15 | header['TAG'] = val 16 | end 17 | 18 | def track_links(val = nil) 19 | self.track_links=(val) unless val.nil? 20 | header['TRACK-LINKS'].to_s 21 | end 22 | 23 | def track_links=(val) 24 | header['TRACK-LINKS'] = ::Postmark::Inflector.to_postmark(val) 25 | end 26 | 27 | def track_opens(val = nil) 28 | self.track_opens=(val) unless val.nil? 29 | header['TRACK-OPENS'].to_s 30 | end 31 | 32 | def track_opens=(val) 33 | header['TRACK-OPENS'] = (!!val).to_s 34 | end 35 | 36 | def metadata(val = nil) 37 | if val 38 | @metadata = val 39 | else 40 | @metadata ||= {} 41 | end 42 | end 43 | 44 | def metadata=(val) 45 | @metadata = val 46 | end 47 | 48 | def postmark_attachments=(value) 49 | Kernel.warn("Mail::Message#postmark_attachments= is deprecated and will " \ 50 | "be removed in the future. Please consider using the native " \ 51 | "attachments API provided by Mail library.") 52 | @_attachments = value 53 | end 54 | 55 | def postmark_attachments 56 | return [] if @_attachments.nil? 57 | Kernel.warn("Mail::Message#postmark_attachments is deprecated and will " \ 58 | "be removed in the future. Please consider using the native " \ 59 | "attachments API provided by Mail library.") 60 | 61 | ::Postmark::MessageHelper.attachments_to_postmark(@_attachments) 62 | end 63 | 64 | def template_alias(val = nil) 65 | return self[:postmark_template_alias] && self[:postmark_template_alias].to_s if val.nil? 66 | self[:postmark_template_alias] = val 67 | end 68 | 69 | attr_writer :template_model 70 | def template_model(model = nil) 71 | return @template_model if model.nil? 72 | @template_model = model 73 | end 74 | 75 | def message_stream(val = nil) 76 | self.message_stream = val unless val.nil? 77 | header['MESSAGE-STREAM'].to_s 78 | end 79 | 80 | def message_stream=(val) 81 | header['MESSAGE-STREAM'] = val 82 | end 83 | 84 | def templated? 85 | !!template_alias 86 | end 87 | 88 | def prerender 89 | raise ::Postmark::Error, 'Cannot prerender a message without an associated template alias' unless templated? 90 | 91 | unless delivery_method.is_a?(::Mail::Postmark) 92 | raise ::Postmark::MailAdapterError, "Cannot render templates via #{delivery_method.class} adapter." 93 | end 94 | 95 | client = delivery_method.api_client 96 | template = client.get_template(template_alias) 97 | response = client.validate_template(template.merge(:test_render_model => template_model || {})) 98 | 99 | raise ::Postmark::InvalidTemplateError, response unless response[:all_content_is_valid] 100 | 101 | self.body = nil 102 | 103 | subject response[:subject][:rendered_content] 104 | 105 | text_part do 106 | body response[:text_body][:rendered_content] 107 | end 108 | 109 | html_part do 110 | content_type 'text/html; charset=UTF-8' 111 | body response[:html_body][:rendered_content] 112 | end 113 | 114 | self 115 | end 116 | 117 | def text? 118 | if defined?(super) 119 | super 120 | else 121 | has_content_type? ? !!(main_type =~ /^text$/i) : false 122 | end 123 | end 124 | 125 | def html? 126 | text? && !!(sub_type =~ /^html$/i) 127 | end 128 | 129 | def body_html 130 | if multipart? && html_part 131 | html_part.decoded 132 | elsif html? 133 | decoded 134 | end 135 | end 136 | 137 | def body_text 138 | if multipart? && text_part 139 | text_part.decoded 140 | elsif text? && !html? 141 | decoded 142 | elsif !html? 143 | body.decoded 144 | end 145 | end 146 | 147 | def export_attachments 148 | export_native_attachments + postmark_attachments 149 | end 150 | 151 | def export_headers 152 | [].tap do |headers| 153 | self.header.fields.each do |field| 154 | key, value = field.name, field.value 155 | next if reserved_headers.include? key.downcase 156 | headers << { "Name" => key, "Value" => value } 157 | end 158 | end 159 | end 160 | 161 | def to_postmark_hash 162 | ready_to_send! 163 | ::Postmark::MailMessageConverter.new(self).run 164 | end 165 | 166 | protected 167 | 168 | def pack_attachment_data(data) 169 | ::Postmark::MessageHelper.encode_in_base64(data) 170 | end 171 | 172 | def export_native_attachments 173 | attachments.map do |attachment| 174 | basics = {"Name" => attachment.filename, 175 | "Content" => pack_attachment_data(attachment.body.decoded), 176 | "ContentType" => attachment.mime_type} 177 | specials = attachment.inline? ? {'ContentID' => attachment.url} : {} 178 | 179 | basics.update(specials) 180 | end 181 | end 182 | 183 | def reserved_headers 184 | %q[ 185 | return-path x-pm-rcpt 186 | from reply-to 187 | sender received 188 | date content-type 189 | cc bcc 190 | subject tag 191 | attachment to 192 | track-opens track-links 193 | postmark-template-alias 194 | message-stream 195 | ] 196 | end 197 | 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /spec/unit/postmark_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark do 4 | let(:api_token) { double } 5 | let(:secure) { double } 6 | let(:proxy_host) { double } 7 | let(:proxy_port) { double } 8 | let(:proxy_user) { double } 9 | let(:proxy_pass) { double } 10 | let(:host) { double } 11 | let(:port) { double } 12 | let(:path_prefix) { double } 13 | let(:max_retries) { double } 14 | 15 | before do 16 | subject.api_token = api_token 17 | subject.secure = secure 18 | subject.proxy_host = proxy_host 19 | subject.proxy_port = proxy_port 20 | subject.proxy_user = proxy_user 21 | subject.proxy_pass = proxy_pass 22 | subject.host = host 23 | subject.port = port 24 | subject.path_prefix = path_prefix 25 | subject.max_retries = max_retries 26 | end 27 | 28 | context "attr readers" do 29 | it { expect(subject).to respond_to(:secure) } 30 | it { expect(subject).to respond_to(:api_key) } 31 | it { expect(subject).to respond_to(:api_token) } 32 | it { expect(subject).to respond_to(:proxy_host) } 33 | it { expect(subject).to respond_to(:proxy_port) } 34 | it { expect(subject).to respond_to(:proxy_user) } 35 | it { expect(subject).to respond_to(:proxy_pass) } 36 | it { expect(subject).to respond_to(:host) } 37 | it { expect(subject).to respond_to(:port) } 38 | it { expect(subject).to respond_to(:path_prefix) } 39 | it { expect(subject).to respond_to(:http_open_timeout) } 40 | it { expect(subject).to respond_to(:http_read_timeout) } 41 | it { expect(subject).to respond_to(:max_retries) } 42 | end 43 | 44 | context "attr writers" do 45 | it { expect(subject).to respond_to(:secure=) } 46 | it { expect(subject).to respond_to(:api_key=) } 47 | it { expect(subject).to respond_to(:api_token=) } 48 | it { expect(subject).to respond_to(:proxy_host=) } 49 | it { expect(subject).to respond_to(:proxy_port=) } 50 | it { expect(subject).to respond_to(:proxy_user=) } 51 | it { expect(subject).to respond_to(:proxy_pass=) } 52 | it { expect(subject).to respond_to(:host=) } 53 | it { expect(subject).to respond_to(:port=) } 54 | it { expect(subject).to respond_to(:path_prefix=) } 55 | it { expect(subject).to respond_to(:http_open_timeout=) } 56 | it { expect(subject).to respond_to(:http_read_timeout=) } 57 | it { expect(subject).to respond_to(:max_retries=) } 58 | it { expect(subject).to respond_to(:response_parser_class=) } 59 | it { expect(subject).to respond_to(:api_client=) } 60 | end 61 | 62 | describe ".response_parser_class" do 63 | 64 | after do 65 | subject.instance_variable_set(:@response_parser_class, nil) 66 | end 67 | 68 | it "returns :ActiveSupport when ActiveSupport::JSON is available" do 69 | expect(subject.response_parser_class).to eq :ActiveSupport 70 | end 71 | 72 | it "returns :Json when ActiveSupport::JSON is not available" do 73 | hide_const("ActiveSupport::JSON") 74 | expect(subject.response_parser_class).to eq :Json 75 | end 76 | 77 | end 78 | 79 | describe ".configure" do 80 | 81 | it 'yields itself to the block' do 82 | expect { |b| subject.configure(&b) }.to yield_with_args(subject) 83 | end 84 | 85 | end 86 | 87 | describe ".api_client" do 88 | let(:api_client) { double } 89 | 90 | context "when shared client instance already exists" do 91 | 92 | it 'returns the existing instance' do 93 | subject.instance_variable_set(:@api_client, api_client) 94 | expect(subject.api_client).to eq api_client 95 | end 96 | 97 | end 98 | 99 | context "when shared client instance does not exist" do 100 | 101 | it 'creates a new instance of Postmark::ApiClient' do 102 | allow(Postmark::ApiClient).to receive(:new). 103 | with(api_token, 104 | :secure => secure, 105 | :proxy_host => proxy_host, 106 | :proxy_port => proxy_port, 107 | :proxy_user => proxy_user, 108 | :proxy_pass => proxy_pass, 109 | :host => host, 110 | :port => port, 111 | :path_prefix => path_prefix, 112 | :max_retries => max_retries). 113 | and_return(api_client) 114 | expect(subject.api_client).to eq api_client 115 | end 116 | 117 | end 118 | 119 | end 120 | 121 | describe ".deliver_message" do 122 | let(:api_client) { double } 123 | let(:message) { double } 124 | 125 | before do 126 | subject.api_client = api_client 127 | end 128 | 129 | it 'delegates the method to the shared api client instance' do 130 | allow(api_client).to receive(:deliver_message).with(message) 131 | subject.deliver_message(message) 132 | end 133 | 134 | it 'is also accessible as .send_through_postmark' do 135 | allow(api_client).to receive(:deliver_message).with(message) 136 | subject.send_through_postmark(message) 137 | end 138 | end 139 | 140 | describe ".deliver_messages" do 141 | let(:api_client) { double } 142 | let(:message) { double } 143 | 144 | before do 145 | subject.api_client = api_client 146 | end 147 | 148 | it 'delegates the method to the shared api client instance' do 149 | allow(api_client).to receive(:deliver_messages).with(message) 150 | subject.deliver_messages(message) 151 | end 152 | end 153 | 154 | describe ".delivery_stats" do 155 | let(:api_client) { double } 156 | 157 | before do 158 | subject.api_client = api_client 159 | end 160 | 161 | it 'delegates the method to the shared api client instance' do 162 | allow(api_client).to receive(:delivery_stats) 163 | subject.delivery_stats 164 | end 165 | end 166 | end -------------------------------------------------------------------------------- /spec/unit/postmark/helpers/message_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::MessageHelper do 4 | let(:attachments) { 5 | [ 6 | File.open(empty_gif_path), 7 | {:name => "img2.gif", 8 | :content => Postmark::MessageHelper.encode_in_base64(File.read(empty_gif_path)), 9 | :content_type => "application/octet-stream"} 10 | ] 11 | } 12 | 13 | let(:postmark_attachments) { 14 | content = Postmark::MessageHelper.encode_in_base64(File.read(empty_gif_path)) 15 | [ 16 | {"Name" => "empty.gif", 17 | "Content" => content, 18 | "ContentType" => "application/octet-stream"}, 19 | {"Name" => "img2.gif", 20 | "Content" => content, 21 | "ContentType" => "application/octet-stream"} 22 | ] 23 | } 24 | 25 | let(:headers) { 26 | [{:name => "CUSTOM-HEADER", :value => "value"}] 27 | } 28 | 29 | let(:postmark_headers) { 30 | [{"Name" => "CUSTOM-HEADER", "Value" => "value"}] 31 | } 32 | 33 | 34 | describe ".to_postmark" do 35 | let(:message) { 36 | { 37 | :from => "sender@example.com", 38 | :to => "receiver@example.com", 39 | :cc => "copied@example.com", 40 | :bcc => "blank-copied@example.com", 41 | :subject => "Test", 42 | :tag => "Invitation", 43 | :html_body => "Hello", 44 | :text_body => "Hello", 45 | :reply_to => "reply@example.com" 46 | } 47 | } 48 | 49 | let(:postmark_message) { 50 | { 51 | "From" => "sender@example.com", 52 | "To" => "receiver@example.com", 53 | "Cc" => "copied@example.com", 54 | "Bcc"=> "blank-copied@example.com", 55 | "Subject" => "Test", 56 | "Tag" => "Invitation", 57 | "HtmlBody" => "Hello", 58 | "TextBody" => "Hello", 59 | "ReplyTo" => "reply@example.com", 60 | } 61 | } 62 | 63 | let(:message_with_headers) { 64 | message.merge(:headers => headers) 65 | } 66 | 67 | let(:postmark_message_with_headers) { 68 | postmark_message.merge("Headers" => postmark_headers) 69 | } 70 | 71 | let(:message_with_headers_and_attachments) { 72 | message_with_headers.merge(:attachments => attachments) 73 | } 74 | 75 | let(:postmark_message_with_headers_and_attachments) { 76 | postmark_message_with_headers.merge("Attachments" => postmark_attachments) 77 | } 78 | 79 | let(:message_with_open_tracking) { 80 | message.merge(:track_opens => true) 81 | } 82 | 83 | let(:message_with_open_tracking_false) { 84 | message.merge(:track_opens => false) 85 | } 86 | 87 | let(:postmark_message_with_open_tracking) { 88 | postmark_message.merge("TrackOpens" => true) 89 | } 90 | 91 | let(:postmark_message_with_open_tracking_false) { 92 | postmark_message.merge("TrackOpens" => false) 93 | } 94 | 95 | it 'converts messages without custom headers and attachments correctly' do 96 | expect(subject.to_postmark(message)).to eq postmark_message 97 | end 98 | 99 | it 'converts messages with custom headers and without attachments correctly' do 100 | expect(subject.to_postmark(message_with_headers)).to eq postmark_message_with_headers 101 | end 102 | 103 | it 'converts messages with custom headers and attachments correctly' do 104 | expect(subject.to_postmark(message_with_headers_and_attachments)).to eq postmark_message_with_headers_and_attachments 105 | end 106 | 107 | context 'open tracking' do 108 | 109 | it 'converts messages with open tracking flag set to true correctly' do 110 | expect(subject.to_postmark(message_with_open_tracking)).to eq(postmark_message_with_open_tracking) 111 | end 112 | 113 | it 'converts messages with open tracking flag set to false correctly' do 114 | expect(subject.to_postmark(message_with_open_tracking_false)).to eq(postmark_message_with_open_tracking_false) 115 | end 116 | 117 | end 118 | 119 | context 'metadata' do 120 | it 'converts messages with metadata correctly' do 121 | metadata = {"test" => "value"} 122 | data= message.merge(:metadata => metadata) 123 | expect(subject.to_postmark(data)).to include(postmark_message.merge("Metadata" => metadata)) 124 | end 125 | end 126 | 127 | context 'link tracking' do 128 | let(:message_with_link_tracking_html) { message.merge(:track_links => :html_only) } 129 | let(:message_with_link_tracking_text) { message.merge(:track_links => :text_only) } 130 | let(:message_with_link_tracking_all) { message.merge(:track_links => :html_and_text) } 131 | let(:message_with_link_tracking_none) { message.merge(:track_links => :none) } 132 | 133 | let(:postmark_message_with_link_tracking_html) { postmark_message.merge("TrackLinks" => 'HtmlOnly') } 134 | let(:postmark_message_with_link_tracking_text) { postmark_message.merge("TrackLinks" => 'TextOnly') } 135 | let(:postmark_message_with_link_tracking_all) { postmark_message.merge("TrackLinks" => 'HtmlAndText') } 136 | let(:postmark_message_with_link_tracking_none) { postmark_message.merge("TrackLinks" => 'None') } 137 | 138 | it 'converts html body link tracking to Postmark format' do 139 | expect(subject.to_postmark(message_with_link_tracking_html)).to eq(postmark_message_with_link_tracking_html) 140 | end 141 | 142 | it 'converts text body link tracking to Postmark format' do 143 | expect(subject.to_postmark(message_with_link_tracking_text)).to eq(postmark_message_with_link_tracking_text) 144 | end 145 | 146 | it 'converts html and text body link tracking to Postmark format' do 147 | expect(subject.to_postmark(message_with_link_tracking_all)).to eq(postmark_message_with_link_tracking_all) 148 | end 149 | 150 | it 'converts no link tracking to Postmark format' do 151 | expect(subject.to_postmark(message_with_link_tracking_none)).to eq(postmark_message_with_link_tracking_none) 152 | end 153 | end 154 | end 155 | 156 | describe ".headers_to_postmark" do 157 | it 'converts headers to Postmark format' do 158 | expect(subject.headers_to_postmark(headers)).to eq postmark_headers 159 | end 160 | 161 | it 'accepts single header as a non-array' do 162 | expect(subject.headers_to_postmark(headers.first)).to eq [postmark_headers.first] 163 | end 164 | end 165 | 166 | describe ".attachments_to_postmark" do 167 | it 'converts attachments to Postmark format' do 168 | expect(subject.attachments_to_postmark(attachments)).to eq postmark_attachments 169 | end 170 | 171 | it 'accepts single attachment as a non-array' do 172 | expect(subject.attachments_to_postmark(attachments.first)).to eq [postmark_attachments.first] 173 | end 174 | end 175 | 176 | end -------------------------------------------------------------------------------- /spec/unit/postmark/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe(Postmark::Error) do 4 | it {is_expected.to be_a(StandardError)} 5 | end 6 | 7 | describe(Postmark::HttpClientError) do 8 | it {is_expected.to be_a(Postmark::Error)} 9 | it {expect(subject.retry?).to be true} 10 | end 11 | 12 | describe(Postmark::HttpServerError) do 13 | it {is_expected.to be_a(Postmark::Error)} 14 | 15 | describe '.build' do 16 | context 'picks an appropriate subclass for code' do 17 | subject {Postmark::HttpServerError.build(code, Postmark::Json.encode({}))} 18 | 19 | context '401' do 20 | let(:code) {'401'} 21 | 22 | it {is_expected.to be_a(Postmark::InvalidApiKeyError)} 23 | its(:status_code) {is_expected.to eq 401} 24 | end 25 | 26 | context '422' do 27 | let(:code) {'422'} 28 | 29 | it {is_expected.to be_a(Postmark::ApiInputError)} 30 | its(:status_code) {is_expected.to eq 422} 31 | end 32 | 33 | context '500' do 34 | let(:code) {'500'} 35 | 36 | it {is_expected.to be_a(Postmark::InternalServerError)} 37 | its(:status_code) {is_expected.to eq 500} 38 | end 39 | 40 | context 'others' do 41 | let(:code) {'999'} 42 | 43 | it {is_expected.to be_a(Postmark::UnexpectedHttpResponseError)} 44 | its(:status_code) {is_expected.to eq code.to_i} 45 | end 46 | end 47 | end 48 | 49 | describe '#retry?' do 50 | it 'is true for 5XX status codes' do 51 | (500...600).each do |code| 52 | expect(Postmark::HttpServerError.new(code).retry?).to be true 53 | end 54 | end 55 | 56 | it 'is false for other codes except 5XX' do 57 | [200, 300, 400].each do |code| 58 | expect(Postmark::HttpServerError.new(code).retry?).to be false 59 | end 60 | end 61 | end 62 | 63 | describe '#message ' do 64 | it 'uses "Message" field on postmark response if available' do 65 | data = {'Message' => 'Postmark error message'} 66 | error = Postmark::HttpServerError.new(502, Postmark::Json.encode(data), data) 67 | expect(error.message).to eq data['Message'] 68 | end 69 | 70 | it 'falls back to a message generated from status code' do 71 | error = Postmark::HttpServerError.new(502, '') 72 | expect(error.message).to match(/The Postmark API responded with HTTP status \d+/) 73 | end 74 | end 75 | end 76 | 77 | describe(Postmark::ApiInputError) do 78 | describe '.build' do 79 | context 'picks an appropriate subclass for error code' do 80 | let(:response) {{'ErrorCode' => code}} 81 | 82 | subject do 83 | Postmark::ApiInputError.build(Postmark::Json.encode(response), response) 84 | end 85 | 86 | shared_examples_for 'api input error' do 87 | its(:status_code) {is_expected.to eq 422} 88 | it {expect(subject.retry?).to be false} 89 | it {is_expected.to be_a(Postmark::ApiInputError)} 90 | it {is_expected.to be_a(Postmark::HttpServerError)} 91 | end 92 | 93 | context '406' do 94 | let(:code) {Postmark::ApiInputError::INACTIVE_RECIPIENT} 95 | 96 | it {is_expected.to be_a(Postmark::InactiveRecipientError)} 97 | it_behaves_like 'api input error' 98 | end 99 | 100 | context '300' do 101 | let(:code) {Postmark::ApiInputError::INVALID_EMAIL_ADDRESS} 102 | 103 | it {is_expected.to be_a(Postmark::InvalidEmailAddressError)} 104 | it_behaves_like 'api input error' 105 | end 106 | 107 | context 'others' do 108 | let(:code) {'9999'} 109 | 110 | it_behaves_like 'api input error' 111 | end 112 | end 113 | end 114 | end 115 | 116 | describe Postmark::InvalidTemplateError do 117 | subject(:error) {Postmark::InvalidTemplateError.new(:foo => 'bar')} 118 | 119 | it 'is created with a response' do 120 | expect(error.message).to start_with('Failed to render the template.') 121 | expect(error.postmark_response).to eq(:foo => 'bar') 122 | end 123 | end 124 | 125 | describe(Postmark::TimeoutError) do 126 | it {is_expected.to be_a(Postmark::Error)} 127 | it {expect(subject.retry?).to be true} 128 | end 129 | 130 | describe(Postmark::UnknownMessageType) do 131 | it 'exists for backward compatibility' do 132 | is_expected.to be_a(Postmark::Error) 133 | end 134 | end 135 | 136 | describe(Postmark::InvalidApiKeyError) do 137 | it {is_expected.to be_a(Postmark::Error)} 138 | end 139 | 140 | describe(Postmark::InternalServerError) do 141 | it {is_expected.to be_a(Postmark::Error)} 142 | end 143 | 144 | describe(Postmark::UnexpectedHttpResponseError) do 145 | it {is_expected.to be_a(Postmark::Error)} 146 | end 147 | 148 | describe(Postmark::MailAdapterError) do 149 | it {is_expected.to be_a(Postmark::Error)} 150 | end 151 | 152 | describe(Postmark::InvalidEmailAddressError) do 153 | describe '.new' do 154 | let(:response) {{'Message' => message}} 155 | 156 | subject do 157 | Postmark::InvalidEmailAddressError.new( 158 | Postmark::ApiInputError::INVALID_EMAIL_ADDRESS, Postmark::Json.encode(response), response) 159 | end 160 | 161 | let(:message) do 162 | "Error parsing 'To': Illegal email address 'johne.xample.com'. It must contain the '@' symbol." 163 | end 164 | 165 | it 'body is set' do 166 | expect(subject.body).to eq(Postmark::Json.encode(response)) 167 | end 168 | 169 | it 'parsed body is set' do 170 | expect(subject.parsed_body).to eq(response) 171 | end 172 | 173 | it 'error code is set' do 174 | expect(subject.error_code).to eq(Postmark::ApiInputError::INVALID_EMAIL_ADDRESS) 175 | end 176 | end 177 | end 178 | 179 | describe(Postmark::InactiveRecipientError) do 180 | describe '.parse_recipients' do 181 | let(:recipients) do 182 | %w(nothing@postmarkapp.com noth.ing+2@postmarkapp.com noth.ing+2-1@postmarkapp.com) 183 | end 184 | 185 | subject {Postmark::InactiveRecipientError.parse_recipients(message)} 186 | 187 | context '1/1 inactive' do 188 | let(:message) do 189 | 'You tried to send to a recipient that has been marked as ' \ 190 | "inactive.\nFound inactive addresses: #{recipients[0]}.\n" \ 191 | 'Inactive recipients are ones that have generated a hard ' \ 192 | 'bounce or a spam complaint.' 193 | end 194 | 195 | it {is_expected.to eq(recipients.take(1))} 196 | end 197 | 198 | context 'i/n inactive, n > 1, i < n - new message format' do 199 | let(:message) { "Message OK, but will not deliver to these inactive addresses: #{recipients[0...2].join(', ')}" } 200 | 201 | it {is_expected.to eq(recipients.take(2))} 202 | end 203 | 204 | context 'i/n inactive, n > 1, i < n' do 205 | let(:message) do 206 | 'Message OK, but will not deliver to these inactive addresses: ' \ 207 | "#{recipients[0...2].join(', ')}. Inactive recipients are ones that " \ 208 | 'have generated a hard bounce or a spam complaint.' 209 | end 210 | 211 | it {is_expected.to eq(recipients.take(2))} 212 | end 213 | 214 | context 'n/n inactive, n > 1' do 215 | let(:message) do 216 | 'You tried to send to recipients that have all been marked as ' \ 217 | "inactive.\nFound inactive addresses: #{recipients.join(', ')}.\n" \ 218 | 'Inactive recipients are ones that have generated a hard bounce or a spam complaint.' 219 | end 220 | 221 | it {is_expected.to eq(recipients)} 222 | end 223 | 224 | context 'unknown error format' do 225 | let(:message) {recipients.join(', ')} 226 | 227 | it {is_expected.to eq([])} 228 | end 229 | end 230 | 231 | describe '.new' do 232 | let(:address) {'user@example.org'} 233 | let(:response) {{'Message' => message}} 234 | 235 | subject do 236 | Postmark::InactiveRecipientError.new( 237 | Postmark::ApiInputError::INACTIVE_RECIPIENT, 238 | Postmark::Json.encode(response), 239 | response) 240 | end 241 | 242 | let(:message) do 243 | 'You tried to send to a recipient that has been marked as ' \ 244 | "inactive.\nFound inactive addresses: #{address}.\n" \ 245 | 'Inactive recipients are ones that have generated a hard ' \ 246 | 'bounce or a spam complaint.' 247 | end 248 | 249 | it 'parses recipients from json payload' do 250 | expect(subject.recipients).to eq([address]) 251 | end 252 | 253 | it 'body is set' do 254 | expect(subject.body).to eq(Postmark::Json.encode(response)) 255 | end 256 | 257 | it 'parsed body is set' do 258 | expect(subject.parsed_body).to eq(response) 259 | end 260 | 261 | it 'error code is set' do 262 | expect(subject.error_code).to eq(Postmark::ApiInputError::INACTIVE_RECIPIENT) 263 | end 264 | end 265 | end 266 | 267 | describe(Postmark::DeliveryError) do 268 | it 'is an alias to Error for backwards compatibility' do 269 | expect(subject.class).to eq(Postmark::Error) 270 | end 271 | end 272 | 273 | describe(Postmark::InvalidMessageError) do 274 | it 'is an alias to Error for backwards compatibility' do 275 | expect(subject.class).to eq(Postmark::ApiInputError) 276 | end 277 | end 278 | 279 | describe(Postmark::UnknownError) do 280 | it 'is an alias for backwards compatibility' do 281 | expect(subject.class).to eq(Postmark::UnexpectedHttpResponseError) 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | 3 | == 1.22.1 4 | * Migrated to ActiveCampaign 5 | 6 | == 1.22.0 7 | 8 | * Disabled automatic retries of failed requests by default. You can enabled it by passing `max_retries` option to the client constructor. 9 | 10 | == 1.21.8 11 | 12 | * Fixed passing and receiving SubscriptionManagementConfiguration when creating/updating message streams (#94). 13 | 14 | == 1.21.7 15 | 16 | * Improved parsing recipients with Postmark::InactiveRecipientError.parse_recipients method 17 | 18 | == 1.21.6 19 | 20 | * Improved error handling for email sending related to invalid email addresses 21 | 22 | == 1.21.5 23 | 24 | * Added support for archiving/unarchiving message streams 25 | 26 | == 1.21.4 27 | 28 | * Fixed Postmark::ApiClient#deliver_messages_with_templates (#104) 29 | 30 | == 1.21.3 31 | 32 | * Remove default SSL version setting and rely on Net::HTTP/OpenSSL default. 33 | 34 | == 1.21.2 35 | 36 | * Ensure sending via message stream uses the correct message stream 37 | 38 | == 1.21.1 39 | 40 | * Fixed Postmark::ApiClient#get_message_streams 41 | 42 | == 1.21.0 43 | 44 | * Added support for message streams and suppressions 45 | 46 | == 1.20.0 47 | 48 | * Removed deprecated trigger endpoints 49 | 50 | == 1.19.2 51 | 52 | Allow possibility to change TLS version for HTTP client. 53 | 54 | == 1.19.1 55 | 56 | Bounce tags endoint removed, since it's no longer supported by API. 57 | 58 | == 1.19.0 59 | 60 | Webhooks management support is added. 61 | 62 | == 1.18.0 63 | 64 | Custom headers with any type of character casing is supported now. 65 | 66 | == 1.17.0 67 | 68 | * Update sent email message properly and not altering it's Message-ID with Postmark unique message id. 69 | 70 | == 1.16.0 71 | 72 | * Added support for template pushes. 73 | 74 | == 1.15.0 75 | 76 | * Extended Mail::Message objects with support for Postmark templates. 77 | * Added ApiClient#deliver_message_with_template and ApiClient#deliver_messages_with_templates 78 | * Removed Rake from dependencies. 79 | 80 | == 1.14.0 81 | 82 | * Added support for verifying DKIM/Return-Path. 83 | * Added support for searching inbound rules. 84 | * Updated README. 85 | 86 | == 1.13.0 87 | 88 | * Changed default value returned by Mail::Message#metadata to a mutable hash (makes things easier for postmark-rails). 89 | * All message JSON payloads now include an empty metadata object even if metadata is unset. 90 | 91 | == 1.12.0 92 | 93 | * Added support for attaching metadata to messages. 94 | 95 | == 1.11.0 96 | 97 | * New, improved, and backwards-compatible gem errors (see README). 98 | * Added support for retrieving message clicks using the Messages API. 99 | * Added support for sending templated message in batches. 100 | * Added support for assigning link tracking mode via `Mail::Message` headers. 101 | 102 | == 1.10.0 103 | 104 | * Fix a bug when open tracking flag is set to false by default, when open tracking flag is not set by a user. 105 | * Added support for link tracking 106 | 107 | == 1.9.1 108 | 109 | * Fix a bug when port setting is not respected. 110 | * Made `Postmark::HttpClient#protocol` method public. 111 | 112 | == 1.9.0 113 | 114 | * Added methods to access domains API endoints. 115 | 116 | == 1.8.1 117 | 118 | * Technical release. Fixed gemspec. 119 | 120 | == 1.8.0 121 | 122 | * Added missing `description` attribute to `Postmark::Bounce` #50. 123 | * Restricted `rake` dependency to `< 11.0.0` for Ruby < 1.9 via gemspec. 124 | * Restricted `json` dependency to `< 2.0.0` for Ruby < 2.0 via gemspec. 125 | 126 | == 1.7.1 127 | 128 | * Explicitly set TLS version used by the client. 129 | 130 | == 1.7.0 131 | 132 | * Add methods to access stats API endpoints. 133 | 134 | == 1.6.0 135 | 136 | * Add methods to access new templates API endpoints. 137 | 138 | == 1.5.0 139 | 140 | * Call API access strings tokens instead of keys. Keep backwards compatibility. 141 | 142 | == 1.4.3 143 | 144 | * Fix a regression when using the gem with older mail gem versions not implementing Mail::Message#text?. 145 | 146 | == 1.4.2 147 | 148 | * Fix a regression when using the gem with older mail gem versions introduced in 1.4.1. Affected mail gem versions are 2.5.3 and below. 149 | 150 | == 1.4.1 151 | 152 | * Fix an exception when sending a Mail::Message containing quoted-printable parts with unicode characters. 153 | 154 | == 1.4.0 155 | 156 | * Add descriptive User-Agent string. 157 | * Enable secure HTTP connections by default. 158 | 159 | == 1.3.1 160 | 161 | * Allow track_open header to be String for compatibility with older versions of the mail gem. 162 | 163 | == 1.3.0 164 | 165 | * Add support for TrackOpens flag of the Delivery API. 166 | * Add support for the Opens API. 167 | * Add support for the Triggers API. 168 | 169 | == 1.2.1 170 | 171 | * Fixed a bug in Postmark::ApiClient causing #get_bounces to return unexpected value. 172 | 173 | == 1.2.0 174 | 175 | * Added support for the Postmark Account API. 176 | * Added #bounces and #messages methods to Postmark::ApiClient returning Ruby enumerators. 177 | 178 | == 1.1.2 179 | 180 | * Fixed HTTP verb used to update server info from POST to PUT to support the breaking change in the API. 181 | 182 | == 1.1.1 183 | 184 | * Fixed inbound support for the Postmark Messages API. 185 | 186 | == 1.1.0 187 | 188 | * Added support for inline attachments when using the Mail gem. 189 | * Added support for the Postmark Messages API. 190 | 191 | == 1.0.2 192 | 193 | * Removed metaprogramming executed at runtime. [#37] 194 | * Fixed invalid check for a blank recipient. [#38] 195 | 196 | == 1.0.1 197 | 198 | * Fixed an issue causing recipient names to disappear from "To", "Cc" and "Reply-To" headers when using with Mail library. 199 | 200 | == 1.0.0 201 | 202 | * Introduced new instance-based architecture (see README for more details). 203 | * Removed TMail support. 204 | * Added support for sending emails in batches. 205 | * Added API to send emails without Mail library. 206 | * Introduced lock-free approach for Mail::Postmark delivery method. 207 | * Deprecated the Mail::Message#postmark_attachments method 208 | * Added Postmark::Inbound module. 209 | * Added integration tests. 210 | * Added support for the "server" endpoint of the Postmark API. 211 | * Improved unit test coverage. 212 | * Added more examples to the README file. 213 | * Added official JRuby support. 214 | * Fixed the inconsistent behaviour of Mail::Message#tag method added by the gem. 215 | * Added Mail::Message#delivered property and Mail::Message#delivered? predicate. 216 | * Added Mail::Message#postmark_response method. 217 | * Removed Postmark::AttachmentsFixForMail class (that hack no longer works). 218 | * Added Travis-CI for integration tests. 219 | 220 | == 0.9.19 221 | 222 | * Added support for native attachments API provided by Ruby Mail library. 223 | 224 | == 0.9.18 225 | 226 | * Fixed regression introduced by removing ActiveSupport#wrap in case when a Hash instance is passed. 227 | * Fixed broken Ruby 1.8.7 support (uninitialized constant Postmark::HttpClient::Mutex (NameError)). 228 | * Added unit tests for attachments handling. 229 | * Removed unneeded debug output from shared RSpec examples. 230 | 231 | == 0.9.17 232 | 233 | * Removed date from gemspec. 234 | * Removed unneeded debug output when sending attachments. 235 | 236 | == 0.9.16 237 | 238 | * Thread-safe HTTP requests. 239 | * Fixed inproper method of ActiveSupport::JSON detection. 240 | * Removed unexpected ActiveSupport dependency from Postmark::SharedMessageExtensions#postmark_attachments= method. 241 | * Used Markdown to format README. 242 | * Updated README. 243 | 244 | == 0.9.15 245 | 246 | * Save a received MessageID in message headers. 247 | 248 | == 0.9.14 249 | 250 | * Parse Subject and MessageID from the Bounce API response. 251 | 252 | == 0.9.13 253 | 254 | * Added error_code to DeliveryError. 255 | * Added retries for Timeout::Error. 256 | 257 | == 0.9.12 258 | 259 | * Fixed a problem of attachments processing when using deliver! method on Mail object. 260 | * Removed activesupport dependency for Postmark::AttachmentsFixForMail. 261 | * Added specs for AttachmentFixForMail. 262 | 263 | == 0.9.11 264 | 265 | * Replaced Jeweler by Bundler. 266 | * Updated RSpec to 2.8. 267 | * Fixed specs. 268 | * Refactored the codebase. 269 | 270 | == 0.9.10 271 | 272 | * Fixed Ruby 1.9 compatibility issue. 273 | 274 | == 0.9.9 275 | 276 | * Added support for non-array reply_to addresses. 277 | 278 | == 0.9.8 279 | 280 | * Fixed bug that caused unexpected multiple email deliveries on Ruby 1.9.2/Rails 3.0.7. 281 | 282 | == 0.9.7 283 | 284 | * All delivery exceptions are now childs of Postmark::DeliveryError. Easier to rescue that way. 285 | 286 | == 0.9.6 287 | 288 | * Fixed exception when content-type wasn't explicitly specified. 289 | * Removed tmail from the list of dependencies. 290 | 291 | == 0.9.5 292 | 293 | * Fixed a problem of HTML content detection when using Mail gem. 294 | 295 | == 0.9.4 296 | 297 | * Fixed bug that caused full name to be dropped from From address. 298 | 299 | == 0.9.3 300 | 301 | * Removed all "try" calls from the code. It's not always available and not essential anyway. 302 | 303 | == 0.9.2 304 | 305 | * Fixed "Illegal email address ']'" bug on Ruby 1.9 306 | 307 | == 0.9.1 308 | 309 | * Fixed TypeError when calling Bounce.all. 310 | * Fixed NoMethodError when trying to read bounce info. 311 | 312 | == 0.9.0 313 | 314 | * Added support for attachments. 315 | 316 | == 0.8.0 317 | 318 | * Added support for Rails 3. 319 | -------------------------------------------------------------------------------- /spec/unit/postmark/http_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::HttpClient do 4 | 5 | def response_body(status, message = "") 6 | {"ErrorCode" => status, "Message" => message}.to_json 7 | end 8 | 9 | let(:api_token) { "provided-postmark-api-token" } 10 | let(:http_client) { Postmark::HttpClient.new(api_token) } 11 | subject { http_client } 12 | 13 | context "attr writers" do 14 | it { expect(subject).to respond_to(:api_token=) } 15 | it { expect(subject).to respond_to(:api_key=) } 16 | end 17 | 18 | context "attr readers" do 19 | it { expect(subject).to respond_to(:http) } 20 | it { expect(subject).to respond_to(:secure) } 21 | it { expect(subject).to respond_to(:api_token) } 22 | it { expect(subject).to respond_to(:api_key) } 23 | it { expect(subject).to respond_to(:proxy_host) } 24 | it { expect(subject).to respond_to(:proxy_port) } 25 | it { expect(subject).to respond_to(:proxy_user) } 26 | it { expect(subject).to respond_to(:proxy_pass) } 27 | it { expect(subject).to respond_to(:host) } 28 | it { expect(subject).to respond_to(:port) } 29 | it { expect(subject).to respond_to(:path_prefix) } 30 | it { expect(subject).to respond_to(:http_open_timeout) } 31 | it { expect(subject).to respond_to(:http_read_timeout) } 32 | it { expect(subject).to respond_to(:http_ssl_version) } 33 | end 34 | 35 | context "when it is created without options" do 36 | its(:api_token) { is_expected.to eq api_token } 37 | its(:api_key) { is_expected.to eq api_token } 38 | its(:host) { is_expected.to eq 'api.postmarkapp.com' } 39 | its(:port) { is_expected.to eq 443 } 40 | its(:secure) { is_expected.to be true } 41 | its(:path_prefix) { is_expected.to eq '/' } 42 | its(:http_read_timeout) { is_expected.to eq 15 } 43 | its(:http_open_timeout) { is_expected.to eq 5 } 44 | 45 | it 'does not provide a default which utilizes the Net::HTTP default', :skip_ruby_version => ['1.8.7'] do 46 | http_client = subject.http 47 | expect(http_client.ssl_version).to eq nil 48 | end 49 | end 50 | 51 | context "when it is created with options" do 52 | let(:secure) { true } 53 | let(:proxy_host) { "providedproxyhostname.com" } 54 | let(:proxy_port) { 42 } 55 | let(:proxy_user) { "provided proxy user" } 56 | let(:proxy_pass) { "provided proxy pass" } 57 | let(:host) { "providedhostname.org" } 58 | let(:port) { 4443 } 59 | let(:path_prefix) { "/provided/path/prefix" } 60 | let(:http_open_timeout) { 42 } 61 | let(:http_read_timeout) { 42 } 62 | let(:http_ssl_version) { :TLSv1_2} 63 | 64 | subject { Postmark::HttpClient.new(api_token, 65 | :secure => secure, 66 | :proxy_host => proxy_host, 67 | :proxy_port => proxy_port, 68 | :proxy_user => proxy_user, 69 | :proxy_pass => proxy_pass, 70 | :host => host, 71 | :port => port, 72 | :path_prefix => path_prefix, 73 | :http_open_timeout => http_open_timeout, 74 | :http_read_timeout => http_read_timeout, 75 | :http_ssl_version => http_ssl_version) } 76 | 77 | its(:api_token) { is_expected.to eq api_token } 78 | its(:api_key) { is_expected.to eq api_token } 79 | its(:secure) { is_expected.to eq secure } 80 | its(:proxy_host) { is_expected.to eq proxy_host } 81 | its(:proxy_port) { is_expected.to eq proxy_port } 82 | its(:proxy_user) { is_expected.to eq proxy_user } 83 | its(:proxy_pass) { is_expected.to eq proxy_pass } 84 | its(:host) { is_expected.to eq host } 85 | its(:port) { is_expected.to eq port } 86 | its(:path_prefix) { is_expected.to eq path_prefix } 87 | its(:http_open_timeout) { is_expected.to eq http_open_timeout } 88 | its(:http_read_timeout) { is_expected.to eq http_read_timeout } 89 | its(:http_ssl_version) { is_expected.to eq http_ssl_version } 90 | 91 | it 'uses port 80 for plain HTTP connections' do 92 | expect(Postmark::HttpClient.new(api_token, :secure => false).port).to eq(80) 93 | end 94 | 95 | it 'uses port 443 for secure HTTP connections' do 96 | expect(Postmark::HttpClient.new(api_token, :secure => true).port).to eq(443) 97 | end 98 | 99 | it 'respects port over secure option' do 100 | client = Postmark::HttpClient.new(api_token, :port => 80, :secure => true) 101 | expect(client.port).to eq(80) 102 | expect(client.protocol).to eq('https') 103 | end 104 | end 105 | 106 | describe "#post" do 107 | let(:target_path) { "path/on/server" } 108 | let(:target_url) { "https://api.postmarkapp.com/#{target_path}" } 109 | 110 | it "sends a POST request to provided URI" do 111 | FakeWeb.register_uri(:post, target_url, :body => response_body(200)) 112 | subject.post(target_path) 113 | expect(FakeWeb.last_request.method).to eq('POST') 114 | expect(FakeWeb.last_request.path).to eq('/' + target_path) 115 | end 116 | 117 | it "raises a custom error when API token authorization fails" do 118 | FakeWeb.register_uri(:post, target_url, :body => response_body(401), :status => [ "401", "Unauthorized" ]) 119 | expect { subject.post(target_path) }.to raise_error Postmark::InvalidApiKeyError 120 | end 121 | 122 | it "raises a custom error when sent JSON was not valid" do 123 | FakeWeb.register_uri(:post, target_url, :body => response_body(422), :status => [ "422", "Invalid" ]) 124 | expect { subject.post(target_path) }.to raise_error Postmark::InvalidMessageError 125 | end 126 | 127 | it "raises a custom error when server fails to process the request" do 128 | FakeWeb.register_uri(:post, target_url, :body => response_body(500), 129 | :status => [ "500", "Internal Server Error" ]) 130 | expect { subject.post(target_path) }.to raise_error Postmark::InternalServerError 131 | end 132 | 133 | it "raises a custom error when the request times out" do 134 | expect(subject.http).to receive(:post).at_least(:once).and_raise(Timeout::Error) 135 | expect { subject.post(target_path) }.to raise_error Postmark::TimeoutError 136 | end 137 | 138 | it "raises a default error when unknown issue occurs" do 139 | FakeWeb.register_uri(:post, target_url, :body => response_body(485), 140 | :status => [ "485", "Custom HTTP response status" ]) 141 | expect { subject.post(target_path) }.to raise_error Postmark::UnknownError 142 | end 143 | 144 | end 145 | 146 | describe "#get" do 147 | let(:target_path) { "path/on/server" } 148 | let(:target_url) { "https://api.postmarkapp.com/#{target_path}" } 149 | 150 | it "sends a GET request to provided URI" do 151 | FakeWeb.register_uri(:get, target_url, :body => response_body(200)) 152 | subject.get(target_path) 153 | expect(FakeWeb.last_request.method).to eq('GET') 154 | expect(FakeWeb.last_request.path).to eq('/' + target_path) 155 | end 156 | 157 | it "raises a custom error when API token authorization fails" do 158 | FakeWeb.register_uri(:get, target_url, :body => response_body(401), :status => [ "401", "Unauthorized" ]) 159 | expect { subject.get(target_path) }.to raise_error Postmark::InvalidApiKeyError 160 | end 161 | 162 | it "raises a custom error when sent JSON was not valid" do 163 | FakeWeb.register_uri(:get, target_url, :body => response_body(422), :status => [ "422", "Invalid" ]) 164 | expect { subject.get(target_path) }.to raise_error Postmark::InvalidMessageError 165 | end 166 | 167 | it "raises a custom error when server fails to process the request" do 168 | FakeWeb.register_uri(:get, target_url, :body => response_body(500), 169 | :status => [ "500", "Internal Server Error" ]) 170 | expect { subject.get(target_path) }.to raise_error Postmark::InternalServerError 171 | end 172 | 173 | it "raises a custom error when the request times out" do 174 | expect(subject.http).to receive(:get).at_least(:once).and_raise(Timeout::Error) 175 | expect { subject.get(target_path) }.to raise_error Postmark::TimeoutError 176 | end 177 | 178 | it "raises a default error when unknown issue occurs" do 179 | FakeWeb.register_uri(:get, target_url, :body => response_body(485), 180 | :status => [ "485", "Custom HTTP response status" ]) 181 | expect { subject.get(target_path) }.to raise_error Postmark::UnknownError 182 | end 183 | 184 | end 185 | 186 | describe "#put" do 187 | let(:target_path) { "path/on/server" } 188 | let(:target_url) { "https://api.postmarkapp.com/#{target_path}" } 189 | 190 | it "sends a PUT request to provided URI" do 191 | FakeWeb.register_uri(:put, target_url, :body => response_body(200)) 192 | subject.put(target_path) 193 | expect(FakeWeb.last_request.method).to eq('PUT') 194 | expect(FakeWeb.last_request.path).to eq('/' + target_path) 195 | end 196 | 197 | it "raises a custom error when API token authorization fails" do 198 | FakeWeb.register_uri(:put, target_url, :body => response_body(401), 199 | :status => [ "401", "Unauthorized" ]) 200 | expect { subject.put(target_path) }.to raise_error Postmark::InvalidApiKeyError 201 | end 202 | 203 | it "raises a custom error when sent JSON was not valid" do 204 | FakeWeb.register_uri(:put, target_url, :body => response_body(422), 205 | :status => [ "422", "Invalid" ]) 206 | expect { subject.put(target_path) }.to raise_error Postmark::InvalidMessageError 207 | end 208 | 209 | it "raises a custom error when server fails to process the request" do 210 | FakeWeb.register_uri(:put, target_url, :body => response_body(500), 211 | :status => [ "500", "Internal Server Error" ]) 212 | expect { subject.put(target_path) }.to raise_error Postmark::InternalServerError 213 | end 214 | 215 | it "raises a custom error when the request times out" do 216 | expect(subject.http).to receive(:put).at_least(:once).and_raise(Timeout::Error) 217 | expect { subject.put(target_path) }.to raise_error Postmark::TimeoutError 218 | end 219 | 220 | it "raises a default error when unknown issue occurs" do 221 | FakeWeb.register_uri(:put, target_url, :body => response_body(485), 222 | :status => [ "485", "Custom HTTP response status" ]) 223 | expect { subject.put(target_path) }.to raise_error Postmark::UnknownError 224 | end 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /spec/unit/postmark/message_extensions/mail_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mail::Message do 4 | before do 5 | allow(Kernel).to receive(:warn) 6 | end 7 | 8 | let(:mail_message) do 9 | Mail.new do 10 | from "sheldon@bigbangtheory.com" 11 | to "lenard@bigbangtheory.com" 12 | subject "Hello!" 13 | body "Hello Sheldon!" 14 | end 15 | end 16 | 17 | let(:mail_html_message) do 18 | Mail.new do 19 | from "sheldon@bigbangtheory.com" 20 | to "lenard@bigbangtheory.com" 21 | subject "Hello!" 22 | content_type 'text/html; charset=UTF-8' 23 | body "Hello Sheldon!" 24 | end 25 | end 26 | 27 | let(:templated_message) do 28 | Mail.new do 29 | from "sheldon@bigbangtheory.com" 30 | to "lenard@bigbangtheory.com" 31 | template_alias "Hello!" 32 | template_model :name => "Sheldon" 33 | end 34 | end 35 | 36 | describe '#tag' do 37 | 38 | it 'value set on tag=' do 39 | mail_message.tag='value' 40 | expect(mail_message.tag).to eq 'value' 41 | end 42 | 43 | it 'value set on tag()' do 44 | mail_message.tag('value') 45 | expect(mail_message.tag).to eq 'value' 46 | end 47 | 48 | end 49 | 50 | describe '#track_opens' do 51 | it 'returns nil if unset' do 52 | expect(mail_message.track_opens).to eq '' 53 | end 54 | 55 | context 'when assigned via #track_opens=' do 56 | it 'returns assigned value to track opens' do 57 | mail_message.track_opens = true 58 | expect(mail_message.track_opens).to eq 'true' 59 | end 60 | 61 | it 'returns assigned value to not track opens' do 62 | mail_message.track_opens = false 63 | expect(mail_message.track_opens).to eq 'false' 64 | end 65 | end 66 | 67 | context 'flag set on track_opens()' do 68 | it 'true' do 69 | mail_message.track_opens(true) 70 | expect(mail_message.track_opens).to eq 'true' 71 | end 72 | 73 | it 'false' do 74 | mail_message.track_opens(false) 75 | expect(mail_message.track_opens).to eq 'false' 76 | end 77 | end 78 | end 79 | 80 | describe '#metadata' do 81 | let(:metadata) { { :test => 'test' } } 82 | 83 | it 'returns a mutable empty hash if unset' do 84 | expect(mail_message.metadata).to eq({}) 85 | expect(mail_message.metadata.equal?(mail_message.metadata)).to be true 86 | end 87 | 88 | it 'supports assigning non-null values (for the builder DSL)' do 89 | expect { mail_message.metadata(metadata) }.to change { mail_message.metadata }.to(metadata) 90 | expect { mail_message.metadata(nil) }.to_not change { mail_message.metadata } 91 | end 92 | 93 | it 'returns value assigned via metadata=' do 94 | expect { mail_message.metadata = metadata }.to change { mail_message.metadata }.to(metadata) 95 | end 96 | end 97 | 98 | describe '#track_links' do 99 | it 'return empty string when if unset' do 100 | expect(mail_message.track_links).to eq '' 101 | end 102 | 103 | context 'when assigned via #track_links=' do 104 | it 'returns track html only body value in Postmark format' do 105 | mail_message.track_links=:html_only 106 | expect(mail_message.track_links).to eq 'HtmlOnly' 107 | end 108 | end 109 | 110 | context 'when assigned via track_links()' do 111 | it 'returns track html only body value in Postmark format' do 112 | mail_message.track_links(:html_only) 113 | expect(mail_message.track_links).to eq 'HtmlOnly' 114 | end 115 | end 116 | end 117 | 118 | describe "#html?" do 119 | it 'is true for html only email' do 120 | expect(mail_html_message).to be_html 121 | end 122 | end 123 | 124 | describe "#body_html" do 125 | it 'returns html body if present' do 126 | expect(mail_html_message.body_html).to eq "Hello Sheldon!" 127 | end 128 | end 129 | 130 | describe "#body_text" do 131 | it 'returns text body if present' do 132 | expect(mail_message.body_text).to eq "Hello Sheldon!" 133 | end 134 | end 135 | 136 | describe "#postmark_attachments=" do 137 | let(:attached_hash) { {'Name' => 'picture.jpeg', 138 | 'ContentType' => 'image/jpeg'} } 139 | 140 | it "stores attachments as an array" do 141 | mail_message.postmark_attachments = attached_hash 142 | expect(mail_message.instance_variable_get(:@_attachments)).to include(attached_hash) 143 | end 144 | 145 | it "is deprecated" do 146 | expect(Kernel).to receive(:warn).with(/deprecated/) 147 | mail_message.postmark_attachments = attached_hash 148 | end 149 | end 150 | 151 | describe "#postmark_attachments" do 152 | let(:attached_file) { double("file") } 153 | let(:attached_hash) { {'Name' => 'picture.jpeg', 154 | 'ContentType' => 'image/jpeg'} } 155 | let(:exported_file) { {'Name' => 'file.jpeg', 156 | 'ContentType' => 'application/octet-stream', 157 | 'Content' => ''} } 158 | 159 | before do 160 | allow(attached_file).to receive(:is_a?) { |arg| arg == File ? true : false } 161 | allow(attached_file).to receive(:path) { '/tmp/file.jpeg' } 162 | end 163 | 164 | it "supports multiple attachment formats" do 165 | expect(IO).to receive(:read).with("/tmp/file.jpeg").and_return("") 166 | 167 | mail_message.postmark_attachments = [attached_hash, attached_file] 168 | attachments = mail_message.export_attachments 169 | 170 | expect(attachments).to include(attached_hash) 171 | expect(attachments).to include(exported_file) 172 | end 173 | 174 | it "is deprecated" do 175 | mail_message.postmark_attachments = attached_hash 176 | expect(Kernel).to receive(:warn).with(/deprecated/) 177 | mail_message.postmark_attachments 178 | end 179 | end 180 | 181 | describe "#export_attachments" do 182 | let(:file_data) { 'binarydatahere' } 183 | let(:exported_data) { 184 | {'Name' => 'face.jpeg', 185 | 'Content' => "YmluYXJ5ZGF0YWhlcmU=\n", 186 | 'ContentType' => 'image/jpeg'} 187 | } 188 | 189 | context 'given a regular attachment' do 190 | 191 | it "exports native attachments" do 192 | mail_message.attachments["face.jpeg"] = file_data 193 | expect(mail_message.export_attachments).to include(exported_data) 194 | end 195 | 196 | it "still supports the deprecated attachments API" do 197 | mail_message.attachments["face.jpeg"] = file_data 198 | mail_message.postmark_attachments = exported_data 199 | expect(mail_message.export_attachments).to eq [exported_data, exported_data] 200 | end 201 | 202 | end 203 | 204 | context 'given an inline attachment' do 205 | 206 | it "exports the attachment with related content id" do 207 | mail_message.attachments.inline["face.jpeg"] = file_data 208 | attachments = mail_message.export_attachments 209 | 210 | expect(attachments.count).to_not be_zero 211 | expect(attachments.first).to include(exported_data) 212 | expect(attachments.first).to have_key('ContentID') 213 | expect(attachments.first['ContentID']).to start_with('cid:') 214 | end 215 | 216 | end 217 | 218 | end 219 | 220 | describe "#export_headers" do 221 | let(:mail_message_with_reserved_headers) do 222 | mail_message.header['Return-Path'] = 'bounce@postmarkapp.com' 223 | mail_message.header['From'] = 'info@postmarkapp.com' 224 | mail_message.header['Sender'] = 'info@postmarkapp.com' 225 | mail_message.header['Received'] = 'from mta.pstmrk.it ([72.14.252.155]:54907)' 226 | mail_message.header['Date'] = 'January 25, 2013 3:30:58 PM PDT' 227 | mail_message.header['Content-Type'] = 'application/json' 228 | mail_message.header['To'] = 'lenard@bigbangtheory.com' 229 | mail_message.header['Cc'] = 'sheldon@bigbangtheory.com' 230 | mail_message.header['Bcc'] = 'penny@bigbangtheory.com' 231 | mail_message.header['Subject'] = 'You want not to use a bogus header' 232 | mail_message.header['Tag'] = 'bogus-tag' 233 | mail_message.header['Attachment'] = 'anydatahere' 234 | mail_message.header['Allowed-Header'] = 'value' 235 | mail_message.header['TRACK-OPENS'] = 'true' 236 | mail_message.header['TRACK-LINKS'] = 'HtmlOnly' 237 | mail_message 238 | end 239 | 240 | 241 | it 'only allowed headers' do 242 | headers = mail_message_with_reserved_headers.export_headers 243 | header_names = headers.map { |h| h['Name'] } 244 | 245 | aggregate_failures do 246 | expect(header_names).to include('Allowed-Header') 247 | expect(header_names.count).to eq 1 248 | end 249 | end 250 | 251 | it 'custom header character case preserved' do 252 | custom_header = {"Name"=>"custom-Header", "Value"=>"cUsTomHeaderValue"} 253 | mail_message.header[custom_header['Name']] = custom_header['Value'] 254 | 255 | expect(mail_message.export_headers.first).to match(custom_header) 256 | end 257 | end 258 | 259 | describe "#to_postmark_hash" do 260 | # See mail_message_converter_spec.rb 261 | end 262 | 263 | describe '#templated?' do 264 | it { expect(mail_message).to_not be_templated } 265 | it { expect(templated_message).to be_templated } 266 | end 267 | 268 | describe '#prerender' do 269 | let(:model) { templated_message.template_model } 270 | let(:model_text) { model[:name] } 271 | 272 | let(:template_response) do 273 | { 274 | :html_body => '{{ name }}', 275 | :text_body => '{{ name }}' 276 | } 277 | end 278 | 279 | let(:successful_render_response) do 280 | { 281 | :all_content_is_valid => true, 282 | :subject => { 283 | :rendered_content => 'Subject' 284 | }, 285 | :text_body => { 286 | :rendered_content => model_text 287 | }, 288 | :html_body => { 289 | :rendered_content => "#{model_text}" 290 | } 291 | } 292 | end 293 | 294 | let(:failed_render_response) do 295 | { 296 | :all_content_is_valid => false, 297 | :subject => { 298 | :rendered_content => 'Subject' 299 | }, 300 | :text_body => { 301 | :rendered_content => model_text 302 | }, 303 | :html_body => { 304 | :rendered_content => nil, 305 | :validation_errors => [ 306 | { :message => 'The syntax for this template is invalid.', :line => 1, :character_position => 1 } 307 | ] 308 | } 309 | } 310 | end 311 | 312 | subject(:rendering) { message.prerender } 313 | 314 | context 'when called on a non-templated message' do 315 | let(:message) { mail_message } 316 | 317 | it 'raises a Postmark::Error' do 318 | expect { rendering }.to raise_error(Postmark::Error, /Cannot prerender/) 319 | end 320 | end 321 | 322 | context 'when called on a templated message' do 323 | let(:message) { templated_message } 324 | 325 | before do 326 | message.delivery_method delivery_method 327 | end 328 | 329 | context 'and using a non-Postmark delivery method' do 330 | let(:delivery_method) { Mail::SMTP } 331 | 332 | it { expect { rendering }.to raise_error(Postmark::MailAdapterError) } 333 | end 334 | 335 | context 'and using a Postmark delivery method' do 336 | let(:delivery_method) { Mail::Postmark } 337 | 338 | before do 339 | expect_any_instance_of(Postmark::ApiClient). 340 | to receive(:get_template).with(message.template_alias). 341 | and_return(template_response) 342 | expect_any_instance_of(Postmark::ApiClient). 343 | to receive(:validate_template).with(template_response.merge(:test_render_model => model)). 344 | and_return(render_response) 345 | end 346 | 347 | context 'and rendering succeeds' do 348 | let(:render_response) { successful_render_response } 349 | 350 | it 'sets HTML and Text parts to rendered values' do 351 | expect { rendering }. 352 | to change { message.subject }.to(render_response[:subject][:rendered_content]). 353 | and change { message.body_text }.to(render_response[:text_body][:rendered_content]). 354 | and change { message.body_html }.to(render_response[:html_body][:rendered_content]) 355 | end 356 | end 357 | 358 | context 'and rendering fails' do 359 | let(:render_response) { failed_render_response } 360 | 361 | it 'raises Postmark::InvalidTemplateError' do 362 | expect { rendering }.to raise_error(Postmark::InvalidTemplateError) 363 | end 364 | end 365 | end 366 | end 367 | end 368 | end 369 | -------------------------------------------------------------------------------- /lib/postmark/api_client.rb: -------------------------------------------------------------------------------- 1 | module Postmark 2 | class ApiClient < Client 3 | attr_accessor :max_batch_size 4 | 5 | def initialize(api_token, options = {}) 6 | options = options.dup 7 | @max_batch_size = options.delete(:max_batch_size) || 500 8 | super 9 | end 10 | 11 | def deliver(message_hash = {}) 12 | data = serialize(MessageHelper.to_postmark(message_hash)) 13 | 14 | with_retries do 15 | format_response http_client.post("email", data) 16 | end 17 | end 18 | 19 | def deliver_in_batches(message_hashes) 20 | in_batches(message_hashes) do |batch, offset| 21 | data = serialize(batch.map { |h| MessageHelper.to_postmark(h) }) 22 | 23 | with_retries do 24 | http_client.post("email/batch", data) 25 | end 26 | end 27 | end 28 | 29 | def deliver_message(message) 30 | if message.templated? 31 | raise ArgumentError, 32 | "Please use #{self.class}#deliver_message_with_template to deliver messages with templates." 33 | end 34 | 35 | data = serialize(message.to_postmark_hash) 36 | 37 | with_retries do 38 | response, error = take_response_of { http_client.post("email", data) } 39 | update_message(message, response) 40 | raise error if error 41 | format_response(response, :compatible => true) 42 | end 43 | end 44 | 45 | def deliver_message_with_template(message) 46 | raise ArgumentError, 'Templated delivery requested, but the template is missing.' unless message.templated? 47 | 48 | data = serialize(message.to_postmark_hash) 49 | 50 | with_retries do 51 | response, error = take_response_of { http_client.post("email/withTemplate", data) } 52 | update_message(message, response) 53 | raise error if error 54 | format_response(response, :compatible => true) 55 | end 56 | end 57 | 58 | def deliver_messages(messages) 59 | if messages.any? { |m| m.templated? } 60 | raise ArgumentError, 61 | "Some of the provided messages have templates. Please use " \ 62 | "#{self.class}#deliver_messages_with_templates to deliver those." 63 | end 64 | 65 | in_batches(messages) do |batch, offset| 66 | data = serialize(batch.map { |m| m.to_postmark_hash }) 67 | 68 | with_retries do 69 | http_client.post("email/batch", data).tap do |response| 70 | response.each_with_index do |r, i| 71 | update_message(messages[offset + i], r) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | 78 | def deliver_messages_with_templates(messages) 79 | unless messages.all? { |m| m.templated? } 80 | raise ArgumentError, 'Templated delivery requested, but one or more messages lack templates.' 81 | end 82 | 83 | in_batches(messages) do |batch, offset| 84 | mapped = batch.map { |m| m.to_postmark_hash } 85 | data = serialize(:Messages => mapped) 86 | 87 | with_retries do 88 | http_client.post("email/batchWithTemplates", data).tap do |response| 89 | response.each_with_index do |r, i| 90 | update_message(messages[offset + i], r) 91 | end 92 | end 93 | end 94 | end 95 | end 96 | 97 | def delivery_stats 98 | response = format_response(http_client.get("deliverystats"), :compatible => true) 99 | 100 | if response[:bounces] 101 | response[:bounces] = format_response(response[:bounces]) 102 | end 103 | 104 | response 105 | end 106 | 107 | def messages(options = {}) 108 | path, name, params = extract_messages_path_and_params(options) 109 | find_each(path, name, params) 110 | end 111 | 112 | def get_messages(options = {}) 113 | path, name, params = extract_messages_path_and_params(options) 114 | load_batch(path, name, params).last 115 | end 116 | 117 | def get_messages_count(options = {}) 118 | path, _, params = extract_messages_path_and_params(options) 119 | get_resource_count(path, params) 120 | end 121 | 122 | def get_message(id, options = {}) 123 | get_for_message('details', id, options) 124 | end 125 | 126 | def dump_message(id, options = {}) 127 | get_for_message('dump', id, options) 128 | end 129 | 130 | def bounces(options = {}) 131 | find_each('bounces', 'Bounces', options) 132 | end 133 | 134 | def get_bounces(options = {}) 135 | _, batch = load_batch('bounces', 'Bounces', options) 136 | batch 137 | end 138 | 139 | def get_bounce(id) 140 | format_response http_client.get("bounces/#{id}") 141 | end 142 | 143 | def dump_bounce(id) 144 | format_response http_client.get("bounces/#{id}/dump") 145 | end 146 | 147 | def activate_bounce(id) 148 | format_response http_client.put("bounces/#{id}/activate")["Bounce"] 149 | end 150 | 151 | def opens(options = {}) 152 | find_each('messages/outbound/opens', 'Opens', options) 153 | end 154 | 155 | def clicks(options = {}) 156 | find_each('messages/outbound/clicks', 'Clicks', options) 157 | end 158 | 159 | def get_opens(options = {}) 160 | _, batch = load_batch('messages/outbound/opens', 'Opens', options) 161 | batch 162 | end 163 | 164 | def get_clicks(options = {}) 165 | _, batch = load_batch('messages/outbound/clicks', 'Clicks', options) 166 | batch 167 | end 168 | 169 | def get_opens_by_message_id(message_id, options = {}) 170 | _, batch = load_batch("messages/outbound/opens/#{message_id}", 171 | 'Opens', 172 | options) 173 | batch 174 | end 175 | 176 | def get_clicks_by_message_id(message_id, options = {}) 177 | _, batch = load_batch("messages/outbound/clicks/#{message_id}", 178 | 'Clicks', 179 | options) 180 | batch 181 | end 182 | 183 | def opens_by_message_id(message_id, options = {}) 184 | find_each("messages/outbound/opens/#{message_id}", 'Opens', options) 185 | end 186 | 187 | def clicks_by_message_id(message_id, options = {}) 188 | find_each("messages/outbound/clicks/#{message_id}", 'Clicks', options) 189 | end 190 | 191 | def create_trigger(type, options) 192 | type = Postmark::Inflector.to_postmark(type).downcase 193 | data = serialize(HashHelper.to_postmark(options)) 194 | format_response http_client.post("triggers/#{type}", data) 195 | end 196 | 197 | def get_trigger(type, id) 198 | format_response http_client.get("triggers/#{type}/#{id}") 199 | end 200 | 201 | def delete_trigger(type, id) 202 | type = Postmark::Inflector.to_postmark(type).downcase 203 | format_response http_client.delete("triggers/#{type}/#{id}") 204 | end 205 | 206 | def get_triggers(type, options = {}) 207 | type = Postmark::Inflector.to_postmark(type) 208 | _, batch = load_batch("triggers/#{type.downcase}", type, options) 209 | batch 210 | end 211 | 212 | def triggers(type, options = {}) 213 | type = Postmark::Inflector.to_postmark(type) 214 | find_each("triggers/#{type.downcase}", type, options) 215 | end 216 | 217 | def server_info 218 | format_response http_client.get("server") 219 | end 220 | 221 | def update_server_info(attributes = {}) 222 | data = HashHelper.to_postmark(attributes) 223 | format_response http_client.put("server", serialize(data)) 224 | end 225 | 226 | def get_templates(options = {}) 227 | load_batch('templates', 'Templates', options) 228 | end 229 | 230 | def templates(options = {}) 231 | find_each('templates', 'Templates', options) 232 | end 233 | 234 | def get_template(id) 235 | format_response http_client.get("templates/#{id}") 236 | end 237 | 238 | def create_template(attributes = {}) 239 | data = serialize(HashHelper.to_postmark(attributes)) 240 | 241 | format_response http_client.post('templates', data) 242 | end 243 | 244 | def update_template(id, attributes = {}) 245 | data = serialize(HashHelper.to_postmark(attributes)) 246 | 247 | format_response http_client.put("templates/#{id}", data) 248 | end 249 | 250 | def delete_template(id) 251 | format_response http_client.delete("templates/#{id}") 252 | end 253 | 254 | def validate_template(attributes = {}) 255 | data = serialize(HashHelper.to_postmark(attributes)) 256 | response = format_response(http_client.post('templates/validate', data)) 257 | 258 | response.each do |k, v| 259 | next unless v.is_a?(Hash) && k != :suggested_template_model 260 | 261 | response[k] = HashHelper.to_ruby(v) 262 | 263 | if response[k].has_key?(:validation_errors) 264 | ruby_hashes = response[k][:validation_errors].map do |err| 265 | HashHelper.to_ruby(err) 266 | end 267 | response[k][:validation_errors] = ruby_hashes 268 | end 269 | end 270 | 271 | response 272 | end 273 | 274 | def deliver_with_template(attributes = {}) 275 | data = serialize(MessageHelper.to_postmark(attributes)) 276 | 277 | with_retries do 278 | format_response http_client.post('email/withTemplate', data) 279 | end 280 | end 281 | 282 | def deliver_in_batches_with_templates(message_hashes) 283 | in_batches(message_hashes) do |batch, offset| 284 | mapped = batch.map { |h| MessageHelper.to_postmark(h) } 285 | data = serialize(:Messages => mapped) 286 | 287 | with_retries do 288 | http_client.post('email/batchWithTemplates', data) 289 | end 290 | end 291 | end 292 | 293 | def get_stats_totals(options = {}) 294 | format_response(http_client.get('stats/outbound', options)) 295 | end 296 | 297 | def get_stats_counts(stat, options = {}) 298 | url = "stats/outbound/#{stat}" 299 | url << "/#{options[:type]}" if options.has_key?(:type) 300 | 301 | response = format_response(http_client.get(url, options)) 302 | response[:days].map! { |d| HashHelper.to_ruby(d) } 303 | response 304 | end 305 | 306 | def get_webhooks(options = {}) 307 | options = HashHelper.to_postmark(options) 308 | _, batch = load_batch('webhooks', 'Webhooks', options) 309 | batch 310 | end 311 | 312 | def get_webhook(id) 313 | format_response http_client.get("webhooks/#{id}") 314 | end 315 | 316 | def create_webhook(attributes = {}) 317 | data = serialize(HashHelper.to_postmark(attributes)) 318 | 319 | format_response http_client.post('webhooks', data) 320 | end 321 | 322 | def update_webhook(id, attributes = {}) 323 | data = serialize(HashHelper.to_postmark(attributes)) 324 | 325 | format_response http_client.put("webhooks/#{id}", data) 326 | end 327 | 328 | def delete_webhook(id) 329 | format_response http_client.delete("webhooks/#{id}") 330 | end 331 | 332 | def get_message_streams(options = {}) 333 | _, batch = load_batch('message-streams', 'MessageStreams', options) 334 | batch 335 | end 336 | 337 | def message_streams(options = {}) 338 | find_each('message-streams', 'MessageStreams', options) 339 | end 340 | 341 | def get_message_stream(id) 342 | format_response(http_client.get("message-streams/#{id}")) 343 | end 344 | 345 | def create_message_stream(attributes = {}) 346 | data = serialize(HashHelper.to_postmark(attributes, :deep => true)) 347 | format_response(http_client.post('message-streams', data), :deep => true) 348 | end 349 | 350 | def update_message_stream(id, attributes) 351 | data = serialize(HashHelper.to_postmark(attributes, :deep => true)) 352 | format_response(http_client.patch("message-streams/#{id}", data), :deep => true) 353 | end 354 | 355 | def archive_message_stream(id) 356 | format_response http_client.post("message-streams/#{id}/archive") 357 | end 358 | 359 | def unarchive_message_stream(id) 360 | format_response http_client.post("message-streams/#{id}/unarchive") 361 | end 362 | 363 | def dump_suppressions(stream_id, options = {}) 364 | _, batch = load_batch("message-streams/#{stream_id}/suppressions/dump", 'Suppressions', options) 365 | batch 366 | end 367 | 368 | def create_suppressions(stream_id, email_addresses) 369 | data = serialize(:Suppressions => Array(email_addresses).map { |e| HashHelper.to_postmark(:email_address => e) }) 370 | format_response(http_client.post("message-streams/#{stream_id}/suppressions", data)) 371 | end 372 | 373 | def delete_suppressions(stream_id, email_addresses) 374 | data = serialize(:Suppressions => Array(email_addresses).map { |e| HashHelper.to_postmark(:email_address => e) }) 375 | format_response(http_client.post("message-streams/#{stream_id}/suppressions/delete", data)) 376 | end 377 | 378 | protected 379 | 380 | def in_batches(messages) 381 | r = messages.each_slice(max_batch_size).each_with_index.map do |batch, i| 382 | yield batch, i * max_batch_size 383 | end 384 | 385 | format_response r.flatten 386 | end 387 | 388 | def update_message(message, response) 389 | response ||= {} 390 | message['X-PM-Message-Id'] = response['MessageID'] 391 | message.delivered = response['ErrorCode'] && response['ErrorCode'].zero? 392 | message.postmark_response = response 393 | end 394 | 395 | def get_for_message(action, id, options = {}) 396 | path, _, params = extract_messages_path_and_params(options) 397 | format_response http_client.get("#{path}/#{id}/#{action}", params) 398 | end 399 | 400 | def extract_messages_path_and_params(options = {}) 401 | options = options.dup 402 | messages_key = options[:inbound] ? 'InboundMessages' : 'Messages' 403 | path = options.delete(:inbound) ? 'messages/inbound' : 'messages/outbound' 404 | [path, messages_key, options] 405 | end 406 | end 407 | end 408 | -------------------------------------------------------------------------------- /spec/unit/postmark/mail_message_converter_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe Postmark::MailMessageConverter do 5 | subject {Postmark::MailMessageConverter} 6 | 7 | let(:mail_message) do 8 | Mail.new do 9 | from "sheldon@bigbangtheory.com" 10 | to "lenard@bigbangtheory.com" 11 | subject "Hello!" 12 | body "Hello Sheldon!" 13 | end 14 | end 15 | 16 | let(:mail_html_message) do 17 | Mail.new do 18 | from "sheldon@bigbangtheory.com" 19 | to "lenard@bigbangtheory.com" 20 | subject "Hello!" 21 | content_type 'text/html; charset=UTF-8' 22 | body "Hello Sheldon!" 23 | end 24 | end 25 | 26 | let(:mail_message_with_open_tracking) do 27 | Mail.new do 28 | from "sheldon@bigbangtheory.com" 29 | to "lenard@bigbangtheory.com" 30 | subject "Hello!" 31 | content_type 'text/html; charset=UTF-8' 32 | body "Hello Sheldon!" 33 | track_opens true 34 | end 35 | end 36 | 37 | let(:mail_message_with_open_tracking_disabled) do 38 | Mail.new do 39 | from "sheldon@bigbangtheory.com" 40 | to "lenard@bigbangtheory.com" 41 | subject "Hello!" 42 | content_type 'text/html; charset=UTF-8' 43 | body "Hello Sheldon!" 44 | track_opens false 45 | end 46 | end 47 | 48 | let(:mail_message_with_open_tracking_set_variable) do 49 | mail = mail_html_message 50 | mail.track_opens = true 51 | mail 52 | end 53 | 54 | let(:mail_message_with_open_tracking_disabled_set_variable) do 55 | mail = mail_html_message 56 | mail.track_opens = false 57 | mail 58 | end 59 | 60 | let(:mail_message_with_link_tracking_all) do 61 | mail = mail_html_message 62 | mail.track_links :html_and_text 63 | mail 64 | end 65 | 66 | let(:mail_message_with_link_tracking_html) do 67 | mail = mail_html_message 68 | mail.track_links = :html_only 69 | mail 70 | end 71 | 72 | let(:mail_message_with_link_tracking_text) do 73 | mail = mail_html_message 74 | mail.track_links = :text_only 75 | mail 76 | end 77 | 78 | let(:mail_message_with_link_tracking_none) do 79 | mail = mail_html_message 80 | mail.track_links = :none 81 | mail 82 | end 83 | 84 | let(:tagged_mail_message) do 85 | Mail.new do 86 | from "sheldon@bigbangtheory.com" 87 | to "lenard@bigbangtheory.com" 88 | subject "Hello!" 89 | body "Hello Sheldon!" 90 | tag "sheldon" 91 | end 92 | end 93 | 94 | let(:mail_message_without_body) do 95 | Mail.new do 96 | from "sheldon@bigbangtheory.com" 97 | to "lenard@bigbangtheory.com" 98 | subject "Hello!" 99 | end 100 | end 101 | 102 | let(:mail_multipart_message) do 103 | Mail.new do 104 | from "sheldon@bigbangtheory.com" 105 | to "lenard@bigbangtheory.com" 106 | subject "Hello!" 107 | text_part do 108 | body "Hello Sheldon!" 109 | end 110 | html_part do 111 | body "Hello Sheldon!" 112 | end 113 | end 114 | end 115 | 116 | let(:mail_message_with_attachment) do 117 | Mail.new do 118 | from "sheldon@bigbangtheory.com" 119 | to "lenard@bigbangtheory.com" 120 | subject "Hello!" 121 | body "Hello Sheldon!" 122 | add_file empty_gif_path 123 | end 124 | end 125 | 126 | let(:mail_message_with_named_addresses) do 127 | Mail.new do 128 | from "Sheldon " 129 | to "\"Leonard Hofstadter\" " 130 | subject "Hello!" 131 | body "Hello Sheldon!" 132 | reply_to '"Penny The Neighbor" ' 133 | end 134 | end 135 | 136 | let(:mail_message_quoted_printable) do 137 | Mail.new do 138 | from "Sheldon " 139 | to "\"Leonard Hofstadter\" " 140 | subject "Hello!" 141 | content_type 'text/plain; charset=utf-8' 142 | content_transfer_encoding 'quoted-printable' 143 | body 'Он здесь бывал: еще не в галифе.' 144 | reply_to '"Penny The Neighbor" ' 145 | end 146 | end 147 | 148 | let(:multipart_message_quoted_printable) do 149 | Mail.new do 150 | from "sheldon@bigbangtheory.com" 151 | to "lenard@bigbangtheory.com" 152 | subject "Hello!" 153 | text_part do 154 | content_type 'text/plain; charset=utf-8' 155 | content_transfer_encoding 'quoted-printable' 156 | body 'Загадочное послание.' 157 | end 158 | html_part do 159 | content_type 'text/html; charset=utf-8' 160 | content_transfer_encoding 'quoted-printable' 161 | body 'Загадочное послание.' 162 | end 163 | end 164 | end 165 | 166 | let(:templated_message) do 167 | Mail.new do 168 | from "sheldon@bigbangtheory.com" 169 | to "lenard@bigbangtheory.com" 170 | template_alias "hello" 171 | template_model :name => "Sheldon" 172 | end 173 | end 174 | 175 | it 'converts plain text messages correctly' do 176 | expect(subject.new(mail_message).run).to eq({ 177 | "From" => "sheldon@bigbangtheory.com", 178 | "Subject" => "Hello!", 179 | "TextBody" => "Hello Sheldon!", 180 | "To" => "lenard@bigbangtheory.com"}) 181 | end 182 | 183 | it 'converts tagged text messages correctly' do 184 | expect(subject.new(tagged_mail_message).run).to eq({ 185 | "From" => "sheldon@bigbangtheory.com", 186 | "Subject" => "Hello!", 187 | "TextBody" => "Hello Sheldon!", 188 | "Tag" => "sheldon", 189 | "To" => "lenard@bigbangtheory.com"}) 190 | end 191 | 192 | it 'converts plain text messages without body correctly' do 193 | expect(subject.new(mail_message_without_body).run).to eq({ 194 | "From" => "sheldon@bigbangtheory.com", 195 | "Subject" => "Hello!", 196 | "To" => "lenard@bigbangtheory.com"}) 197 | end 198 | 199 | it 'converts html messages correctly' do 200 | expect(subject.new(mail_html_message).run).to eq({ 201 | "From" => "sheldon@bigbangtheory.com", 202 | "Subject" => "Hello!", 203 | "HtmlBody" => "Hello Sheldon!", 204 | "To" => "lenard@bigbangtheory.com"}) 205 | end 206 | 207 | it 'converts multipart messages correctly' do 208 | expect(subject.new(mail_multipart_message).run).to eq({ 209 | "From" => "sheldon@bigbangtheory.com", 210 | "Subject" => "Hello!", 211 | "HtmlBody" => "Hello Sheldon!", 212 | "TextBody" => "Hello Sheldon!", 213 | "To" => "lenard@bigbangtheory.com"}) 214 | end 215 | 216 | it 'converts messages with attachments correctly' do 217 | expect(subject.new(mail_message_with_attachment).run).to eq({ 218 | "From" => "sheldon@bigbangtheory.com", 219 | "Subject" => "Hello!", 220 | "Attachments" => [{"Name" => "empty.gif", 221 | "Content" => encoded_empty_gif_data, 222 | "ContentType" => "image/gif"}], 223 | "TextBody" => "Hello Sheldon!", 224 | "To" => "lenard@bigbangtheory.com"}) 225 | end 226 | 227 | it 'converts messages with named addresses correctly' do 228 | expect(subject.new(mail_message_with_named_addresses).run).to eq({ 229 | "From" => "Sheldon ", 230 | "Subject" => "Hello!", 231 | "TextBody" => "Hello Sheldon!", 232 | "To" => "Leonard Hofstadter ", 233 | "ReplyTo" => 'Penny The Neighbor '}) 234 | end 235 | 236 | it 'convertes templated messages correctly' do 237 | expect(subject.new(templated_message).run).to eq({ 238 | "From" => "sheldon@bigbangtheory.com", 239 | "TemplateAlias" => "hello", 240 | "TemplateModel" => {:name => "Sheldon"}, 241 | "To" => "lenard@bigbangtheory.com"}) 242 | end 243 | 244 | context 'open tracking' do 245 | context 'setup inside of mail' do 246 | it 'converts open tracking enabled messages correctly' do 247 | expect(subject.new(mail_message_with_open_tracking).run).to eq({ 248 | "From" => "sheldon@bigbangtheory.com", 249 | "Subject" => "Hello!", 250 | "HtmlBody" => "Hello Sheldon!", 251 | "To" => "lenard@bigbangtheory.com", 252 | "TrackOpens" => true}) 253 | end 254 | 255 | it 'converts open tracking disabled messages correctly' do 256 | expect(subject.new(mail_message_with_open_tracking_disabled).run).to eq({ 257 | "From" => "sheldon@bigbangtheory.com", 258 | "Subject" => "Hello!", 259 | "HtmlBody" => "Hello Sheldon!", 260 | "To" => "lenard@bigbangtheory.com", 261 | "TrackOpens" => false}) 262 | end 263 | end 264 | 265 | context 'setup with tracking variable' do 266 | it 'converts open tracking enabled messages correctly' do 267 | expect(subject.new(mail_message_with_open_tracking_set_variable).run).to eq({ 268 | "From" => "sheldon@bigbangtheory.com", 269 | "Subject" => "Hello!", 270 | "HtmlBody" => "Hello Sheldon!", 271 | "To" => "lenard@bigbangtheory.com", 272 | "TrackOpens" => true}) 273 | end 274 | 275 | it 'converts open tracking disabled messages correctly' do 276 | expect(subject.new(mail_message_with_open_tracking_disabled_set_variable).run).to eq({ 277 | "From" => "sheldon@bigbangtheory.com", 278 | "Subject" => "Hello!", 279 | "HtmlBody" => "Hello Sheldon!", 280 | "To" => "lenard@bigbangtheory.com", 281 | "TrackOpens" => false}) 282 | end 283 | end 284 | end 285 | 286 | context 'link tracking' do 287 | it 'converts html and text link tracking enabled messages correctly' do 288 | expect(subject.new(mail_message_with_link_tracking_all).run).to eq({ 289 | "From" => "sheldon@bigbangtheory.com", 290 | "Subject" => "Hello!", 291 | "HtmlBody" => "Hello Sheldon!", 292 | "To" => "lenard@bigbangtheory.com", 293 | "TrackLinks" => 'HtmlAndText'}) 294 | end 295 | 296 | it 'converts html only link tracking enabled messages correctly' do 297 | expect(subject.new(mail_message_with_link_tracking_html).run).to eq({ 298 | "From" => "sheldon@bigbangtheory.com", 299 | "Subject" => "Hello!", 300 | "HtmlBody" => "Hello Sheldon!", 301 | "To" => "lenard@bigbangtheory.com", 302 | "TrackLinks" => 'HtmlOnly'}) 303 | end 304 | 305 | it 'converts text only link tracking enabled messages correctly' do 306 | expect(subject.new(mail_message_with_link_tracking_text).run).to eq({ 307 | "From" => "sheldon@bigbangtheory.com", 308 | "Subject" => "Hello!", 309 | "HtmlBody" => "Hello Sheldon!", 310 | "To" => "lenard@bigbangtheory.com", 311 | "TrackLinks" => 'TextOnly'}) 312 | end 313 | 314 | it 'converts link tracking disabled messages correctly' do 315 | expect(subject.new(mail_message_with_link_tracking_none).run).to eq ({ 316 | "From" => "sheldon@bigbangtheory.com", 317 | "Subject" => "Hello!", 318 | "HtmlBody" => "Hello Sheldon!", 319 | "To" => "lenard@bigbangtheory.com", 320 | "TrackLinks" => 'None'}) 321 | end 322 | 323 | it 'converts link tracking options when set via header' do 324 | msg = mail_html_message 325 | msg[:track_links] = :html_and_text 326 | expect(subject.new(msg).run).to include('TrackLinks' => 'HtmlAndText') 327 | end 328 | end 329 | 330 | context 'metadata' do 331 | it 'converts single metadata field' do 332 | metadata = {:test => 'test'} 333 | msg = mail_html_message 334 | msg.metadata = metadata 335 | expect(subject.new(msg).run).to include('Metadata' => metadata) 336 | end 337 | 338 | it 'converts unicode metadata field metadata' do 339 | metadata = {:test => "Велик"} 340 | msg = mail_html_message 341 | msg.metadata = metadata 342 | expect(subject.new(msg).run).to include('Metadata' => metadata) 343 | end 344 | 345 | it 'converts multiple metadata fields' do 346 | metadata = {} 347 | 10.times {|i| metadata["test#{i + 1}"] = "t" * 80} 348 | msg = mail_html_message 349 | msg.metadata = metadata 350 | expect(subject.new(msg).run).to include('Metadata' => metadata) 351 | end 352 | end 353 | 354 | it 'correctly decodes unicode in messages transfered as quoted-printable' do 355 | expect(subject.new(mail_message_quoted_printable).run).to include('TextBody' => 'Он здесь бывал: еще не в галифе.') 356 | end 357 | 358 | it 'correctly decodes unicode in multipart quoted-printable messages' do 359 | expect(subject.new(multipart_message_quoted_printable).run).to include( 360 | 'TextBody' => 'Загадочное послание.', 361 | 'HtmlBody' => 'Загадочное послание.') 362 | end 363 | 364 | context 'when bcc is empty' do 365 | it 'excludes bcc from message' do 366 | mail_message.bcc = nil 367 | expect(mail_message.to_postmark_hash.keys).not_to include('Bcc') 368 | end 369 | end 370 | 371 | context 'when cc is empty' do 372 | it 'excludes cc from message' do 373 | mail_message.cc = nil 374 | expect(mail_message.to_postmark_hash.keys).not_to include('Cc') 375 | end 376 | end 377 | 378 | describe 'passing message stream' do 379 | context 'when not set' do 380 | specify { expect(subject.new(mail_message).run).not_to include('MessageStream') } 381 | end 382 | 383 | context 'when set' do 384 | before do 385 | mail_message.message_stream = 'weekly-newsletter' 386 | end 387 | 388 | it 'passes message stream to the API call' do 389 | expect(subject.new(mail_message).run).to include('MessageStream' => 'weekly-newsletter') 390 | end 391 | end 392 | end 393 | end 394 | -------------------------------------------------------------------------------- /spec/unit/postmark/account_api_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Postmark::AccountApiClient do 4 | let(:api_token) { 'abcd-efgh' } 5 | subject { Postmark::AccountApiClient} 6 | 7 | it 'can be created with an API token' do 8 | expect { subject.new(api_token) }.not_to raise_error 9 | end 10 | 11 | it 'can be created with an API token and options hash' do 12 | expect { subject.new(api_token, :http_read_timeout => 5) }.not_to raise_error 13 | end 14 | 15 | context 'instance' do 16 | subject { Postmark::AccountApiClient.new(api_token) } 17 | 18 | it 'uses the auth header specific for Account API' do 19 | auth_header = subject.http_client.auth_header_name 20 | expect(auth_header).to eq('X-Postmark-Account-Token') 21 | end 22 | 23 | describe '#senders' do 24 | let(:response) { 25 | { 26 | 'TotalCount' => 10, 'SenderSignatures' => [{}, {}] 27 | } 28 | } 29 | 30 | it 'is aliased as #signatures' do 31 | expect(subject).to respond_to(:signatures) 32 | expect(subject).to respond_to(:signatures).with(1).argument 33 | end 34 | 35 | it 'returns an enumerator' do 36 | expect(subject.senders).to be_kind_of(Enumerable) 37 | end 38 | 39 | it 'lazily loads senders' do 40 | allow(subject.http_client).to receive(:get). 41 | with('senders', an_instance_of(Hash)).and_return(response) 42 | subject.senders.take(1000) 43 | end 44 | end 45 | 46 | describe '#get_senders' do 47 | let(:response) { 48 | { 49 | "TotalCount" => 1, 50 | "SenderSignatures" => [{ 51 | "Domain" => "example.com", 52 | "EmailAddress" => "someone@example.com", 53 | "ReplyToEmailAddress" => "info@example.com", 54 | "Name" => "Example User", 55 | "Confirmed" => true, 56 | "ID" => 8139 57 | }] 58 | } 59 | } 60 | 61 | it 'is aliased as #get_signatures' do 62 | expect(subject).to respond_to(:get_signatures).with(1).argument 63 | end 64 | 65 | it 'performs a GET request to /senders endpoint' do 66 | allow(subject.http_client).to receive(:get). 67 | with('senders', :offset => 0, :count => 30). 68 | and_return(response) 69 | subject.get_senders 70 | end 71 | 72 | it 'formats the keys of returned list of senders' do 73 | allow(subject.http_client).to receive(:get).and_return(response) 74 | keys = subject.get_senders.map { |s| s.keys }.flatten 75 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 76 | end 77 | 78 | it 'accepts offset and count options' do 79 | allow(subject.http_client).to receive(:get). 80 | with('senders', :offset => 10, :count => 42). 81 | and_return(response) 82 | subject.get_senders(:offset => 10, :count => 42) 83 | end 84 | end 85 | 86 | describe '#get_senders_count' do 87 | let(:response) { {'TotalCount' => 42} } 88 | 89 | it 'is aliased as #get_signatures_count' do 90 | expect(subject).to respond_to(:get_signatures_count) 91 | expect(subject).to respond_to(:get_signatures_count).with(1).argument 92 | end 93 | 94 | it 'returns a total number of senders' do 95 | allow(subject.http_client).to receive(:get). 96 | with('senders', an_instance_of(Hash)).and_return(response) 97 | expect(subject.get_senders_count).to eq(42) 98 | end 99 | end 100 | 101 | describe '#get_sender' do 102 | let(:response) { 103 | { 104 | "Domain" => "example.com", 105 | "EmailAddress" => "someone@example.com", 106 | "ReplyToEmailAddress" => "info@example.com", 107 | "Name" => "Example User", 108 | "Confirmed" => true, 109 | "ID" => 8139 110 | } 111 | } 112 | 113 | it 'is aliased as #get_signature' do 114 | expect(subject).to respond_to(:get_signature).with(1).argument 115 | end 116 | 117 | it 'performs a GET request to /senders/:id endpoint' do 118 | allow(subject.http_client).to receive(:get).with("senders/42"). 119 | and_return(response) 120 | subject.get_sender(42) 121 | end 122 | 123 | it 'formats the keys of returned response' do 124 | allow(subject.http_client).to receive(:get).and_return(response) 125 | keys = subject.get_sender(42).keys 126 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 127 | end 128 | end 129 | 130 | describe '#create_sender' do 131 | let(:response) { 132 | { 133 | "Domain" => "example.com", 134 | "EmailAddress" => "someone@example.com", 135 | "ReplyToEmailAddress" => "info@example.com", 136 | "Name" => "Example User", 137 | "Confirmed" => true, 138 | "ID" => 8139 139 | } 140 | } 141 | 142 | it 'is aliased as #create_signature' do 143 | expect(subject).to respond_to(:create_signature).with(1).argument 144 | end 145 | 146 | it 'performs a POST request to /senders endpoint' do 147 | allow(subject.http_client).to receive(:post). 148 | with("senders", an_instance_of(String)).and_return(response) 149 | subject.create_sender(:name => 'Chris Nagele') 150 | end 151 | 152 | it 'converts the sender attributes names to camel case' do 153 | allow(subject.http_client).to receive(:post). 154 | with("senders", {'FooBar' => 'bar'}.to_json).and_return(response) 155 | subject.create_sender(:foo_bar => 'bar') 156 | end 157 | 158 | it 'formats the keys of returned response' do 159 | allow(subject.http_client).to receive(:post).and_return(response) 160 | keys = subject.create_sender(:foo => 'bar').keys 161 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 162 | end 163 | end 164 | 165 | describe '#update_sender' do 166 | let(:response) { 167 | { 168 | "Domain" => "example.com", 169 | "EmailAddress" => "someone@example.com", 170 | "ReplyToEmailAddress" => "info@example.com", 171 | "Name" => "Example User", 172 | "Confirmed" => true, 173 | "ID" => 8139 174 | } 175 | } 176 | 177 | it 'is aliased as #update_signature' do 178 | expect(subject).to respond_to(:update_signature).with(1).argument 179 | expect(subject).to respond_to(:update_signature).with(2).arguments 180 | end 181 | 182 | it 'performs a PUT request to /senders/:id endpoint' do 183 | allow(subject.http_client).to receive(:put). 184 | with('senders/42', an_instance_of(String)).and_return(response) 185 | subject.update_sender(42, :name => 'Chris Nagele') 186 | end 187 | 188 | it 'converts the sender attributes names to camel case' do 189 | allow(subject.http_client).to receive(:put). 190 | with('senders/42', {'FooBar' => 'bar'}.to_json).and_return(response) 191 | subject.update_sender(42, :foo_bar => 'bar') 192 | end 193 | 194 | it 'formats the keys of returned response' do 195 | allow(subject.http_client).to receive(:put).and_return(response) 196 | keys = subject.update_sender(42, :foo => 'bar').keys 197 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 198 | end 199 | end 200 | 201 | describe '#resend_sender_confirmation' do 202 | let(:response) { 203 | { 204 | "ErrorCode" => 0, 205 | "Message" => "Confirmation email for Sender Signature someone@example.com was re-sent." 206 | } 207 | } 208 | 209 | it 'is aliased as #resend_signature_confirmation' do 210 | expect(subject).to respond_to(:resend_signature_confirmation). 211 | with(1).argument 212 | end 213 | 214 | it 'performs a POST request to /senders/:id/resend endpoint' do 215 | allow(subject.http_client).to receive(:post). 216 | with('senders/42/resend').and_return(response) 217 | subject.resend_sender_confirmation(42) 218 | end 219 | 220 | it 'formats the keys of returned response' do 221 | allow(subject.http_client).to receive(:post).and_return(response) 222 | keys = subject.resend_sender_confirmation(42).keys 223 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 224 | end 225 | end 226 | 227 | describe '#verified_sender_spf?' do 228 | let(:response) { {"SPFVerified" => true} } 229 | let(:false_response) { {"SPFVerified" => false} } 230 | 231 | it 'is aliased as #verified_signature_spf?' do 232 | expect(subject).to respond_to(:verified_signature_spf?).with(1).argument 233 | end 234 | 235 | it 'performs a POST request to /senders/:id/verifyspf endpoint' do 236 | allow(subject.http_client).to receive(:post). 237 | with('senders/42/verifyspf').and_return(response) 238 | subject.verified_sender_spf?(42) 239 | end 240 | 241 | it 'returns false when SPFVerified field of the response is false' do 242 | allow(subject.http_client).to receive(:post).and_return(false_response) 243 | expect(subject.verified_sender_spf?(42)).to be false 244 | end 245 | 246 | it 'returns true when SPFVerified field of the response is true' do 247 | allow(subject.http_client).to receive(:post).and_return(response) 248 | expect(subject.verified_sender_spf?(42)).to be true 249 | end 250 | end 251 | 252 | describe '#request_new_sender_dkim' do 253 | let(:response) { 254 | { 255 | "Domain" => "example.com", 256 | "EmailAddress" => "someone@example.com", 257 | "ReplyToEmailAddress" => "info@example.com", 258 | "Name" => "Example User", 259 | "Confirmed" => true, 260 | "ID" => 8139 261 | } 262 | } 263 | 264 | it 'is aliased as #request_new_signature_dkim' do 265 | expect(subject).to respond_to(:request_new_signature_dkim). 266 | with(1).argument 267 | end 268 | 269 | it 'performs a POST request to /senders/:id/requestnewdkim endpoint' do 270 | allow(subject.http_client).to receive(:post). 271 | with('senders/42/requestnewdkim').and_return(response) 272 | subject.request_new_sender_dkim(42) 273 | end 274 | 275 | it 'formats the keys of returned response' do 276 | allow(subject.http_client).to receive(:post).and_return(response) 277 | keys = subject.request_new_sender_dkim(42).keys 278 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 279 | end 280 | end 281 | 282 | describe '#delete_sender' do 283 | let(:response) { 284 | { 285 | "ErrorCode" => 0, 286 | "Message" => "Signature someone@example.com removed." 287 | } 288 | } 289 | 290 | it 'is aliased as #delete_signature' do 291 | expect(subject).to respond_to(:delete_signature).with(1).argument 292 | end 293 | 294 | it 'performs a DELETE request to /senders/:id endpoint' do 295 | allow(subject.http_client).to receive(:delete). 296 | with('senders/42').and_return(response) 297 | subject.delete_sender(42) 298 | end 299 | 300 | it 'formats the keys of returned response' do 301 | allow(subject.http_client).to receive(:delete).and_return(response) 302 | keys = subject.delete_sender(42).keys 303 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 304 | end 305 | end 306 | 307 | describe '#domains' do 308 | let(:response) {{'TotalCount' => 10, 'Domains' => [{}, {}]}} 309 | 310 | it 'returns an enumerator' do 311 | expect(subject.domains).to be_kind_of(Enumerable) 312 | end 313 | 314 | it 'lazily loads domains' do 315 | allow(subject.http_client).to receive(:get). 316 | with('domains', an_instance_of(Hash)).and_return(response) 317 | subject.domains.take(1000) 318 | end 319 | 320 | end 321 | 322 | describe '#get_domains' do 323 | 324 | let(:response) { 325 | { 326 | "TotalCount" => 1, 327 | "Domains" => [{ 328 | "Name" => "example.com", 329 | "ID" => 8139 330 | }] 331 | } 332 | } 333 | 334 | it 'performs a GET request to /domains endpoint' do 335 | allow(subject.http_client).to receive(:get). 336 | with('domains', :offset => 0, :count => 30). 337 | and_return(response) 338 | subject.get_domains 339 | end 340 | 341 | it 'formats the keys of returned list of domains' do 342 | allow(subject.http_client).to receive(:get).and_return(response) 343 | keys = subject.get_domains.map { |s| s.keys }.flatten 344 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 345 | end 346 | 347 | it 'accepts offset and count options' do 348 | allow(subject.http_client).to receive(:get). 349 | with('domains', :offset => 10, :count => 42). 350 | and_return(response) 351 | subject.get_domains(:offset => 10, :count => 42) 352 | end 353 | 354 | end 355 | 356 | describe '#get_domains_count' do 357 | let(:response) { {'TotalCount' => 42} } 358 | 359 | it 'returns a total number of domains' do 360 | allow(subject.http_client).to receive(:get). 361 | with('domains', an_instance_of(Hash)).and_return(response) 362 | expect(subject.get_domains_count).to eq(42) 363 | end 364 | end 365 | 366 | describe '#get_domain' do 367 | let(:response) { 368 | { 369 | "Name" => "example.com", 370 | "ID" => 8139 371 | } 372 | } 373 | 374 | it 'performs a GET request to /domains/:id endpoint' do 375 | allow(subject.http_client).to receive(:get).with("domains/42"). 376 | and_return(response) 377 | subject.get_domain(42) 378 | end 379 | 380 | it 'formats the keys of returned response' do 381 | allow(subject.http_client).to receive(:get).and_return(response) 382 | keys = subject.get_domain(42).keys 383 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 384 | end 385 | end 386 | 387 | describe '#create_domain' do 388 | let(:response) { 389 | { 390 | "Name" => "example.com", 391 | "ID" => 8139 392 | } 393 | } 394 | 395 | it 'performs a POST request to /domains endpoint' do 396 | allow(subject.http_client).to receive(:post). 397 | with("domains", an_instance_of(String)).and_return(response) 398 | subject.create_domain(:name => 'example.com') 399 | end 400 | 401 | it 'converts the domain attributes names to camel case' do 402 | allow(subject.http_client).to receive(:post). 403 | with("domains", {'FooBar' => 'bar'}.to_json).and_return(response) 404 | subject.create_domain(:foo_bar => 'bar') 405 | end 406 | 407 | it 'formats the keys of returned response' do 408 | allow(subject.http_client).to receive(:post).and_return(response) 409 | keys = subject.create_domain(:foo => 'bar').keys 410 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 411 | end 412 | end 413 | 414 | describe '#update_domain' do 415 | let(:response) { 416 | { 417 | "Name" => "example.com", 418 | "ReturnPathDomain" => "return.example.com", 419 | "ID" => 8139 420 | } 421 | } 422 | 423 | it 'performs a PUT request to /domains/:id endpoint' do 424 | allow(subject.http_client).to receive(:put). 425 | with('domains/42', an_instance_of(String)).and_return(response) 426 | subject.update_domain(42, :return_path_domain => 'updated-return.example.com') 427 | end 428 | 429 | it 'converts the domain attributes names to camel case' do 430 | allow(subject.http_client).to receive(:put). 431 | with('domains/42', {'FooBar' => 'bar'}.to_json).and_return(response) 432 | subject.update_domain(42, :foo_bar => 'bar') 433 | end 434 | 435 | it 'formats the keys of returned response' do 436 | allow(subject.http_client).to receive(:put).and_return(response) 437 | keys = subject.update_domain(42, :foo => 'bar').keys 438 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 439 | end 440 | end 441 | 442 | describe '#verified_domain_spf?' do 443 | let(:response) { {"SPFVerified" => true} } 444 | let(:false_response) { {"SPFVerified" => false} } 445 | 446 | it 'performs a POST request to /domains/:id/verifyspf endpoint' do 447 | allow(subject.http_client).to receive(:post). 448 | with('domains/42/verifyspf').and_return(response) 449 | subject.verified_domain_spf?(42) 450 | end 451 | 452 | it 'returns false when SPFVerified field of the response is false' do 453 | allow(subject.http_client).to receive(:post).and_return(false_response) 454 | expect(subject.verified_domain_spf?(42)).to be false 455 | end 456 | 457 | it 'returns true when SPFVerified field of the response is true' do 458 | allow(subject.http_client).to receive(:post).and_return(response) 459 | expect(subject.verified_domain_spf?(42)).to be true 460 | end 461 | end 462 | 463 | describe '#rotate_domain_dkim' do 464 | let(:response) { 465 | { 466 | "Name" => "example.com", 467 | "ID" => 8139 468 | } 469 | } 470 | 471 | it 'performs a POST request to /domains/:id/rotatedkim endpoint' do 472 | allow(subject.http_client).to receive(:post). 473 | with('domains/42/rotatedkim').and_return(response) 474 | subject.rotate_domain_dkim(42) 475 | end 476 | 477 | it 'formats the keys of returned response' do 478 | allow(subject.http_client).to receive(:post).and_return(response) 479 | keys = subject.rotate_domain_dkim(42).keys 480 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 481 | end 482 | end 483 | 484 | describe '#delete_domain' do 485 | let(:response) { 486 | { 487 | "ErrorCode" => 0, 488 | "Message" => "Domain example.com removed." 489 | } 490 | } 491 | 492 | it 'performs a DELETE request to /domains/:id endpoint' do 493 | allow(subject.http_client).to receive(:delete). 494 | with('domains/42').and_return(response) 495 | subject.delete_domain(42) 496 | end 497 | 498 | it 'formats the keys of returned response' do 499 | allow(subject.http_client).to receive(:delete).and_return(response) 500 | keys = subject.delete_sender(42).keys 501 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 502 | end 503 | end 504 | 505 | describe '#servers' do 506 | let(:response) { {'TotalCount' => 10, 'Servers' => [{}, {}]} } 507 | 508 | it 'returns an enumerator' do 509 | expect(subject.servers).to be_kind_of(Enumerable) 510 | end 511 | 512 | it 'lazily loads servers' do 513 | allow(subject.http_client).to receive(:get). 514 | with('servers', an_instance_of(Hash)).and_return(response) 515 | subject.servers.take(100) 516 | end 517 | end 518 | 519 | describe '#get_servers' do 520 | let(:response) { 521 | { 522 | 'TotalCount' => 1, 523 | 'Servers' => [ 524 | { 525 | "ID" => 11635, 526 | "Name" => "Production01", 527 | "ApiTokens" => [ 528 | "fe6ec0cf-ff06-787a-b5e9-e77a41c61ce3" 529 | ], 530 | "ServerLink" => "https://postmarkapp.com/servers/11635/overview", 531 | "Color" => "red", 532 | "SmtpApiActivated" => true, 533 | "RawEmailEnabled" => false, 534 | "InboundAddress" => "7373de3ebd66acea228fjkdkf88dd7d5@inbound.postmarkapp.com", 535 | "InboundHookUrl" => "http://inboundhook.example.com/inbound", 536 | "BounceHookUrl" => "http://bouncehook.example.com/bounce", 537 | "InboundDomain" => "", 538 | "InboundHash" => "7373de3ebd66acea228fjkdkf88dd7d5" 539 | } 540 | ] 541 | } 542 | } 543 | 544 | it 'performs a GET request to /servers endpoint' do 545 | allow(subject.http_client).to receive(:get). 546 | with('servers', an_instance_of(Hash)).and_return(response) 547 | subject.get_servers 548 | end 549 | 550 | it 'formats the keys of returned list of servers' do 551 | allow(subject.http_client).to receive(:get).and_return(response) 552 | keys = subject.get_servers.map { |s| s.keys }.flatten 553 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 554 | end 555 | 556 | it 'accepts offset and count options' do 557 | allow(subject.http_client).to receive(:get). 558 | with('servers', :offset => 30, :count => 50). 559 | and_return(response) 560 | subject.get_servers(:offset => 30, :count => 50) 561 | end 562 | end 563 | 564 | describe '#get_server' do 565 | let(:response) { 566 | { 567 | "ID" => 7438, 568 | "Name" => "Staging Testing", 569 | "ApiTokens" => [ 570 | "fe6ec0cf-ff06-44aa-jf88-e77a41c61ce3" 571 | ], 572 | "ServerLink" => "https://postmarkapp.com/servers/7438/overview", 573 | "Color" => "red", 574 | "SmtpApiActivated" => true, 575 | "RawEmailEnabled" => false, 576 | "InboundAddress" => "7373de3ebd66acea22812731fb1dd7d5@inbound.postmarkapp.com", 577 | "InboundHookUrl" => "", 578 | "BounceHookUrl" => "", 579 | "InboundDomain" => "", 580 | "InboundHash" => "7373de3ebd66acea22812731fb1dd7d5" 581 | } 582 | } 583 | 584 | it 'performs a GET request to /servers/:id endpoint' do 585 | allow(subject.http_client).to receive(:get). 586 | with('servers/42').and_return(response) 587 | subject.get_server(42) 588 | end 589 | 590 | it 'formats the keys of returned response' do 591 | allow(subject.http_client).to receive(:get).and_return(response) 592 | keys = subject.get_server(42).keys 593 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 594 | end 595 | end 596 | 597 | describe '#get_servers_count' do 598 | let(:response) { {'TotalCount' => 42} } 599 | 600 | it 'returns a total number of servers' do 601 | allow(subject.http_client).to receive(:get). 602 | with('servers', an_instance_of(Hash)).and_return(response) 603 | expect(subject.get_servers_count).to eq(42) 604 | end 605 | 606 | end 607 | 608 | describe '#create_server' do 609 | let(:response) { 610 | { 611 | "Name" => "Staging Testing", 612 | "Color" => "red", 613 | "SmtpApiActivated" => true, 614 | "RawEmailEnabled" => false, 615 | "InboundHookUrl" => "http://hooks.example.com/inbound", 616 | "BounceHookUrl" => "http://hooks.example.com/bounce", 617 | "InboundDomain" => "" 618 | } 619 | } 620 | 621 | it 'performs a POST request to /servers endpoint' do 622 | allow(subject.http_client).to receive(:post). 623 | with('servers', an_instance_of(String)).and_return(response) 624 | subject.create_server(:foo => 'bar') 625 | end 626 | 627 | it 'converts the server attribute names to camel case' do 628 | allow(subject.http_client).to receive(:post). 629 | with('servers', {'FooBar' => 'foo_bar'}.to_json). 630 | and_return(response) 631 | subject.create_server(:foo_bar => 'foo_bar') 632 | end 633 | 634 | it 'formats the keys of returned response' do 635 | allow(subject.http_client).to receive(:post).and_return(response) 636 | keys = subject.create_server(:foo => 'bar').keys 637 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 638 | end 639 | end 640 | 641 | describe '#update_server' do 642 | let(:response) { 643 | { 644 | "ID" => 7450, 645 | "Name" => "Production Testing", 646 | "ApiTokens" => [ 647 | "fe6ec0cf-ff06-44aa-jf88-e77a41c61ce3" 648 | ], 649 | "ServerLink" => "https://postmarkapp.com/servers/7438/overview", 650 | "Color" => "blue", 651 | "SmtpApiActivated" => false, 652 | "RawEmailEnabled" => false, 653 | "InboundAddress" => "7373de3ebd66acea22812731fb1dd7d5@inbound.postmarkapp.com", 654 | "InboundHookUrl" => "http://hooks.example.com/inbound", 655 | "BounceHookUrl" => "http://hooks.example.com/bounce", 656 | "InboundDomain" => "", 657 | "InboundHash" => "7373de3ebd66acea22812731fb1dd7d5" 658 | } 659 | } 660 | 661 | it 'converts the server attribute names to camel case' do 662 | allow(subject.http_client).to receive(:put). 663 | with(an_instance_of(String), {'FooBar' => 'foo_bar'}.to_json). 664 | and_return(response) 665 | subject.update_server(42, :foo_bar => 'foo_bar') 666 | end 667 | 668 | it 'performs a PUT request to /servers/:id endpoint' do 669 | allow(subject.http_client).to receive(:put). 670 | with('servers/42', an_instance_of(String)). 671 | and_return(response) 672 | subject.update_server(42, :foo => 'bar') 673 | end 674 | 675 | it 'formats the keys of returned response' do 676 | allow(subject.http_client).to receive(:put).and_return(response) 677 | keys = subject.update_server(42, :foo => 'bar').keys 678 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 679 | end 680 | end 681 | 682 | describe '#delete_server' do 683 | let(:response) { 684 | { 685 | "ErrorCode" => "0", 686 | "Message" => "Server Production Testing removed." 687 | } 688 | } 689 | 690 | it 'performs a DELETE request to /servers/:id endpoint' do 691 | allow(subject.http_client).to receive(:delete). 692 | with('servers/42').and_return(response) 693 | subject.delete_server(42) 694 | end 695 | 696 | it 'formats the keys of returned response' do 697 | allow(subject.http_client).to receive(:delete).and_return(response) 698 | keys = subject.delete_server(42).keys 699 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 700 | end 701 | end 702 | 703 | describe '#push_templates' do 704 | let(:response) { 705 | {"TotalCount"=>5, 706 | "Templates"=> 707 | [{"Action"=>"Create", "TemplateId"=>nil, "Alias"=>"alias1", "Name"=>"Comment notification"}, 708 | {"Action"=>"Create", "TemplateId"=>nil, "Alias"=>"alias2", "Name"=>"Password reset"}]} 709 | } 710 | 711 | let(:request_data) {{:source_server_id => 1, :destination_server_id => 2, :perform_changes => false}} 712 | 713 | it 'gets templates info and converts it to ruby format' do 714 | allow(subject.http_client).to receive(:put).and_return(response) 715 | templates = subject.push_templates({:source_server_id => 1, :destination_server_id => 2, :perform_changes => false} ) 716 | 717 | expect(templates.size).to eq(2) 718 | expect(templates.first[:action]).to eq('Create') 719 | expect(templates.first[:alias]).to eq('alias1') 720 | end 721 | 722 | it 'formats the keys of returned response' do 723 | allow(subject.http_client).to receive(:put).and_return(response) 724 | templates = subject.push_templates({:source_server_id => 1, :destination_server_id => 2, :perform_changes => false} ) 725 | 726 | keys = templates.map { |template| template.keys }.flatten 727 | expect(keys.all? { |k| k.is_a?(Symbol) }).to be true 728 | end 729 | end 730 | 731 | end 732 | end 733 | --------------------------------------------------------------------------------