Sending emails with attachments is a common requirement for any website. Entity print is a widely popular module that helps to export any Drupal entities as PDFs instantly. Using this module, users can either download an entity as pdf or view the entity as PDF in their browser instantly.
However, in some cases, you might want to send the PDF version of a node/entity as an attachment in an email to the user. Entity print module does not support this out of the box. But we can easily reuse the services provided by the module to build this functionality.
Prerequisites
- A fresh/existing Drupal website.
- Entity print module downloaded and installed using composer.
- A PDF generation library configured on the server.
- Working SMTP configuration with support to send email attachments.
The Goal
Any node can be instantly downloaded as a PDF by accessing the path /print/pdf/node/[node_id] using the Entity print module. We plan to extend this functionality and build a custom module that will
- Print any node as PDF by accessing the path ‘/email/pdf/node/[node_id]'.
- Save the generated PDF in the ‘emailed_pdfs’ folder under the ‘sites/default/files’ directory.
- Send a mail to a given mail id with the generated PDF attached.
- Add the generated PDF to a queue for deletion, after the mail has been sent.
- Redirect back to the node with a status message once the mail has been successfully sent.
The Plan
- Create a Controller with a method process() that will get invoked when the path /email/pdf/node/[node_id] is accessed.
- Create a queue worker plugin which will process items added to the queue during cron.
- Within the process() method:
- Call the prepareDirectory() method first to create the ‘emailed_pdfs’ folder under ‘sites/default/files’.
- Call generatePdfFromNode(), responsible for generating and saving the PDF in the ‘emailed_pdfs’ folder.
- Call sendMail(), responsible for creating the PDF attachment and sending the email.
- Implement hook_mail() to map the email parameters.
- Add the uri of the pdf to the queue for clean up, once the mail has been sent.
- Note: Queues can also be used for generating the PDF in high traffic sites.
The Implementation
- Create the controller ‘EntityPrintMailController’.
- Inject the following services to the controller.
- Now define the generate() method.
/**
* Build the response.
*/
public function process(NodeInterface $node_id) {
// Prepare the destination folder if it does not exist.
if ($this->prepareDestinationFolder()) {
// Generate the PDF from the node.
$data = $this->generatePdfFromNode($node_id);
if (!empty($data)) {
// Pass the 'uri' and 'print engine' values to attach the pdf and send
// the mail.
$result = $this->sendMail($data['uri'], $data['print_engine']);
// If $result = TRUE, Mail has been sent successfully.
if ($result) {
$message = $this->t('Email sent successfully');
$this->messenger()->addStatus($message);
// Add the generated file's uri to the queue so that it can be
// deleted later.
$queue = $this->queueFactory->get('my_module_pdf_remover');
$item = new \stdClass();
$item->uri = $data['uri'];
$queue->createItem($item);
}
}
}
// Redirect back to the node.
return $this->redirect('entity.node.canonical', ['node' => $node_id->id()]);
}
- The prepareDestinationFolder() method uses the ‘prepareDirectory()’ method provided by the ‘file_system’ service to create the ‘emailed_pdfs’ folder.
/**
* Prepares the folder to store the generated PDFs.
*/
public function prepareDestinationFolder() {
$destination_folder = 'public://emailed_pdfs';
// Try to create the directory.
if ($this->fileSystem->prepareDirectory(
$destination_folder, FileSystemInterface::CREATE_DIRECTORY |
FileSystemInterface::MODIFY_PERMISSIONS)
){
// Return 'TRUE' if the folder was successfully created.
return TRUE;
}
else {
// Return 'FALSE' in case of any error.
return FALSE;
}
}
- Once the folder is created, generatePdfFromNode() method converts the node entity to PDF..
/**
* Generates the pdf from the node.
*/
public function generatePdfFromNode(NodeInterface $node) {
// Define the name of the pdf.
$file_name = 'emailed_pdfs/' . $node->label() . '.pdf';
// Generate the pdf.
$print_engine = $this->pluginManagerEntityPrintPrintEngine->createSelectedInstance('pdf');
$file_path = $this->entityPrintPrintBuilder->savePrintable([$node], $print_engine, 'public', $file_name);
if ($file_path) {
return [
'uri' => $file_path,
'print_engine' => $print_engine,
];
}
return [];
}
- Appending ‘/emailed_pdfs’ to the file name ensures that the file gets saved in the ‘emailed_pdfs’ folder.
- Both the uri and binary data of a file are required to attach it in an email.
- The binary data of the file can be obtained from the print engine object.
- So, once the file is saved, an array containing both the file uri and print engine object are returned.
/**
* Send the mail with the given file as attachment.
*/
public function sendMail(string $file_uri, PrintEngineInterface $print_engine) {
$module = 'my_module';
$key = 'node_pdf_mail';
$to_mail = 'test@test.com';
// Create the file attachment.
$attachment = [
'filecontent' => $print_engine->getBlob(),
'filepath' => $file_uri,
'filemime' => 'application/pdf',
];
$params['attachments'] = $attachment;
$params['message'] = 'Mail subject';
$params['subject'] = 'Mail body';
$langcode = $this->languageManager()->getCurrentLanguage()->getId();
// Send the mail.
$result = $this->pluginManagerMail->mail($module, $key, $to_mail, $langcode, $params, NULL, TRUE);
return $result;
}
- The ‘sendMail()’ method accepts the file uri and the print engine object.
- ‘$print_engine->getBlob()’ returns the binary data of the pdf.
- Then hook_mail is added to properly map the mail parameters.
/**
* Implements hook_mail().
*/
function my_module_mail($key, &$message, $params) {
switch ($key) {
case 'node_pdf_mail':
$message['from'] = \Drupal::config('system.site')->get('mail');
$message['subject'] = $params['subject'];
$message['body'][] = $params['message'];
$message['params']['attachments'][] = $params['attachments'];
break;
}
}
- Finally, the uri of the file generated is added to the ‘my_module_pdf_remover’ queue for deleting it.
/**
* Defines 'my_module_pdf_remover' queue worker.
*
* @QueueWorker(
* id = "my_module_pdf_remover",
* title = @Translation("Pdf remover"),
* cron = {"time" = 60},
* )
*/
final class PdfRemover extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* Constructs a new PdfRemover instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
private readonly FileSystemInterface $fileSystem,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('file_system'),
);
}
/**
* {@inheritdoc}
*/
public function processItem($data): void {
// Get the uri of the file to remove.
$uri = $data->uri;
// Delete the file.
$this->fileSystem->delete($uri);
}
}
Verify the Result
- Create a node titled ‘Awesome node’.
- Update the 'to' address in the ‘sendMail()’ method to your email address..
- Assuming the node id is ‘1’, access the path ‘/email/pdf/node/1’.
- You will be redirected to the 'Awesome node' page with a success message stating 'Email sent successfully'.
- Check the folder ‘/sites/default/files/emailed_pdfs’. You will find the generated PDF named ‘Awesome node.pdf’ there.
- If SMTP is configured for your site, you will receive the PDF as an attachment in the email.
- Running the cron would remove the PDF from the folder
A Quick Recap
Hope this blog has given you a basic understanding of how to export a node as a PDF and attach it to emails. Here is an overview of all the things covered in this blog.
- Created a controller that will accept any valid node ids and export that node as PDF.
- Reused services provided by the Entity print module to save the PDF in a separate folder.
- Learned how to add file attachments in emails.
- Created queue workers to clean up the unwanted files.
GitHub URL