Send mail with xls attachment without generate file - ruby-on-rails

I am generating a xls using 'to_xls' gem. So I have like:
my_xls = User.all.to_xls
Now I want to send it using ActionMailer, I tried like that:
attachments[my_xls.original_filename] = {
:content=>my_xls.read,
:mime_type=>my_xls.content_type
}
But for my surprise, my_xls is not a file but a String. I guess I could solve that by opening a new file and writing the string to it, but I'm using Heroku and it doesn't like writing to file (Permission denied). The best solution would be generate a file-like stream data (like getting a file from a HTML form) and send it.
I need something like rails send_data controller method, that send a stream of data to the view without generating a new file.
So, how do I do that?

Something close to this, I might have gotten the mime type wrong, it's generic in the code below, but the format I use in my rails code for action mailer is as follows:
attachment "application/octet-stream" do |a|
a.body = my_xls.read
a.filename = my_xls.original_filename
end
possible types could be:
"application/excel" or "application/vnd.ms-excel" instead of "application/octet-stream"
I have not tested this...
Also if my_xls is a string instead you might have to convert it to bytes before sending it over the wire:
my_xls.bytes.to_a.pack("C*")
there is a SOF topic here talking about this but this is for send_data, but might still apply:
Difficulty with send_data in Ruby on Rails in conjunction with Spreadsheet plug-in
Just trying to point you in a direction that will hopefully help!

The below chunk of code works like a charm. I have tried this, and is in a working application.
def send_excel_report(file_name, emails, subject, email_content, file_path, bcc_emails = [])
attachments["#{file_name}.xls"] = File.read(file_path)
mail(to: emails, subject: subject, from: "my_email#gmail.com", bcc: bcc_emails) do |format|
format.html { render :partial => "users/mail_body"}
end
end
FYI: I have used spreadsheet gem to create the excel

Related

Rails 7 download PDF from binary string

In my Rails 7 app to PDF creation I'm using external service. To get PDF I have to send a GET request and in the response I'm receiving encoded string. Now I would like to give the possibility to download this file without saving it on the server.
How to do so? all searched topics are almost 10y old, is it some modern way (or maybe a gem) to do so ?
Everything I found actually boils down to the code below:
# service which I'm using to create generate pdf from string
class PdfGenerator
def initialize(binary_pdf)
#binary_pdf = binary_pdf
end
attr_reader :binary_pdf
def call
File.open('invoice.pdf', 'wb') do |file|
content = Base64.decode64(binary_pdf)
file << content
end
end
end
# payments_controller.rb
def pdf_download
response = client.invoice_pdf(payment_id: params[:id]) # get request to fetch pdf
PdfGenerator.new(response[:invoice_pdf]).call
end
View where I'm hitting pdf_download endpoint:
<%= link_to t('.pdf_download'), pdf_download_payment_path(payment.id), data: { 'turbo-method' => :post } %>
I expected to start downloading the file but nothing like that happened. In rails server I'm receiving below message instead:
No template found for PaymentsController#pdf_download, rendering head :no_content
Completed 204 No Content in 8ms (ActiveRecord: 0.2ms | Allocations: 2737)
My answer is mostly a guess as working with files is not one of my strengths.
But basically because you have no escape in payments_controller#pdf_download such as render, redirect etc .. your application is expecting a view called "pdf_download" in views/payments folder.
In your controller, you have to send the data to the user, and this should fix the problem:
send_data(file, disposition: 'inline', filename: "invoice.pdf", type: 'application/pdf')
or
send_data(PdfGenerator.new(response[:invoice_pdf]).call, disposition: 'inline', filename: "invoice.pdf", type: 'application/pdf')
in your case. (The call action should return the file, if it doesn't be more explicit with a return statement)
Though one thing : you create invoices from the same file "invoice.pdf" at the root of your app. Because of GIL/GVL only one thread is performing Ruby at a single time then your file will be created in one go, and returned to the user the same way, without any issue.
Though in case you change the way your app work, maybe introducing async, then race may happen where multiple threads are modifying the same single file at the same time. You may then switch to using tempfiles with different names or finding a way to make your invoices all distincts.

Attach Prawn pdf to email

I have been searching for ages and I still cant seem to figure this out,
I am currently using the prawn gem and I want to be able to attach a pdf to an email in my invoice controllers create action. I am currently able to link to a generated pdf from my invoices/show page through http://localhost:3000/invoices/297.pdf but I cant figure out how to attach this pdf to an email.
Currently I am not storing the generated PDF anywhere and my mailer looks like this
Mailer
class InvoiceMailer < ActionMailer::Base
default :from => "notifications#example.com"
def invoice_email(user)
#user = user
mail(:to => user.email, :subject => "Invoice Recieved")
end
end
And my InvoicesController Create Action
{...}
respond_to do |format|
if #invoice.save
InvoiceMailer.invoice_email(#invoice.user).deliver
format.html { redirect_to invoice_url(#invoice, back_path:
{...}
How can I add my invoice as an attachment to this mailer? Do I need to store the invoice somewhere before I can send it?
How you do this depends on how long the PDF generation takes and/or how much load it places on your server and whether you care about that. In my case I was generating PDFs from user-generated-content and I was seeing some PDF creation times up in the 30+ seconds range. Solving for that becomes a run-the-job-somewhere-else and cache-it (whether DB or cloud storage) issue.
#toddmetheny is quite right in suggesting cloud storage for all but the simplest solutions. It gets more interesting if you are hosting on something like Heroku with ephemeral storage, or if you are separating PDF creation from email sending from user requests (e.g. from Heroku again, web dynos vs worker dynos). You can generate the PDF to a local temporary file, but that temporary file may not be there by the time you need to read it in your Mailer running on a 'worker'.
Really simple option
In your Mailer you could generate the PDF to a local file, read it back into memory, then attach it:
def invoice_email(user)
#user = user
attachments['filename_for_user.pdf'] = generate_pdf_content
mail(:to => user.email, :subject => "Invoice Recieved")
end
private
# I had troubles trying to get Prawn to generate 'in memory'
# so just write to, then read, then unlink a tempfile
def generate_pdf_content
pdf = some_method_that_generates_a_prawn_pdf_object
Tempfile.create do |f|
pdf.render_file f
f.flush
File.read(f)
end
end
I suggest you start here to get things working.
More complicated option
Someday you may want to separate the job that generates the PDF (which can take a long time) from the jobs that send email, which are usually much faster. The way I do this is to have a job that generates the PDF to a local Tempfile, then uploads that file to S3 storage and records the S3 object id (I do it in my database, you could just do it as a job attribute you push around).
When complete, that job creates a new mailer job. The mailer job (which may execute on a different server) downloads the PDF from S3 storage to a local file, then adds it to the email much like the simple option above.
You'll need a url you can work with. Any cloud storage solution is an option if you don't want to store it on your database.
Here's some pertinent info on adding attachments to mailers from rails guides on action mailer:
2.3.1 Adding Attachments
Action Mailer makes it very easy to add attachments.
Pass the file name and content and Action Mailer and the Mail gem will automatically guess the mime_type, set the encoding and create the attachment.
attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
When the mail method will be triggered, it will send a multipart email with an attachment, properly nested with the top level being multipart/mixed and the first part being a multipart/alternative containing the plain text and HTML email messages.

How to render a view normally after using render_to_string?

In my Rails application I have an action which creates a XML document using an XML Builder template (rxml) template and render_to_string. The XML document is forwarded to a backend server.
After creating the XML document I want to send a normal HTML response to the browser, but somehow Rails is remembering the first call to render_to_string.
For example:
Rails cannot find the default view show.html.erb because it looks for a show.rxml.
Simply putting a render 'mycontroller/show.html.erb' at the bottom of my action handler makes Rails find the template, but the browser doesn't work because the response header's content type is text/xml.
Is there any way to use render_to_string without "tainting" the actual browser response?
EDIT: It seems that in Rails 2 erase_render_results would do the trick, but in Rails 3 it is no longer available.
The pragmatic answer is that using a view file and two calls to render is Not The Rails Way: views are generally something that is sent to the client, and ActionPack is engineered to work that way.
That said, there's an easy way to achieve what you're trying to do. Rather than using ActionView, you could use Builder::XmlMarkup directly to generate your XML as a string:
def action_in_controller
buffer = ""
xml = Builder::XmlMarkup.new(buffer)
# build your XML - essentially copy your view.xml.builder file here
xml.element("value")
xml.element("value")
# send the contents of buffer to your 3rd server
# allow your controller to render your view normally
end
Have a look at the Builder documentation to see how it works.
The other feature of Builder that you can take advantage of is the fact that XML content is appended to the buffer using <<, so any IO stream can be used. Depending how you're sending content to the other server, you could wrap it all up quite nicely.
Of course, this could end up very messy and long, which is why you'd want to encapsulate this bit of functionality in another class, or as a method in your model.
Seems as if this may be a bug in rails 3 (at least compared to the behavior of 2.3.x render_to_string). In the source for 2.3.8 they clearly take extra steps to reset content_type and set the response body to nil (among other things).
def render_to_string
...
ensure
response.content_type = nil
erase_render_results
reset_variables_added_to_assigns
end
but in the 3.0.3 source for AbstractController::Rendering
def render_to_string(*args, &block)
options = _normalize_args(*args, &block)
_normalize_options(options)
render_to_body(options)
end
You can see there is no explicit resetting of variables, render_to_body just returns view_context.render. It is possible that content-type, response_body, etc are handled elsewhere and this is a red herring, but my first instinct would be to set
response.headers['Content-Type'] = 'text/html'
after your render_to_string before actually rendering.
In migrating the actionwebservice gem I encountered the same error. In their code they circumvent the double render exception by calling the function erase_render_results.
This function is no longer available in rails3. Luckily the fix is quite easy (but it took me a while to find).
Inside actionwebservice the following function was called inside a controller to allow a second render:
def reset_invocation_response
erase_render_results
response.instance_variable_set :#header, Rack::Utils::HeaderHash.new(::ActionController::Response::DEFAULT_HEADERS.merge("cookie" => []))
end
To make this work in rails3, you just have to write:
def reset_invocation_response
self.instance_variable_set(:#_response_body, nil)
response.instance_variable_set :#header, Rack::Utils::HeaderHash.new("cookie" => [], 'Content-Type' => 'text/html')
end
Hope this helps.

How to append to an XML response an error attribute using Ruby on Rails 3?

I am trying to implement REST APIs, so in my RoR3 application I have XML responses. Before to pass to a consumer the XML, I would like to check if there are errors somewhere and, if so, append and send back a response with error messages.
I read "Active Record Validations and Callbacks" guides on the RoR website, but it seems not work in my case.
I extract from the database a resource doing
#response = User.find_by_id(1)
and I would like, if possible, to access #response.errors after a "validation".
Seeing some examples I have seen how to report errors in an XML file
format.xml { render :xml => #response.errors }
but how can I add new errors to the #response?
Maybe something like this:
errors.add(:password, "is invalid")
this works too:
errors.add_to_base('your text')
but you should put it in the model.

Rails: Email a pdf generated with prawn to an email sent by ActionMailer?

I have an ecommerce app. I'm using Prawn to generate pdf invoices of orders. I'm using a standard Prawn setup. In views/admin/orders, I have a file called show.pdf.prawn. When the seller is viewing an order in his admin section, he clicks a link that opens the pdf version of the orders/show view. This all works perfectly.
Now, the tricky part. When an order is completed, I send an email to the seller. What I'd like to do is attach the pdf invoice version of orders/show to that email. Is it possible to do this? The documentation on email attachments is pretty limited and I haven't been able to find resources that go through the workflow that I'm describing.
Any guidance is appreciated.
Sending an attachment with an email is fairly easy with ActionMailer:
class InvoiceMailer < ActionMailer::Base
def email_with_attachment(pdf_invoice)
.
.
.
attachment "application/pdf" do |a|
a.filename = "some_invoice.pdf"
a.body = pdf_invoice
end
end
end
One problem you might have with this is generating the pdf file outside of the prawnto method (when using the prawnto plugin)-
If this is is the case I strongly recommend you to use this approach instead.
I had the same problem, i managed to do it by generating the pdf from a model, much easier than evaluating the template. I replied with the answer here :
Save a Prawn PDF as a Paperclip attachment?

Resources