The application goes offline when trying to send more than thousands of emails in Rails with AWS SES - ruby-on-rails

I have implemented a platform using rails, and the goal is to send thousands of emails to customers with one click. The concept is that an email array runs each loop and inside each loop runs send email functionality like below.
#emails = ['abc#gmai.com', 'abc#example.com'] # More than 3 thousands
#emails.each do |email|
aws_email_sender(email, #email_subject, #email_body_html)
end
And the email function is like below:
def aws_email_sender(recipient, subject, htmlbody)
sender = "hello#example.com"
awsregion = "ap-west-1"
# The HTML body of the email.
htmlbodycontent = "#{htmlbody}"
# The email body for recipients with non-HTML email clients.
textbody = "This email was sent with Amazon SES using the AWS SDK for Ruby."
# Specify the text encoding scheme.
encoding = "UTF-8"
# Create a new SES resource and specify a region
ses = Aws::SES::Client.new(region: awsregion)
# Try to send the email.
begin
# Provide the contents of the email.
resp = ses.send_email({
destination: {
to_addresses: [recipient]
},
message: {
body: {
html: {
charset: encoding,
data: htmlbodycontent
},
text: {
charset: encoding,
data: textbody,
},
},
subject: {
charset: encoding,
data: subject,
},
},
source: sender,
});
# If something goes wrong, display an error message.
rescue Aws::SES::Errors::ServiceError => error
puts "Email not sent. Error message: #{error}"
end
end
The email is sending well by AWS but my rails application has gone down like
A timeout occurred, error code 524
I couldn't get the breaking point, why has my application gone down every time?
Thanks in Advance

If 524 is an HTTP status code then it means...
Cloudflare was able to make a TCP connection to the website behind them, but it did not reply with an HTTP response before the connection timed out.
Meaning your Rails app is behind a Cloudflare proxy. Cloudflare received an HTTP request, forwarded it to your app, waited around for your app to respond, but your app never did. A more detailed explanation can be found here.
Probably because it's trying to send emails to 3000 people one-by-one.
There's two strategies to fix this.
Use Bulk Email
Since the content of the email is the same for everyone, use an email template to send bulk email using the #send_bulk_templated_email method.
You can send to up to 50 addresses at a time, so use #each_slice to loop through emails in slices of 50.
This will be more efficient, but your app will still be waiting around for 3000/50 = 60 AWS API calls. At worst it will still time out. At best the user will be waiting around for a form submission.
Use A Background Job
Anytime your app needs to do something that might take a lot of time, like using a service or a large database query, consider putting it into a background job. The Rails app queues up a job to send the emails, and then it can respond to the web request while the mailing is handled in the background. This has other advantages: errors calling the service won't cause an error for the user, and failed jobs due to a temporary service outage can automatically be retried.
In Rails this is done with ActiveJob and you could write a job class to send your mail.
Use ActionMailer
However, Rails also offers a class specifically for sending email in the background: ActionMailer. You can have ActionMailer use AWS with the aws-sdk-rails gem.
config.action_mailer.delivery_method = :ses

Related

AWS-SDK-SES: How Can I Check the Existence of an Email Address in Rails?

I currently have Rails applications that use SES to send emails. Unfortunately no matter how much code I put in my application I still get emails with invalid email addresses.
I want to use AWS to verify if I have a valid email address, meaning that the syntax is correct and does other verification like checking if the mailbox exists.
I installed the aws-sdk-rails gem in my application. I added my access_key_id and secret_access_key to config/credentials.yml.enc.
I added the following code from this awsdocs/aws-doc-sdk-examples GitHub repository to my contact form and made minor changes.
require 'aws-sdk-ses'
# Replace recipient#example.com with a "To" address.
recipient = params[:email]
error = " "
# Create a new SES resource in the us-west-2 region.
# Replace us-west-2 with the AWS Region you're using for Amazon SES.
ses = Aws::SES::Client.new(region: 'us-west-2')
# Try to verify email address.
begin
ses.verify_email_identity({
email_address: recipient
})
puts 'Email sent to ' + recipient
# If something goes wrong, display an error message.
rescue Aws::SES::Errors::ServiceError => error
puts "Email not sent. Error message: #{error}"
end
I entered an email address that AWS SES said that the mailbox didn't exist this morning. However when I ran this code it didn't produce an error as I thought it might. When I checked the Rails debug log error was blank. The region in my code is the one that I use to successfully send transactional emails from all my websites.
I couldn't find any documentation about that code that says how much verification it does for the email address.
Can I use this gem to find if email addresses exist or have other problems like SES checks when an email has a To: email address?

Is there a way to get around H12 timeouts in Heroku using griddler to parse inbound messages with large attachments from sendgrid

My workflow is email--sendgrid--griddler and the rails app runs on heroku. All inbound emails have attachments, and some are rather large. I keep getting H12 timeouts on Heroku because the attachments take more than 30s to upload to Google Cloud Storage.
I have used delayed job for everything I can, but I dont THINK I can pass an attachment from griddler to a delayed job, since the attachment is ephemeral. I had a friend suggest just move to retrieving the emails from gmail instead of using sendgrid and griddler, but that would be more of a rewrite than I am up for at the moment. In an ideal world, I would be able to pass the attachments to a delayed job, but I dont know if that is possible in the end.
email_processor.rb
if pdfs = attachments.select { |attachment| attachment.content_type == 'application/pdf' }
pdfs.each do |att|
# att is a ActionDispatch::Http::UploadedFile type
# content_type = MIME::Types.type_for(att.to_path).first.content_type
content_type = att.content_type
if content_type == 'application/pdf'
# later if we use multiple attachments in single fax, change num_pages
fax = #email_address.phone_number.faxes.create(sending_email: #email_address.address ,twilio_to: result, twilio_from: #email_address.phone_number.number, twilio_num_pages: 1)
attachment = fax.create_attachment
# next two rows should be with delay
attachment.upload_file!(att)
#moved to attachment model for testing
#fax.send!
end
end
file upload from another model
def upload_file!(file)
# file should be ActionDispatch::Http::UploadedFile type
filename = file.original_filename.gsub(/\s+/, '_')
filename = filename[0..15] if filename.size > 16
path = "fax/#{fax.id}/att-#{id}-#{filename}"
upload_out!(file.open, path)
#self.fax.send!
#make_thumbnail_pdf!(file.open)
end
def upload_out!(file, path)
upload = StorageBucket.files.new key: path, body: file, public: true
upload.save # upload file
update_columns url: upload.public_url
self.fax.update(status: 'outbound-uploaded')
self.fax.process!
end
If you cannot receive and upload the attachment in 30 seconds then heroku won't work for receiving emails. You are right - the ephemeral storage on the web dyno is not accessible from the worker dyno running delayed job.
Even if it were possible for the worker dyno to read data from the web dyno's ephemeral storage there is no guarantee the web dyno could handle the POST from sendgrid in 30 seconds if the attachments were large enough.
One option is to configure sendgrid to forward the emails directly to your google app engine - https://cloud.google.com/appengine/docs/standard/python/mail/receiving-mail-with-mail-api
Your app engine script could write the attachments into google cloud storage, and then your app engine script could do a POST to your heroku app with the location of the attachment and the web app could then queue a delayed job to download and handle the attachment.
I ended up just rewriting my email processing completely. I setup gmail as mail target, and then used a scheduled task at Heroku to process the emails(look for unread) and then upload the attachments to Google Cloud Storage. Using a scheduled task gets around the H12 issues.

How do I get the JSON response from Dialogflow with Rails?

I understand the whole process of dialogflow and I have a working deployed bot with 2 different intents. How do I actually get the response from the bot when a user answers questions? (I set the bot on fulfillment to go to my domain). Using rails 5 app and it's deployed with Heroku.
Thanks!
If you have already set the GOOGLE_APPLICATION_CREDENTIALS path to the jso file, now you can test using a ruby script.
Create a ruby file -> ex: chatbot.rb
Write the code bellow in the file.
project_id = "Your Google Cloud project ID"
session_id = "mysession"
texts = ["hello"]
language_code = "en-US"
require "google/cloud/dialogflow"
session_client = Google::Cloud::Dialogflow::Sessions.new
session = session_client.class.session_path project_id, session_id
puts "Session path: #{session}"
texts.each do |text|
query_input = { text: { text: text, language_code: language_code } }
response = session_client.detect_intent session, query_input
query_result = response.query_result
puts "Query text: #{query_result.query_text}"
puts "Intent detected: #{query_result.intent.display_name}"
puts "Intent confidence: #{query_result.intent_detection_confidence}"
puts "Fulfillment text: #{query_result.fulfillment_text}\n"
end
Insert your project_id. You can find this information on your agent on Dialogflow. Click on the gear on the right side of the Agent's name in the left menu.
Run the ruby file in the terminal or in whatever you using to run ruby files. Then you see the bot replying to the "hello" message you have sent.
Obs: Do not forget to install the google-cloud gem:
Not Entirely familiar with Dilogflow, but if you want to receive a response when an action occurs on another app this usually mean you need to receive web-hooks from them
A WebHook is an HTTP callback: an HTTP POST that occurs when something happens; a simple event-notification via HTTP POST. A web application implementing WebHooks will POST a message to a URL when certain things happen.
I would recommend checking their fulfillment documentation for an example. Hope this helps you out.

The stratigy of build a talk-to-talk system using em-websocket in rails?

Maybe it is a good example for server push system. There are many users in the system, and users can talk with each other. It can be accomplished like this: one user sends message(through websocket) to the server, then the server forward the message to the other user. The key is to find the binding between the ws(websocket object) and the user. The example code like below:
EM.run {
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws|
ws.onopen { |handshake|
# extract the user id from handshake and store the binding between user and ws
}
ws.onmessage { |msg|
# extract the text and receiver id from msg
# extract the ws_receiver from the binding
ws_receiver.send(text)
}
end
}
I want to figure out following issues:
The ws object can be serialized so it can be stored into disk or database? Otherwise I can only store the binding into memory.
What the differences between em-websocket and websocket-rails?
Which gem do you recommend for websocket?
You're approaching a use case that websockets are pretty good for, so you're on the right track.
You could serialize the ws object with Marshal, but think of websocket objects as being a bit like http request objects in that they are abstractions for a type of communication. You are probably best off marshaling/storing the data.
em-websocket is a lower(ish) lever websocket library built more or less directly on web-machine. websocket-rails is a higher level abstraction on websockets, with a lot of nice tools built in and pretty ok docs. It is built on top of faye-websocket-rails which is itself built on web machine. *Note, action cable which is the new websocket library for Rails 5 is built on faye.
I've use websocket-rails in the past and rather like it. It will take care of a lot for you. However, if you can use Rails 5 and Action Cable, do that, its the future.
The following is in addition to Chase Gilliam's succinct answer which included references to em-websocket, websocket-rails (which hadn't been maintained in a long while), faye-websocket-rails and ActionCable.
I would recommend the Plezi framework. It works both as an independent application framework as well as a Rails Websocket enhancement.
I would consider the following points as well:
do you need the message to persist between connections (i.e. if the other user if offline, should the message wait in a "message box"? for how long should the message wait?)...?
Do you wish to preserve message history?
These points would help yo decide if to use a persistent storage (i.e. a database) for the messages or not.
i.e., to use Plezi with Rails, create an init_plezi.rb in your application's config/initializers folder. use (as an example) the following code:
class ChatDemo
# use JSON events instead of raw websockets
#auto_dispatch = true
protected #protected functions are hidden from regular Http requests
def auth msg
#user = User.auth_token(msg['token'])
return close unless #user
# creates a websocket "mailbox" that will remain open for 9 hours.
register_as #user.id, lifetime: 60*60*9, max_connections: 5
end
def chat msg, received = false
unless #user # require authentication first
close
return false
end
if received
# this is only true when we sent the message
# using the `broadcast` or `notify` methods
write msg # writes to the client websocket
end
msg['from'] = #user.id
msg['time'] = Plezi.time # an existing time object
unless msg['to'] && registered?(msg['to'])
# send an error message event
return {event: :err, data: 'No recipient or recipient invalid'}.to_json
end
# everything was good, let's send the message and inform
# this will invoke the `chat` event on the other websocket
# notice the `true` is setting the `received` flag.
notify msg['to'], :chat, msg, true
# returning a String will send it to the client
# when using the auto-dispatch feature
{event: 'message_sent', msg: msg}.to_json
end
end
# remember our route for websocket connections.
route '/ws_chat', ChatDemo
# a route to the Javascript client (optional)
route '/ws/client.js', :client
Plezi sets up it's own server (Iodine, a Ruby server), so remember to remove from your application any references to puma, thin or any other custom server.
On the client side you might want to use the Javascript helper provided by Plezi (it's optional)... add:
<script src='/es/client.js' />
<script>
TOKEN = <%= #user.token %>;
c = new PleziClient(PleziClient.origin + "/ws_chat") // the client helper
c.log_events = true // debug
c.chat = function(event) {
// do what you need to print a received message to the screen
// `event` is the JSON data. i.e.: event.event == 'chat'
}
c.error = function(event) {
// do what you need to print a received message to the screen
alert(event.data);
}
c.message_sent = function(event) {
// invoked after the message was sent
}
// authenticate once connection is established
c.onopen = function(event) {
c.emit({event: 'auth', token: TOKEN});
}
// // to send a chat message:
// c.emit{event: 'chat', to: 8, data: "my chat message"}
</script>
I didn't test the actual message code because it's just a skeleton and also it requires a Rails app with a User model and a token that I didn't want to edit just to answer a question (no offense).

Playing around with mails in Rails

I`m trying to create the following feature: You register and receive an email like vouldjeff+ewr#myapp.com and when you send something to this email it automatically appears in something like your wall... So my problem is how to realize the creation of the email and the receiving of the mail itself.
Any ideas?
Ruby provides Net/IMAP and Net/POP3 you can use to login into your email account.
Here's a small tutorial.
POP3
pop = Net::POP3.new("pop.gmail.com", port)
pop.enable_ssl
pop.start('YourAccount', 'YourPassword')
if pop.mails.empty?
puts 'No mail.'
else
i = 0
pop.each_mail do |m|
File.open("inbox/#{i}", 'w') do |f|
f.write m.pop
end
m.delete
i += 1
end
puts "#{pop.mails.size} mails popped."
end
pop.finish
IMAP
imap = Net::IMAP.new('imap.gmail.com')
imap.authenticate('LOGIN', 'username', 'password')
imap.select('INBOX')
imap.search(['ALL']).each do |message_id|
msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
MailReader.receive(msg)
imap.store(message_id, "+FLAGS", [:Deleted])
end
imap.expunge()
There might be other options but that's how we do it:
Postfix
Rails Cron Job
Postfix allows you to specify a MySQL table/view to check whether an email address exists or not. You can also define Mail Forwardings.
Create a DB View to match the requirements on Postfix
This View should contain all the email addresses and forward them to a different mail account, like mailparser.
Now your Rails can either
use a POP3/IMAP frontend to the mailserver (you should install Dovecot or Courier then) to fetch the mails and process them
or go to the place on the disk where all the mails are located (check Postfix config for that) and parse the files as TMail objects and process them.
A different option is to make Postfix call script/runner with the Mail data, but rails boot-up can take long and a lot of memory, so I prefer having a Cronjob/Backgroundjob/Worker to do this.
P.S. The Creation of the E-Mail will be done by creating a Model for your Rails app which the View will use as a basis.
Sending E-Mails is simple as pie. Simply have a look at the ActionMailer Basics. If you also want to receive E-Mail, you should write a daemon that fetches Mails from the mailserver continuously in the background.
Here a snippet that fetches Mails via POP:
require 'net/pop'
config = {
:host => "mail.example.com",
:user => "foobar#example.com",
:password => "…",
:port => 110,
:timeout => 10
}
pop = Net::POP3.new(config[:host])
pop.start(config[:user], config[:password])
if pop.mails.empty?
puts "No mails…"
else
pop.mails.each do |mail|
# do stuff with mail
end
end
This is pure Ruby-Code, Rails is not needed for this snippet.

Resources