demarches-normaliennes/app/lib/dolist/api.rb

266 lines
8.2 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

require "support/jsv"
class Dolist::API
CONTACT_URL = "https://apiv9.dolist.net/v1/contacts/read?AccountID=%{account_id}"
EMAIL_LOGS_URL = "https://apiv9.dolist.net/v1/statistics/email/sendings/transactional/search?AccountID=%{account_id}"
EMAIL_KEY = 7
DOLIST_WEB_DASHBOARD = "https://campaign.dolist.net/#/%{account_id}/contacts/%{contact_id}/sendings"
EMAIL_MESSAGES_ADRESSES_REPLIES = "https://apiv9.dolist.net/v1/email/messages/addresses/replies?AccountID=%{account_id}"
EMAIL_MESSAGES_ADRESSES_PACKSENDERS = "https://apiv9.dolist.net/v1/email/messages/addresses/packsenders?AccountID=%{account_id}"
EMAIL_SENDING_TRANSACTIONAL = "https://apiv9.dolist.net/v1/email/sendings/transactional?AccountID=%{account_id}"
EMAIL_SENDING_TRANSACTIONAL_ATTACHMENT = "https://apiv9.dolist.net/v1/email/sendings/transactional/attachment?AccountID=%{account_id}"
EMAIL_SENDING_TRANSACTIONAL_SEARCH = "https://apiv9.dolist.net/v1/email/sendings/transactional/search?AccountID=%{account_id}"
class_attribute :limit_remaining, :limit_reset_at
class << self
def save_rate_limit_headers(headers)
self.limit_remaining = headers["X-Rate-Limit-Remaining"].to_i
self.limit_reset_at = Time.zone.at(headers["X-Rate-Limit-Reset"].to_i / 1_000)
end
def near_rate_limit?
return if limit_remaining.nil?
limit_remaining < 20 # keep 20 requests for non background API calls
end
def sleep_until_limit_reset
return if limit_reset_at.nil? || limit_reset_at.past?
sleep (limit_reset_at - Time.zone.now).ceil
end
def sendable?(mail)
return false if mail.to.blank? # recipient are mandatory
return false if mail.bcc.present? # no bcc support
# Mail having attachments are not yet supported in our account
mail.attachments.none? { !_1.inline? }
end
end
def properly_configured?
client_key.present?
end
def send_email(mail)
if mail.attachments.any? { !_1.inline? }
return send_email_with_attachment(mail)
end
body = { "TransactionalSending": prepare_mail_body(mail) }
url = format_url(EMAIL_SENDING_TRANSACTIONAL)
post(url, body.to_json)
end
def send_email_with_attachment(mail)
uri = URI(format_url(EMAIL_SENDING_TRANSACTIONAL_ATTACHMENT))
request = Net::HTTP::Post.new(uri)
default_headers.each do |key, value|
next if key.to_s == "Content-Type"
request[key] = value
end
boundary = "---011000010111000001101001" # any random string not present in the body
request.content_type = "multipart/form-data; boundary=#{boundary}"
body = "--#{boundary}\r\n"
base64_files(mail.attachments).each do |file|
body << "Content-Disposition: form-data; name=\"#{file.field_name}\"; filename=\"#{file.filename}\"\r\n"
body << "Content-Type: #{file.mime_type}\r\n"
body << "\r\n"
body << file.content
body << "\r\n"
end
body << "\r\n--#{boundary}\r\n"
body << "Content-Disposition: form-data; name=\"TransactionalSending\"\r\n"
body << "Content-Type: text/plain; charset=utf-8\r\n"
body << "\r\n"
body << prepare_mail_body(mail).to_jsv
body << "\r\n--#{boundary}--\r\n"
body << "\r\n"
request.body = body
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
response = http.request(request)
if response.body.empty?
fail "Dolist API returned an empty response"
else
JSON.parse(response.body)
end
end
def sent_mails(email_address)
contact_id = fetch_contact_id(email_address)
if contact_id.nil?
Rails.logger.info "Dolist::API: no contact found for email address '#{email_address}'"
return []
end
dolist_messages = fetch_dolist_messages(contact_id)
dolist_messages.map { |m| to_sent_mail(email_address, contact_id, m) }
rescue StandardError => e
Rails.logger.error e.message
[]
end
def senders
get format_url(EMAIL_MESSAGES_ADRESSES_PACKSENDERS)
end
def replies
get format_url(EMAIL_MESSAGES_ADRESSES_REPLIES)
end
private
def format_url(base)
format(base, account_id: account_id)
end
def sender_id
Rails.cache.fetch("dolist_api_sender_id", expires_in: 1.hour) do
senders.dig("ItemList", 0, "Sender", "ID")
end
end
def get(url)
response = Typhoeus.get(url, headers: default_headers).tap do
self.class.save_rate_limit_headers(_1.headers)
end
JSON.parse(response.response_body)
end
def post(url, body)
response = Typhoeus.post(url, body:, headers: default_headers).tap do
self.class.save_rate_limit_headers(_1.headers)
end
if response.response_body.empty?
fail "Empty response from Dolist API"
else
JSON.parse(response.response_body)
end
end
def default_headers
{
"Content-Type": 'application/json',
"Accept": 'application/json',
"X-API-Key": client_key
}
end
def client_key
Rails.application.secrets.dolist[:api_key]
end
def account_id
Rails.application.secrets.dolist[:account_id]
end
# https://api.dolist.com/documentation/index.html#/b3A6Mzg0MTQ0MDc-rechercher-un-contact
def fetch_contact_id(email_address)
url = format(CONTACT_URL, account_id: account_id)
body = {
Query: { FieldValueList: [{ ID: EMAIL_KEY, Value: email_address }] }
}.to_json
post(url, body)["ID"]
end
# https://api.dolist.com/documentation/index.html#/b3A6Mzg0MTQ4MDk-recuperer-les-statistiques-des-envois-pour-un-contact
def fetch_dolist_messages(contact_id)
url = format(EMAIL_LOGS_URL, account_id: account_id)
body = { SearchQuery: { ContactID: contact_id } }.to_json
post(url, body)["ItemList"]
end
# see: https://api.dolist.com/documentation/index.html#/7edc2948ba01f-creer-un-envoi-transactionnel
def prepare_mail_body(mail)
{
"Type": "TransactionalService",
"Contact": {
"FieldList": [
{
"ID": EMAIL_KEY,
"Value": mail.to.first
}
]
},
"Message": {
"Name": mail['X-Dolist-Message-Name'].value,
"Subject": mail.subject,
"SenderID": sender_id,
"ForceHttp": false, # ForceHttp : force le tracking http non sécurisé (True/False).
"Format": "html",
"DisableOpenTracking": true, # DisableOpenTracking : désactivation du tracking d'ouverture (True/False).
"IsTrackingValidated": true # IsTrackingValidated : est-ce que le tracking de ce message est validé ? (True/False). Passez la valeur True pour un envoi transactionnel.
},
"MessageContent": {
"SourceCode": mail_source_code(mail),
"EncodingType": "UTF8",
"EnableTrackingDetection": false # EnableTrackingDetection : booléen pour lactivation du tracking personnalisé des liens des messages utilisés lors des précédents envois (True/False).
}
}
end
def to_sent_mail(email_address, contact_id, dolist_message)
SentMail.new(
from: ENV['DOLIST_NO_REPLY_EMAIL'],
to: email_address,
subject: dolist_message['SendingName'],
delivered_at: Time.zone.parse(dolist_message['SendDate']),
status: status(dolist_message),
service_name: 'Dolist',
external_url: format(DOLIST_WEB_DASHBOARD, account_id: account_id, contact_id: contact_id)
)
end
def status(dolist_message)
case dolist_message.fetch_values('Status', 'IsDelivered')
in ['Sent', true]
"delivered"
in ['Sent', false]
"sent (delivered ?)"
in [status, _]
status
end
end
def mail_source_code(mail)
if mail.html_part.nil? && mail.text_part.nil?
mail.decoded
else
mail.html_part.body.decoded
end
end
def base64_files(attachments)
attachments.map do |attachment|
raise ArgumentError, "Dolist API does not support non PDF attachments. Given #{attachment.filename} which has mime_type=#{attachment.mime_type}" unless attachment.mime_type == "application/pdf"
field_name = File.basename(attachment.filename, File.extname(attachment.filename))
attachment_content = attachment.body.decoded
attachment_base64 = Base64.strict_encode64(attachment_content)
Dolist::Base64File.new(field_name:, filename: attachment.filename, mime_type: attachment.mime_type, content: attachment_base64)
end
end
end