2023-02-02 11:18:10 +01:00
require " support/jsv "
2022-04-26 11:44:42 +02:00
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 "
2023-02-02 14:50:26 +01:00
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} "
2023-02-02 11:18:10 +01:00
EMAIL_SENDING_TRANSACTIONAL_ATTACHMENT = " https://apiv9.dolist.net/v1/email/sendings/transactional/attachment?AccountID=%{account_id} "
2023-02-02 14:50:26 +01:00
EMAIL_SENDING_TRANSACTIONAL_SEARCH = " https://apiv9.dolist.net/v1/email/sendings/transactional/search?AccountID=%{account_id} "
2022-04-26 11:44:42 +02:00
2023-01-16 23:42:26 +01:00
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
2023-02-02 16:11:52 +01:00
def sendable? ( mail )
2023-02-07 16:51:44 +01:00
return false if mail . to . blank? # recipient are mandatory
return false if mail . bcc . present? # no bcc support
2023-02-02 16:11:52 +01:00
# Mail having attachments are not yet supported in our account
mail . attachments . none? { ! _1 . inline? }
end
2023-01-16 23:42:26 +01:00
end
2022-04-26 11:44:42 +02:00
def properly_configured?
client_key . present?
end
2023-02-02 14:50:26 +01:00
def send_email ( mail )
2023-02-02 11:18:10 +01:00
if mail . attachments . any? { ! _1 . inline? }
return send_email_with_attachment ( mail )
end
2023-02-02 14:50:26 +01:00
2023-02-02 11:18:10 +01:00
body = { " TransactionalSending " : prepare_mail_body ( mail ) }
2023-02-02 14:50:26 +01:00
2023-02-02 11:18:10 +01:00
url = format_url ( EMAIL_SENDING_TRANSACTIONAL )
2023-02-02 14:50:26 +01:00
post ( url , body . to_json )
end
2023-02-02 11:18:10 +01:00
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
2022-04-26 11:44:42 +02:00
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
2023-02-02 14:50:26 +01:00
def senders
2023-02-02 11:18:10 +01:00
get format_url ( EMAIL_MESSAGES_ADRESSES_PACKSENDERS )
2023-02-02 14:50:26 +01:00
end
def replies
2023-02-02 11:18:10 +01:00
get format_url ( EMAIL_MESSAGES_ADRESSES_REPLIES )
2023-02-02 14:50:26 +01:00
end
2022-04-26 11:44:42 +02:00
private
2023-02-02 11:18:10 +01:00
def format_url ( base )
format ( base , account_id : account_id )
end
2023-02-02 14:50:26 +01:00
def sender_id
2023-02-02 16:02:27 +01:00
Rails . cache . fetch ( " dolist_api_sender_id " , expires_in : 1 . hour ) do
senders . dig ( " ItemList " , 0 , " Sender " , " ID " )
end
2023-02-02 14:50:26 +01:00
end
def get ( url )
2023-02-02 11:18:10 +01:00
response = Typhoeus . get ( url , headers : default_headers ) . tap do
2023-02-02 14:50:26 +01:00
self . class . save_rate_limit_headers ( _1 . headers )
end
JSON . parse ( response . response_body )
end
def post ( url , body )
2023-02-02 11:18:10 +01:00
response = Typhoeus . post ( url , body : , headers : default_headers ) . tap do
2023-02-02 14:50:26 +01:00
self . class . save_rate_limit_headers ( _1 . headers )
end
2023-02-02 11:18:10 +01:00
if response . response_body . empty?
fail " Empty response from Dolist API "
else
JSON . parse ( response . response_body )
end
2023-02-02 14:50:26 +01:00
end
2023-02-02 11:18:10 +01:00
def default_headers
2022-04-26 11:44:42 +02:00
{
" 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
2023-01-16 23:42:26 +01:00
post ( url , body ) [ " ID " ]
2022-04-26 11:44:42 +02:00
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
2023-01-16 23:42:26 +01:00
post ( url , body ) [ " ItemList " ]
end
2023-02-02 11:18:10 +01:00
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 " : true ,
" Format " : " html " ,
" DisableOpenTracking " : true ,
" IsTrackingValidated " : true
} ,
" MessageContent " : {
" SourceCode " : mail_source_code ( mail ) ,
" EncodingType " : " UTF8 " ,
" EnableTrackingDetection " : false
}
}
end
2022-04-26 11:44:42 +02:00
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
2023-02-02 14:50:26 +01:00
def mail_source_code ( mail )
if mail . html_part . nil? && mail . text_part . nil?
mail . decoded
else
mail . html_part . body . decoded
end
end
2023-02-02 11:18:10 +01:00
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
2022-04-26 11:44:42 +02:00
end