diff --git a/Gemfile b/Gemfile index 95f82040a..70a30f746 100644 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,6 @@ gem "kaminari" gem "klogger-logger" gem "konfig-config", "~> 3.0" gem "mail" -gem "moonrope" gem "mysql2" gem "nifty-utils" gem "nilify_blanks" diff --git a/Gemfile.lock b/Gemfile.lock index b9cfedf1f..eb937325a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,7 +103,6 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.4) - deep_merge (1.2.2) diff-lcs (1.5.0) domain_name (0.6.20240107) dotenv (3.0.2) @@ -184,10 +183,6 @@ GEM mini_mime (1.1.5) mini_portile2 (2.8.5) minitest (5.22.2) - moonrope (2.0.2) - deep_merge (~> 1.0) - json - rack (>= 1.4) mysql2 (0.5.6) net-http (0.4.1) uri @@ -423,7 +418,6 @@ DEPENDENCIES klogger-logger konfig-config (~> 3.0) mail - moonrope mysql2 nifty-utils nilify_blanks diff --git a/api/authenticator.rb b/api/authenticator.rb deleted file mode 100644 index a69b291fd..000000000 --- a/api/authenticator.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -authenticator :server do - friendly_name "Server Authenticator" - header "X-Server-API-Key", "The API token for a server that you wish to authenticate with.", example: "f29a45f0d4e1744ebaee" - error "InvalidServerAPIKey", "The API token provided in X-Server-API-Key was not valid.", attributes: { token: "The token that was looked up" } - error "ServerSuspended", "The mail server has been suspended" - lookup do - if key = request.headers["X-Server-API-Key"] - if credential = Credential.where(type: "API", key: key).first - if credential.server.suspended? - error "ServerSuspended" - else - credential.use - credential - end - else - error "InvalidServerAPIKey", token: key - end - end - end - rule :default, "AccessDenied", "Must be authenticated as a server." do - identity.is_a?(Credential) - end -end - -authenticator :anonymous do - rule :default, "MustNotBeAuthenticated", "Must not be authenticated." do - identity.nil? - end -end diff --git a/api/controllers/messages_api_controller.rb b/api/controllers/messages_api_controller.rb deleted file mode 100644 index f76cb0d21..000000000 --- a/api/controllers/messages_api_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -controller :messages do - friendly_name "Messages API" - description "This API allows you to access message details" - authenticator :server - - action :message do - title "Return message details" - description "Returns all details about a message" - param :id, "The ID of the message", type: Integer, required: true - returns Hash, structure: :message, structure_opts: { paramable: { expansions: false } } - error "MessageNotFound", "No message found matching provided ID", attributes: { id: "The ID of the message" } - action do - begin - message = identity.server.message(params.id) - rescue Postal::MessageDB::Message::NotFound - error "MessageNotFound", id: params.id - end - structure :message, message, return: true - end - end - - action :deliveries do - title "Return deliveries for a message" - description "Returns an array of deliveries which have been attempted for this message" - param :id, "The ID of the message", type: Integer, required: true - returns Array, structure: :delivery, structure_opts: { full: true } - error "MessageNotFound", "No message found matching provided ID", attributes: { id: "The ID of the message" } - action do - begin - message = identity.server.message(params.id) - rescue Postal::MessageDB::Message::NotFound - error "MessageNotFound", id: params.id - end - message.deliveries.map do |d| - structure :delivery, d - end - end - end -end diff --git a/api/controllers/send_api_controller.rb b/api/controllers/send_api_controller.rb deleted file mode 100644 index c22935f05..000000000 --- a/api/controllers/send_api_controller.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -controller :send do - friendly_name "Send API" - description "This API allows you to send messages" - authenticator :server - - action :message do - title "Send a message" - description "This action allows you to send a message by providing the appropriate options" - # Acceptable Parameters - param :to, "The e-mail addresses of the recipients (max 50)", type: Array - param :cc, "The e-mail addresses of any CC contacts (max 50)", type: Array - param :bcc, "The e-mail addresses of any BCC contacts (max 50)", type: Array - param :from, "The e-mail address for the From header", type: String - param :sender, "The e-mail address for the Sender header", type: String - param :subject, "The subject of the e-mail", type: String - param :tag, "The tag of the e-mail", type: String - param :reply_to, "Set the reply-to address for the mail", type: String - param :plain_body, "The plain text body of the e-mail", type: String - param :html_body, "The HTML body of the e-mail", type: String - param :attachments, "An array of attachments for this e-mail", type: Array - param :headers, "A hash of additional headers", type: Hash - param :bounce, "Is this message a bounce?", type: :boolean - # Errors - error "ValidationError", "The provided data was not sufficient to send an email", attributes: { errors: "A hash of error details" } - error "NoRecipients", "There are no recipients defined to receive this message" - error "NoContent", "There is no content defined for this e-mail" - error "TooManyToAddresses", "The maximum number of To addresses has been reached (maximum 50)" - error "TooManyCCAddresses", "The maximum number of CC addresses has been reached (maximum 50)" - error "TooManyBCCAddresses", "The maximum number of BCC addresses has been reached (maximum 50)" - error "FromAddressMissing", "The From address is missing and is required" - error "UnauthenticatedFromAddress", "The From address is not authorised to send mail from this server" - error "AttachmentMissingName", "An attachment is missing a name" - error "AttachmentMissingData", "An attachment is missing data" - # Return - returns Hash - # Action - action do - attributes = {} - attributes[:to] = params.to - attributes[:cc] = params.cc - attributes[:bcc] = params.bcc - attributes[:from] = params.from - attributes[:sender] = params.sender - attributes[:subject] = params.subject - attributes[:reply_to] = params.reply_to - attributes[:plain_body] = params.plain_body - attributes[:html_body] = params.html_body - attributes[:bounce] = params.bounce ? true : false - attributes[:tag] = params.tag - attributes[:custom_headers] = params.headers - attributes[:attachments] = [] - (params.attachments || []).each do |attachment| - next unless attachment.is_a?(Hash) - - attributes[:attachments] << { name: attachment["name"], content_type: attachment["content_type"], data: attachment["data"], base64: true } - end - message = OutgoingMessagePrototype.new(identity.server, request.ip, "api", attributes) - message.credential = identity - if message.valid? - result = message.create_messages - { message_id: message.message_id, messages: result } - else - error message.errors.first - end - end - end - - action :raw do - title "Send a raw RFC2822 message" - description "This action allows you to send us a raw RFC2822 formatted message along with the recipients that it should be sent to. This is similar to sending a message through our SMTP service." - param :mail_from, "The address that should be logged as sending the message", type: String, required: true - param :rcpt_to, "The addresses this message should be sent to", type: Array, required: true - param :data, "A base64 encoded RFC2822 message to send", type: String, required: true - param :bounce, "Is this message a bounce?", type: :boolean - returns Hash - error "UnauthenticatedFromAddress", "The From address is not authorised to send mail from this server" - action do - # Decode the raw message - raw_message = Base64.decode64(params.data) - - # Parse through mail to get the from/sender headers - mail = Mail.new(raw_message.split("\r\n\r\n", 2).first) - from_headers = { "from" => mail.from, "sender" => mail.sender } - authenticated_domain = identity.server.find_authenticated_domain_from_headers(from_headers) - - # If we're not authenticated, don't continue - if authenticated_domain.nil? - error "UnauthenticatedFromAddress" - end - - # Store the result ready to return - result = { message_id: nil, messages: {} } - params.rcpt_to.uniq.each do |rcpt_to| - message = identity.server.message_db.new_message - message.rcpt_to = rcpt_to - message.mail_from = params.mail_from - message.raw_message = raw_message - message.received_with_ssl = true - message.scope = "outgoing" - message.domain_id = authenticated_domain.id - message.credential_id = identity.id - message.bounce = params.bounce - message.save - result[:message_id] = message.message_id if result[:message_id].nil? - result[:messages][rcpt_to] = { id: message.id, token: message.token } - end - result - end - end -end diff --git a/api/structures/delivery_api_structure.rb b/api/structures/delivery_api_structure.rb deleted file mode 100644 index 69cf82d88..000000000 --- a/api/structures/delivery_api_structure.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -structure :delivery do - basic :id - basic :status - basic :details - basic :output, value: proc { o.output&.strip } - basic :sent_with_ssl, value: proc { o.sent_with_ssl } - basic :log_id - basic :time, value: proc { o.time&.to_f } - basic :timestamp, value: proc { o.timestamp.to_f } -end diff --git a/api/structures/message_api_structure.rb b/api/structures/message_api_structure.rb deleted file mode 100644 index 6cc3dcdab..000000000 --- a/api/structures/message_api_structure.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -structure :message do - basic :id - basic :token - - expansion(:status) do - { - status: o.status, - last_delivery_attempt: o.last_delivery_attempt&.to_f, - held: o.held, - hold_expiry: o.hold_expiry&.to_f - } - end - - expansion(:details) do - { - rcpt_to: o.rcpt_to, - mail_from: o.mail_from, - subject: o.subject, - message_id: o.message_id, - timestamp: o.timestamp.to_f, - direction: o.scope, - size: o.size, - bounce: o.bounce, - bounce_for_id: o.bounce_for_id, - tag: o.tag, - received_with_ssl: o.received_with_ssl - } - end - - expansion(:inspection) do - { - inspected: o.inspected, - spam: o.spam, - spam_score: o.spam_score.to_f, - threat: o.threat, - threat_details: o.threat_details - } - end - - expansion(:plain_body) { o.plain_body } - - expansion(:html_body) { o.html_body } - - expansion(:attachments) do - o.attachments.map do |attachment| - { - filename: attachment.filename.to_s, - content_type: attachment.mime_type, - data: Base64.encode64(attachment.body.to_s), - size: attachment.body.to_s.bytesize, - hash: Digest::SHA1.hexdigest(attachment.body.to_s) - } - end - end - - expansion(:headers) { o.headers } - - expansion(:raw_message) { Base64.encode64(o.raw_message) } - - expansion(:activity_entries) do - { - loads: o.loads, - clicks: o.clicks - } - end -end diff --git a/app/controllers/legacy_api/base_controller.rb b/app/controllers/legacy_api/base_controller.rb new file mode 100644 index 000000000..bc42d5454 --- /dev/null +++ b/app/controllers/legacy_api/base_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module LegacyAPI + class BaseController < ActionController::Base + + before_action :start_timer + before_action :authenticate_as_server + + private + + def start_timer + @start_time = Time.now.to_f + end + + def authenticate_as_server + key = request.headers["X-Server-API-Key"] + if key.blank? + render_error "AccessDenied", + message: "Must be authenticated as a server." + return + end + + credential = Credential.where(type: "API", key: key).first + if credential.nil? + render_error "InvalidServerAPIKey", + message: "The API token provided in X-Server-API-Key was not valid.", + token: key + return + end + + if credential.server.suspended? + render_error "ServerSuspended" + return + end + + credential.use + @current_credential = credential + end + + def render_success(data) + render json: { status: "success", + time: Time.now.to_f - @start_time, + flags: {}, + data: data } + end + + def render_error(code, data = {}) + render json: { status: "error", + time: Time.now.to_f - @start_time, + flags: {}, + data: data.merge(code: code) } + end + + def render_parameter_error(message) + render json: { status: "parameter-error", + time: Time.now.to_f - @start_time, + flags: {}, + data: { message: message } } + end + + end +end diff --git a/app/controllers/legacy_api/messages_controller.rb b/app/controllers/legacy_api/messages_controller.rb new file mode 100644 index 000000000..3463a6694 --- /dev/null +++ b/app/controllers/legacy_api/messages_controller.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module LegacyAPI + class MessagesController < BaseController + + def message + if params["id"].blank? + render_parameter_error "`id` parameter is required but is missing" + return + end + + message = @current_credential.server.message(params["id"]) + + message_hash = { id: message.id, token: message.token } + + expansions = params["_expansions"] + + if expansions.include?("status") + message_hash[:status] = { + status: message.status, + last_delivery_attempt: message.last_delivery_attempt&.to_f, + held: message.held, + hold_expiry: message.hold_expiry&.to_f + } + end + + if expansions.include?("details") + message_hash[:details] = { + rcpt_to: message.rcpt_to, + mail_from: message.mail_from, + subject: message.subject, + message_id: message.message_id, + timestamp: message.timestamp.to_f, + direction: message.scope, + size: message.size, + bounce: message.bounce, + bounce_for_id: message.bounce_for_id, + tag: message.tag, + received_with_ssl: message.received_with_ssl + } + end + + if expansions.include?("inspection") + message_hash[:inspection] = { + inspected: message.inspected, + spam: message.spam, + spam_score: message.spam_score.to_f, + threat: message.threat, + threat_details: message.threat_details + } + end + + if expansions.include?("plain_body") + message_hash[:plain_body] = message.plain_body + end + + if expansions.include?("html_body") + message_hash[:html_body] = message.html_body + end + + if expansions.include?("attachments") + message_hash[:attachments] = message.attachments.map do |attachment| + { + filename: attachment.filename.to_s, + content_type: attachment.mime_type, + data: Base64.encode64(attachment.body.to_s), + size: attachment.body.to_s.bytesize, + hash: Digest::SHA1.hexdigest(attachment.body.to_s) + } + end + end + + if expansions.include?("headers") + message_hash[:headers] = message.headers + end + + if expansions.include?("raw_message") + message_hash[:raw_message] = Base64.encode64(message.raw_message) + end + + if expansions.include?("activity_entries") + message_hash[:activity_entries] = { + loads: message.loads, + clicks: message.clicks + } + end + + render_success message_hash + rescue Postal::MessageDB::Message::NotFound + render_error "MessageNotFound", + message: "No message found matching provided ID", + id: params["id"] + end + + def deliveries + if params["id"].blank? + render_parameter_error "`id` parameter is required but is missing" + return + end + + message = @current_credential.server.message(params["id"]) + + deliveries = message.deliveries.map do |d| + { + id: d.id, + status: d.status, + details: d.details, + output: d.output&.strip, + sent_with_ssl: d.sent_with_ssl, + log_id: d.log_id, + time: d.time&.to_f, + timestamp: d.timestamp.to_f + } + end + + render_success deliveries + rescue Postal::MessageDB::Message::NotFound + render_error "MessageNotFound", + message: "No message found matching provided ID", + id: params["id"] + end + + end +end diff --git a/app/controllers/legacy_api/send_controller.rb b/app/controllers/legacy_api/send_controller.rb new file mode 100644 index 000000000..786225efb --- /dev/null +++ b/app/controllers/legacy_api/send_controller.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module LegacyAPI + class SendController < BaseController + + ERROR_MESSAGES = { + "NoRecipients" => "There are no recipients defined to receive this message", + "NoContent" => "There is no content defined for this e-mail", + "TooManyToAddresses" => "The maximum number of To addresses has been reached (maximum 50)", + "TooManyCCAddresses" => "The maximum number of CC addresses has been reached (maximum 50)", + "TooManyBCCAddresses" => "The maximum number of BCC addresses has been reached (maximum 50)", + "FromAddressMissing" => "The From address is missing and is required", + "UnauthenticatedFromAddress" => "The From address is not authorised to send mail from this server", + "AttachmentMissingName" => "An attachment is missing a name", + "AttachmentMissingData" => "An attachment is missing data" + }.freeze + + def message + attributes = {} + attributes[:to] = params["to"] + attributes[:cc] = params["cc"] + attributes[:bcc] = params["bcc"] + attributes[:from] = params["from"] + attributes[:sender] = params["sender"] + attributes[:subject] = params["subject"] + attributes[:reply_to] = params["reply_to"] + attributes[:plain_body] = params["plain_body"] + attributes[:html_body] = params["html_body"] + attributes[:bounce] = params["bounce"] ? true : false + attributes[:tag] = params["tag"] + attributes[:custom_headers] = params["headers"].to_unsafe_hash + attributes[:attachments] = [] + + (params["attachments"] || []).each do |attachment| + attachment = attachment.to_unsafe_hash if attachment.is_a?(ActionController::Parameters) + next unless attachment.is_a?(Hash) + + attributes[:attachments] << { name: attachment["name"], content_type: attachment["content_type"], data: attachment["data"], base64: true } + end + + message = OutgoingMessagePrototype.new(@current_credential.server, request.ip, "api", attributes) + message.credential = @current_credential + if message.valid? + result = message.create_messages + render_success message_id: message.message_id, messages: result + else + render_error message.errors.first, message: ERROR_MESSAGES[message.errors.first] + end + end + + def raw + unless params["rcpt_to"].is_a?(Array) + render_parameter_error "`rcpt_to` parameter is required but is missing" + return + end + + if params["mail_from"].blank? + render_parameter_error "`mail_from` parameter is required but is missing" + return + end + + if params["data"].blank? + render_parameter_error "`data` parameter is required but is missing" + return + end + + # Decode the raw message + raw_message = Base64.decode64(params["data"]) + + # Parse through mail to get the from/sender headers + mail = Mail.new(raw_message.split("\r\n\r\n", 2).first) + from_headers = { "from" => mail.from, "sender" => mail.sender } + authenticated_domain = @current_credential.server.find_authenticated_domain_from_headers(from_headers) + + # If we're not authenticated, don't continue + if authenticated_domain.nil? + render_error "UnauthenticatedFromAddress" + return + end + + # Store the result ready to return + result = { message_id: nil, messages: {} } + if params["rcpt_to"].is_a?(Array) + params["rcpt_to"].uniq.each do |rcpt_to| + message = @current_credential.server.message_db.new_message + message.rcpt_to = rcpt_to + message.mail_from = params["mail_from"] + message.raw_message = raw_message + message.received_with_ssl = true + message.scope = "outgoing" + message.domain_id = authenticated_domain.id + message.credential_id = @current_credential.id + message.bounce = params["bounce"] ? true : false + message.save + result[:message_id] = message.message_id if result[:message_id].nil? + result[:messages][rcpt_to] = { id: message.id, token: message.token } + end + end + render_success result + end + + end +end diff --git a/config/routes.rb b/config/routes.rb index aa6e20ccc..10c7e666d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true Rails.application.routes.draw do + # Legacy API Routes + match "/api/v1/send/message" => "legacy_api/send#message", via: [:get, :post] + match "/api/v1/send/raw" => "legacy_api/send#raw", via: [:get, :post] + match "/api/v1/messages/message" => "legacy_api/messages#message", via: [:get, :post] + match "/api/v1/messages/deliveries" => "legacy_api/messages#deliveries", via: [:get, :post] + scope "org/:org_permalink", as: "organization" do resources :domains, only: [:index, :new, :create, :destroy] do match :verify, on: :member, via: [:get, :post] diff --git a/script/generate_api_docs.sh b/script/generate_api_docs.sh deleted file mode 100755 index ad1c68e0f..000000000 --- a/script/generate_api_docs.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -set -e - -if [ ! -d /tmp/postal-api/.git ]; -then - git clone git@github.com:atech/postal-api /tmp/postal-api -else - git -C /tmp/postal-api reset --hard HEAD - git -C /tmp/postal-api pull origin master -fi - -rm -Rf /tmp/postal-api/* - -bundle exec moonrope api /tmp/postal-api - -cd /tmp/postal-api - -git add . -git commit -m "update docs" -git push origin master diff --git a/spec/apis/legacy_api/send/message_spec.rb b/spec/apis/legacy_api/send/message_spec.rb index da3855ca8..af33e6a08 100644 --- a/spec/apis/legacy_api/send/message_spec.rb +++ b/spec/apis/legacy_api/send/message_spec.rb @@ -40,192 +40,195 @@ let(:server) { create(:server) } let(:credential) { create(:credential, server: server) } let(:domain) { create(:domain, owner: server) } - let(:default_params) do - { - to: ["test@example.com"], - cc: ["cc@example.com"], - bcc: ["bcc@example.com"], - from: "test@#{domain.name}", - sender: "sender@#{domain.name}", - tag: "test-tag", - reply_to: "reply@example.com", - plain_body: "plain text", - html_body: "

html

", - attachments: [{ name: "test1.txt", content_type: "text/plain", data: Base64.encode64("hello world 1") }, - { name: "test2.txt", content_type: "text/plain", data: Base64.encode64("hello world 2") },], - headers: { "x-test-header-1" => "111", "x-test-header-2" => "222" }, - bounce: false, - subject: "Test" - } - end - let(:params) { default_params } - - before do - post "/api/v1/send/message", - headers: { "x-server-api-key" => credential.key, - "content-type" => "application/json" }, - params: params.to_json - end - context "when no recipients are provided" do - let(:params) { default_params.merge(to: [], cc: [], bcc: []) } + context "when parameters are provided in a JSON body" do + let(:default_params) do + { + to: ["test@example.com"], + cc: ["cc@example.com"], + bcc: ["bcc@example.com"], + from: "test@#{domain.name}", + sender: "sender@#{domain.name}", + tag: "test-tag", + reply_to: "reply@example.com", + plain_body: "plain text", + html_body: "

html

", + attachments: [{ name: "test1.txt", content_type: "text/plain", data: Base64.encode64("hello world 1") }, + { name: "test2.txt", content_type: "text/plain", data: Base64.encode64("hello world 2") },], + headers: { "x-test-header-1" => "111", "x-test-header-2" => "222" }, + bounce: false, + subject: "Test" + } + end + let(:params) { default_params } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "NoRecipients" - expect(parsed_body["data"]["message"]).to match(/there are no recipients defined to receive this message/i) + before do + post "/api/v1/send/message", + headers: { "x-server-api-key" => credential.key, + "content-type" => "application/json" }, + params: params.to_json end - end - context "when no content is provided" do - let(:params) { default_params.merge(html_body: nil, plain_body: nil) } + context "when no recipients are provided" do + let(:params) { default_params.merge(to: [], cc: [], bcc: []) } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "NoContent" - expect(parsed_body["data"]["message"]).to match(/there is no content defined for this e-mail/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "NoRecipients" + expect(parsed_body["data"]["message"]).to match(/there are no recipients defined to receive this message/i) + end end - end - context "when the number of 'To' recipients exceeds the maximum" do - let(:params) { default_params.merge(to: ["a@a.com"] * 51) } + context "when no content is provided" do + let(:params) { default_params.merge(html_body: nil, plain_body: nil) } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "TooManyToAddresses" - expect(parsed_body["data"]["message"]).to match(/the maximum number of To addresses has been reached/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "NoContent" + expect(parsed_body["data"]["message"]).to match(/there is no content defined for this e-mail/i) + end end - end - context "when the number of 'CC' recipients exceeds the maximum" do - let(:params) { default_params.merge(cc: ["a@a.com"] * 51) } + context "when the number of 'To' recipients exceeds the maximum" do + let(:params) { default_params.merge(to: ["a@a.com"] * 51) } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "TooManyCCAddresses" - expect(parsed_body["data"]["message"]).to match(/the maximum number of CC addresses has been reached/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "TooManyToAddresses" + expect(parsed_body["data"]["message"]).to match(/the maximum number of To addresses has been reached/i) + end end - end - context "when the number of 'BCC' recipients exceeds the maximum" do - let(:params) { default_params.merge(bcc: ["a@a.com"] * 51) } + context "when the number of 'CC' recipients exceeds the maximum" do + let(:params) { default_params.merge(cc: ["a@a.com"] * 51) } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "TooManyBCCAddresses" - expect(parsed_body["data"]["message"]).to match(/the maximum number of BCC addresses has been reached/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "TooManyCCAddresses" + expect(parsed_body["data"]["message"]).to match(/the maximum number of CC addresses has been reached/i) + end end - end - context "when the 'From' address is missing" do - let(:params) { default_params.merge(from: nil) } + context "when the number of 'BCC' recipients exceeds the maximum" do + let(:params) { default_params.merge(bcc: ["a@a.com"] * 51) } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "FromAddressMissing" - expect(parsed_body["data"]["message"]).to match(/the from address is missing and is required/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "TooManyBCCAddresses" + expect(parsed_body["data"]["message"]).to match(/the maximum number of BCC addresses has been reached/i) + end end - end - context "when the 'From' address is not authorised" do - let(:params) { default_params.merge(from: "test@another.com") } + context "when the 'From' address is missing" do + let(:params) { default_params.merge(from: nil) } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "UnauthenticatedFromAddress" - expect(parsed_body["data"]["message"]).to match(/the from address is not authorised to send mail from this server/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "FromAddressMissing" + expect(parsed_body["data"]["message"]).to match(/the from address is missing and is required/i) + end end - end - context "when an attachment is missing a name" do - let(:params) { default_params.merge(attachments: [{ name: nil, content_type: "text/plain", data: Base64.encode64("hello world 1") }]) } + context "when the 'From' address is not authorised" do + let(:params) { default_params.merge(from: "test@another.com") } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "AttachmentMissingName" - expect(parsed_body["data"]["message"]).to match(/an attachment is missing a name/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "UnauthenticatedFromAddress" + expect(parsed_body["data"]["message"]).to match(/the from address is not authorised to send mail from this server/i) + end end - end - context "when an attachment is missing data" do - let(:params) { default_params.merge(attachments: [{ name: "test1.txt", content_type: "text/plain", data: nil }]) } + context "when an attachment is missing a name" do + let(:params) { default_params.merge(attachments: [{ name: nil, content_type: "text/plain", data: Base64.encode64("hello world 1") }]) } - it "returns an error" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "error" - expect(parsed_body["data"]["code"]).to eq "AttachmentMissingData" - expect(parsed_body["data"]["message"]).to match(/an attachment is missing data/i) + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "AttachmentMissingName" + expect(parsed_body["data"]["message"]).to match(/an attachment is missing a name/i) + end end - end - context "when an attachment entry is not a hash" do - let(:params) { default_params.merge(attachments: [123, "string"]) } + context "when an attachment is missing data" do + let(:params) { default_params.merge(attachments: [{ name: "test1.txt", content_type: "text/plain", data: nil }]) } - it "continues as if it wasn't there" do - parsed_body = JSON.parse(response.body) - ["test@example.com", "cc@example.com", "bcc@example.com"].each do |rcpt_to| - message_id = parsed_body["data"]["messages"][rcpt_to]["id"] - message = server.message(message_id) - expect(message.attachments).to be_empty + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "AttachmentMissingData" + expect(parsed_body["data"]["message"]).to match(/an attachment is missing data/i) end end - end - context "when given a complete email to send" do - it "returns details of the messages created" do - parsed_body = JSON.parse(response.body) - expect(parsed_body["status"]).to eq "success" - expect(parsed_body["data"]["messages"]).to match({ - "test@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }, - "cc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }, - "bcc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ } - }) - end + context "when an attachment entry is not a hash" do + let(:params) { default_params.merge(attachments: [123, "string"]) } - it "adds an appropriate received header" do - parsed_body = JSON.parse(response.body) - message_id = parsed_body["data"]["messages"]["test@example.com"]["id"] - message = server.message(message_id) - expect(message.headers["received"].first).to match(/\Afrom api/) + it "continues as if it wasn't there" do + parsed_body = JSON.parse(response.body) + ["test@example.com", "cc@example.com", "bcc@example.com"].each do |rcpt_to| + message_id = parsed_body["data"]["messages"][rcpt_to]["id"] + message = server.message(message_id) + expect(message.attachments).to be_empty + end + end end - it "creates appropriate message objects" do - parsed_body = JSON.parse(response.body) - ["test@example.com", "cc@example.com", "bcc@example.com"].each do |rcpt_to| - message_id = parsed_body["data"]["messages"][rcpt_to]["id"] + context "when given a complete email to send" do + it "returns details of the messages created" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "success" + expect(parsed_body["data"]["messages"]).to match({ + "test@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }, + "cc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }, + "bcc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ } + }) + end + + it "adds an appropriate received header" do + parsed_body = JSON.parse(response.body) + message_id = parsed_body["data"]["messages"]["test@example.com"]["id"] message = server.message(message_id) - expect(message).to have_attributes( - server: server, - rcpt_to: rcpt_to, - mail_from: params[:from], - subject: params[:subject], - message_id: kind_of(String), - timestamp: kind_of(Time), - domain_id: domain.id, - credential_id: credential.id, - bounce: false, - tag: params[:tag], - headers: hash_including("x-test-header-1" => ["111"], - "x-test-header-2" => ["222"], - "sender" => [params[:sender]], - "to" => ["test@example.com"], - "cc" => ["cc@example.com"], - "reply-to" => ["reply@example.com"]), - plain_body: params[:plain_body], - html_body: params[:html_body], - attachments: [ - have_attributes(content_type: /\Atext\/plain/, filename: "test1.txt", body: have_attributes(to_s: "hello world 1")), - have_attributes(content_type: /\Atext\/plain/, filename: "test2.txt", body: have_attributes(to_s: "hello world 2")), - ] - ) + expect(message.headers["received"].first).to match(/\Afrom api/) + end + + it "creates appropriate message objects" do + parsed_body = JSON.parse(response.body) + ["test@example.com", "cc@example.com", "bcc@example.com"].each do |rcpt_to| + message_id = parsed_body["data"]["messages"][rcpt_to]["id"] + message = server.message(message_id) + expect(message).to have_attributes( + server: server, + rcpt_to: rcpt_to, + mail_from: params[:from], + subject: params[:subject], + message_id: kind_of(String), + timestamp: kind_of(Time), + domain_id: domain.id, + credential_id: credential.id, + bounce: false, + tag: params[:tag], + headers: hash_including("x-test-header-1" => ["111"], + "x-test-header-2" => ["222"], + "sender" => [params[:sender]], + "to" => ["test@example.com"], + "cc" => ["cc@example.com"], + "reply-to" => ["reply@example.com"]), + plain_body: params[:plain_body], + html_body: params[:html_body], + attachments: [ + have_attributes(content_type: /\Atext\/plain/, filename: "test1.txt", body: have_attributes(to_s: "hello world 1")), + have_attributes(content_type: /\Atext\/plain/, filename: "test2.txt", body: have_attributes(to_s: "hello world 2")), + ] + ) + end end end end