Rails 4 - How to send newsletter-like emails with delayed_job? - ruby-on-rails

I want to send a summary of our new lists to our users every morning. What's the best approach to do that with Ruby On Rails 4, ActiveRecord (using SendGrid) and Delayed Job?
I am currently doing it this way:
In controller:
def yesterday_listings_for_users
yesterday_listings = Listings.where('status = "0" AND (DATE(created_at) = ?)', Date.today - 1)
if yesterday_listings.count > 0
NotificationMailer.delay.yesterday_listings_for_users_notification
end
render :nothing => true
end
And then in the mailer:
def yesterday_listings_for_users_notification
#listings = Listing.where('status = "0" AND (DATE(created_at) = ?)', Date.today-1)
mail(to: 'myemail#gmail.com', subject: "Latest Listings", from: 'no-reply#mywebsite.com')
end
With using a CRON job, this sends me the report every morning on my email address. I have a few hundreds of users in the database and I would like to send them this email as well.
How to do that? I am wondering about something like this:
def yesterday_listings_for_users_notification
#listings = Listing.where('status = "0" AND (DATE(created_at) = ?)', Date.today-1)
Users.all.each do |user|
mail(to: user.email, subject: "Latest Listings", from: 'no-reply#mywebsite.com')
end
end
However, is looping through hundreds of records in database and sending hundreds of emails in a delayed mailer method recommened (or appropriate)?
Is there a better way to do that?
Thank you in advance!

I usually prefer to use Sidekiq along with Sidetiq but if you want to use delayed_job I would advice you to use the whenever gem for simplicity.
Whenever is a Ruby gem that provides a clear syntax for writing and
deploying cron jobs.
Add gem 'whenever' to your gemfile
run the command wheneverize . which will generate a file config/schedule.rb
In your config/schedule.rb do the following.
every 1.day, :at => '11:30 am' do
runner "User.delay.send_daily_newsletter"
end
In your user.rb define the method send_daily_newsletter and use find_each instead of all.each (batches)
def self.send_daily_newsletter
listings = Listing.where('status = "0" AND (DATE(created_at) = ?)', Date.today - 1).select(:title).to_json
User.select(:id, :email).find_each do |u|
NotificationMailer.delay.send_daily_newsletter(u.email, listings)
end
end
In your notification_mailer.rb define send_daily_newletter
def send_daily_newsletter(user_email, listings)
#listings = listings
mail(to: user_email, subject: "Latest Listings", from: 'no-reply#mywebsite.com')
end
This way you will have one delayed job to get all users and send each email using a separate worker which is the most optimal way to do this task.
Note: Do not forget to change the methods for listings in your view
from, for example, listing.title to listing[:title] since we are
passing the listings as json.
If you do not want to pass them as json every time to every delayed
task just cache the listings in Rails.cache and clear it
after you finish sending.
EDIT:
If you would like to use the cache method since you ran into a problem in the delayed_job gem, edit your send_daily_newsletter method in your mailer. (That's is why I would go to redis-based Sidekiq rather than mysql-based delayed_job.
def send_daily_newsletter(user_email)
#listings = Rails.cache.fetch('today_listings') { Listing.where('status = "0" AND (DATE(created_at) = ?)', Date.today - 1) }
mail(to: user_email, subject: "Latest Listings", from: 'no-reply#mywebsite.com')
end
and in your user.rb
def self.send_daily_newsletter
User.select(:id, :email).find_each do |u|
NotificationMailer.delay.send_daily_newsletter(u.email)
end
Rails.cache.clear('today_listings')
end
Good luck. I have been doing these email newsletters for a while now and they are truly pain :D

Related

sending mail at user assigned time and updating status in database in rails error

I want to send mail at remind_at time of each stage. i am using whenever gem to schedule task. There are user model that contains email of multiple user with role manager, and each manager has one_to_many association with project and project has one_to_many association with stage.
while sending email to each user i want to update attribute mail_status of stage and mail subject change to stage.name for each user. how can i achieve such goal??
stage.rb
def check_project_activity
current_date = Time.now.strftime("%Y-%m-%d").to_s
#stages = Stage.all
#stages.each do |stage|
ProjectMailer.activity_reminder(stage).deliver and stage.mail_status = true and stage.save! if stage.remind_at.strftime("%Y-%m-%d").to_s == current_date
end
end
schedule.rb
every 1.day, at: '4:30 am' do
runner 'stage.project_activity_check'
end
activity_mailer.rb
def activity_reminder(stage)
#stage = stage
mail(:to => User.joins(projects: :stages).where(role: 'manager', stages: { remind_at: Time.now.strftime("%Y-%m-%d") }).pluck(:email), :subject => "activity reminder " + stage.name)
end
i took reference from this post - Sending emails on user-selected dates
but my implementation is not working.
Here is code that i used but problem is it sends email once to all user but i want dynamic change is email subjet according to user and also template body changes-
activity_reminder.rb
desc 'activity reminder email'
task activity_reminder_email: :environment do
ProjectMailer.activity_reminder(self).deliver!
end
project_mailer.rb
def activity_reminder(stage)
#stage = stage
mail(:to => User.joins(projects: :stages).where(role: 'manager', stages: { remind_at: Time.now.strftime("%Y-%m-%d") }).pluck(:email), :subject => "Project Activity Remainder")
end
schedule.rb
every 1.day, at: '4:30 am' do
rake 'activity_reminder_email'
end
it works fine but i want email subject as stage.name to be according to each user and also it should loop though each task and update stage.email_status for executed task.
I think you can do some changes:
You're formatting the remind_at values as a stringified version of a Y-m-d date, in that case, you could just compare them as dates. to_date is enough.
Instead of selecting everything and filtering then, you can filter the rows within your SQL query; cast the remind_at to date and use your RDBMS function to get the current date, after that the comparison remains the same.
Left it untouched, but you could probably select the exact columns you're using in your query. This is often preferred so you get what you just need, nothing more, nothing less.
Prefer && over and, since they don't have the same precedence (&& being higher), which may led to inconsistences in your code (this isn't Python).
You can use update instead of a two-lines assignment.
def check_project_activity
Stage.where('remind_at::date = CURRENT_DATE').each do |stage|
ProjectMailer.activity_reminder(stage).deliver
stage.update(mail_status: true)
end
end
remind_at::date = CURRENT_DATE is PostgreSQL specific, but it can be easily adapted.

Rails Query a List for a CRON Job

I'm a complete novice with CRON jobs but I think I have that set up correctly.
Ultimately what I'm trying to do is send an email every day at 8:00 am to users (and a couple others) that have not logged in within the last 3 days, have not received the email, AND are marked as active OR temp as a status.
So from querying the db in console I know that I can do:
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
That's not certainly clean but I was struggling with finding a contained query that grabbed all that data. Is there a cleaner way to do this query?
I believe I have my cron job set up correctly using a runner. I have whenever installed and in my schedule.rb I have:
every 1.day, at: '8:00 am' do
runner 'ReminderMailer.agent_mailer.deliver'
end
So under app > mailer I created ReminderMailer
class ReminderMailer < ApplicationMailer
helper ReminderHelper
def agent_reminder(user)
#user = user
mail(to: email_recipients(user), subject: 'This is your reminder')
end
def email_recipients(agent)
email_address = ''
email_addresses += agent.notification_emails + ',' if agent.notification_emails
email_addresses += agent.manager
email_address += agent.email
end
end
Where I'm actually struggling is where I should put my queries to send to the mailer, which is why I built a ReminderHelper.
module ReminderHelper
def applicable_agents(user)
agent = []
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
agent << third
return agent
end
end
EDIT: So I know I could in theory do a chain of where queries. There's gotta be a better way right?
So what I need help on is: do I have the right structure in place? Is there a cleaner way to query this data in ActiveRecord for the CRON job? Is there a way to test this?
Try combining them together as if understand the conditions correct
Have not logged in within the last 3 days,
Have not received the email
Are marked as active OR temp as a status
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
module ReminderHelper
def applicable_agents(user)
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
end
end
You don't need to add/ assign them to array. Because this relation is already like an array. You can use .to_a if you need array. If you just want to iterate over them then users.each should work fine.
Update
class User
scope :not_notified, -> { where(notified: false) }
scope :active_or_temp, -> { where(status: ['active', 'temmp']) }
scope :last_login_in, -> (default_days = 3) { where("last_login_at < ?", default_days.days.ago) }
end
and then use
User.not_notified.active_or_temp.last_login_in(3)
Instead of Time.now-3.days it's better to use 3.days.ago because it keeps time zone also in consideration and avoids unnecessary troubles and failing test cases.
Additionally you can create small small scopes and combine them. More read on scopes https://guides.rubyonrails.org/active_record_querying.html

Rails send e-mail in a loop

I have the snippet below running in my Rails app, but it's not iterating properly:
task.users.each do |user|
mail(to: user.email
from: "Company <task." + user.id + "#mail.company.com",
subject: "Update on " + user.assignment)
end
If there's 3 users, it only sends out one e-mail. I've verified that there aren't any errors preventing it from continuing.
I can't simply pass an array of e-mails to the to option because I need to use the user's data in each iteration.
Can this be done like the above?
You need to call the .deliver_now method.
mail(to: user.email
from: "Company <task." + user.id + "#mail.company.com",
subject: "Update on " + user.assignment).deliver_now
I have run into an issue where this kind of delivery doesn't work, possibly because the mail was falling out of scope.
So you may need to do something like this:
class SendWeeklySummary
def run
User.find_each do |user|
UserMailer.weekly_summary(user).deliver_now
end
end
end
Rails Guides docs - see section 2.1.4

How do I save an object only after a delayed_job is successfully completed?

I have a delayed_job designed to send an email using a mailer.
Upon completion, I need to record that the email was sent -- I do this by saving the newly created ContactEmail.
Right now, the new ContactEmail records gets saved even if the delayed_job fails.
How do I correct that so that the new ContactEmail is only saved when the mailer is successfully sent?
Here is the snippet from the cron task which calls the delayed_job:
puts contact_email.subject
contact_email.date_sent = Date.today
contact_email.date_created = Date.today
contact_email.body = email.substituted_message(contact, contact.colleagues)
contact_email.status = "sent"
#Delayed::Job.enqueue OutboundMailer.deliver_campaign_email(contact,contact_email)
Delayed::Job.enqueue SomeMailJob.new(contact,contact_email)
contact_email.save #now save the record
Here is the some_mail_job.rb
class SomeMailJob < Struct.new(:contact, :contact_email)
def perform
OutboundMailer.deliver_campaign_email(contact,contact_email)
end
end
And here is the outbound_mailer:
class OutboundMailer < Postage::Mailer
def campaign_email(contact,email)
subject email.subject
recipients contact.email
from '<me#me.com>'
sent_on Date.today
body :email => email
end
You could update the status in the perform of the job itself.
For example, something like:
contact_email.status = 'queued'
contact_email.save
contact_email.delay.deliver_campaign_email
And then in your ContactEmail class, something to the effect of
def deliver_campaign_email
OutboundMailer.deliver_campaign_email(self.contact, self)
self.status = 'sent' # or handle failure and set it appropriately
self.save
end
delayed_job has some magic bits that it adds to your models that will deal with the persistence.
In order to deal with your OutboundMailer throwing an exception, you can do something like so:
def deliver_campaign_email
begin
OutboundMailer.deliver_campaign_email(self.contact, self)
self.status = 'sent'
rescue
self.status = 'failed' # or better yet grab the the message from the exception
end
self.save
end
You need synchronic delivery so stop using delayed job in this case and do standard mailer delivery.
or add success column to you ContactEmail - initialy save it with false then update in job to true

Sending emails based on intervals using Ruby on Rails

I would like to be able to send a string of emails at a determined interval to different recipients.
I assign to each Contact this series of Emails called a Campaign, where Campaign has Email1, Email2, etc. Each Contact has a Contact.start_date. Each Email has email.days which stores the number of days since a Contact's start-date to send the email.
For example: Email1.days=5, Email2.days=7, Email3.days=11
Contact1.start_date = 4/10/2010; contact2.start_date = 4/08/2010
IF today is 4/15, then Contact1 receives Email 1 (4/15-4/10 = 5 days)
IF today is 4/15, then Contact2 received Email 2 (4/15 - 4/8 = 7 days).
What's a good action to run every day using a cron job that would then follow these rules to send out emails using ActionMailer?
NOTE: The question isn't about using ActionMailer. It is about doing the "math" as well as the execution. Which email to send to whom? I am guessing it has to do with some version of Date - Contact[x].start_date and then compare against email[x].days but I'm not exactly clear how. Thanks.
I'd like guidance on whether to use date.today versus time.now as well.
Note: the intent is that an individual person may need to schedule individual follow-up on a consistent basis. Rather than having to remember when to follow up which email with whom, it would just follow a pre-determined campaign and send for that person.
So it's not a "bulk mail" -- it's really automating the follow-up for individual correspondence.
I would use DelayedJob for this ( assuming you are not sending large number of emails emails a day, i.e. 100's of thousands per day etc.)
class Email < ActiveRecord::Base
belongs_to :campaign
after_create :schedule_email_dispatch
def schedule_email_dispatch
send_at(campaign.created_at + self.days.days, :send_email)
end
def send_email
end
end
Run the workers using the rake task:
rake jobs:work
Every time a new Email object is created a delayed job item is added to the queue. At the correct interval the email will be sent by the worker.
#campaign = Compaign.new(...)
#campaign.emails.build(:days => 1)
#campaign.emails.build(:days => 2)
#campaign.save # now the delay
In the example above, two delayed job entries will be created after saving the campaign. They are executed 1 and 2 days after the creation date of the campaign.
This solution ensures emails are sent approximately around the expected schedule times. In a cron job based solution, disptaching happens at the cron intervals. There can be several hours delay between the intended dispatch time and the actual dispatch time.
If you want to use the cron approach do the following:
class Email < ActiveRecord::Base
def self.dispatch_emails
# find the emails due for dispatch
Email.all(:conditions => ["created_at <= DATE_SUB(?, INTERVAL days DAY)",
Time.now]).each do |email|
email.send_email
end
end
end
In this solution, most of the processing is done by the DB.
Add email.rake file in lib/tasks directory:
task :dispatch_emails => :environment do
Email.dispatch_emails
end
Configure the cron to execute rake dispatch_emails at regular intervals( in your case < 24 hours)
I would create a rake task in RAILS_ROOT/lib/tasks/email.rake
namespace :email do
desc "send emails to contacts"
task :send do
Email.all.each do |email|
# if start_date is a datetime or timestamp column
contacts = Contact.all(:conditions => ["DATE(start_date) = ?", email.days.days.ago.to_date])
# if start_date is a date column
contacts = Contact.all(:conditions => { :start_date => email.days.days.ago.to_date })
contacts.each do |contact|
#code to send the email
end
end
end
end
Then I would use a cronjob to call this rake task every day at 3 a.m.:
0 3 * * * app_user cd RAILS_APP_FOLDER && RAILS_ENV=production rake email:send
I think it would be much easier and more secure (you don't have to worry on authentication and so on) to create a rake task to send the emails. Also you don't have to worry about a possibly very long running request. Just create a file RAILS_ROOT/lib/tasks/email.rake
namespace :email do
desc "Sends scheduled emails"
task :send_scheduled => :enviroment do
Email.send_scheduled_emails
end
end
and in RAILS_ROOT/app/email.rb
class Email < ActiveRecord::Base
# ...
def self.send_scheduled_emails
#send your emails ...
end
end
Then create a cron job
0 0 * * * user cd /your/rails/app/ && RAILS_ENV=production rake emais:send_scheduled
to send the emails every night at 12:00.
I am using rufus-scheduler for scheduled email and twitter updates. You should check it.
I use ar_mailer gem
http://seattlerb.rubyforge.org/ar_mailer/
http://github.com/adzap/ar_mailer
http://blog.segment7.net/articles/2006/08/15/ar_mailer

Resources