Send emails using PHPMailer

As I have to send some emails with PDF attachments I thought about writing a mail service that uses PHPMailer (https://github.com/PHPMailer/PHPMailer) to make my life easier… The opposite happened…

This is what I’ve got so far:

// site/plugins/kirby-phpmailer/service.php

<?php
email::$services['phpmailer'] = function($email) {
	require_once(__DIR__ . DS . 'class.phpmailer.php');
	$mail = new PHPMailer;

	$mail->setFrom($email->from);
	$mail->addReplyTo($email->replyTo);
	$mail->addAddress($email->to);

	if ($email->attachment != null) {
		$mail->addAttachment($email->attachment);
	}
	$mail->isHTML(true);

	$mail->Subject = $email->subject;
	$mail->Body = $email->body;

	if (!$mail->send()) {
		throw new Error('PHPMailer error: ' . $mail->ErrorInfo);
	}
}
// site/plugins/kirby-phpmailer/class.phpmailer.php

<?php
class PHPMailer {
  ...
}

I guess my problems are not very kirby specific but general PHP knowledge: Can I even require in a file in an anonymus function? If so, what is __DIR__ set to? Would it be better to pass $mail to the function, i.e. create a closure? And most importantly: Why does whatever goes wrong here not lead to an entry in my server’s error_log??

The problem is that your service is not being loaded in the first place. The main plugin file must have the same name as the directory. So instead of site/plugins/kirby-phpmailer/service.php, it has to be site/plugins/kirby-phpmailer/kirby-phpmailer.php.

To answer all your other questions: Your code looks completely fine and I think you don’t need to improve anything with it. :smile:

God, now that you mention it… I feel like it’s my first day with kirby again… :disappointed: Thank you!
It is working now.

Any reason why an undefined mail service would not throw an exception and/or stop script execution?

Hm. There is actually a check in the Email class that should throw an error. Are you sure you don’t get any kind of error? Maybe you catch it somewhere?

Turns out I did - I had that part commented out for a while before adding it again, which lead to all the confusion.

Thanks for the topic Fabian. I need to send attachments as well. I’m wondering if you ran into this error:

Notice: Undefined property: Email::$attachment in .../kirby-phpmailer.php on line 15

This is line 15:

if ($email->attachment != null) {

I’m uploading a resume through the kirby uniform plugin, added this service, and am passing the attachment to the service like so:

array(
  '_action' => 'email',
  'to'      => 'luke@example.com',
  'sender'  => 'info@example.com',
  'subject' => 'New resume',
  'service' => 'phpmailer',
  'attachment' => 'resume'

I’m lost. Thanks for any help.

OK, so you can’t just add any element to the array that the uniform plugin sends to the email action. I ended up getting the attachment name from the default $_FILES[field_name_here]['tmp_name'].

Trying to figure out how to do this on my own, I’m using the same as above,

The form submits fine—but no attachment.

    uniform::$actions['upload'] = function($form, $actionOptions) {
     foreach ($actionOptions['files'] as $field) {
        if (!array_key_exists($field, $_FILES)) {
           return [
              'success' => false,
              'message' => "Field {$field} was not submitted"
           ];
        }
     }

     foreach ($actionOptions['files'] as $field) {
        move_uploaded_file($_FILES[$field]['tmp_name'], $actionOptions['target'].'/'.$_FILES[$field]['name']);
     }

     return [
        'success' => true,
        'message' => "All files uploaded"
     ];
    };

   $form = uniform('opportunity-form', array(
         'required' => array('_from' => 'email'),
         'actions'  => array(
            array(
              '_action' => 'email',
              'to'      => 'email@address.com',
              'sender'  => 'email@address.com',
              'subject' => 'Submission',
              'service' => 'phpmailer',
            ),
            array(
              '_action' => 'upload',
              'files' => ['file'],
              'target' => 'content/'.$page->diruri().'/files'
            ),
         )
      )
   );

Do you mean the file is not uploaded or the file is not sent as an attachment to the email?

Sorry—the file is uploaded, just not attached.

Do you only want to send the file as an attachment or do you want to upload and store it in the page content directory as well?

Currently your upload action moves the uploaded file to the content directory but there is no configuration of the email action to send this file as an attachment, too. To send the file as an attachment, change the $email->attachment != null part of the phpmailer service to something like this:

if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
    throw new Error('The file was not uploaded correctly!');
}

$mail->addAttachment($_FILES['file']['tmp_name'], $_FILES['file']['name']);

Note that this is a rather fragile approach since the file field name is hard coded in the email service. It also assumes that the email action is performed before the upload action, otherwise the file would no longer be fund because it was already moved to the page content directory.

If you only want to send the file as an attachment and not store it in the page content directory as well, simply remove the upload action.

Thank you much! It is all making sense now.

Is there a way of passing the attachment name and through the uniform action call, something like this:

'actions' => [
[
	'_action' => 'email',
	'to' => 'x@xxx.com',
	'sender' => 'x@xxx.com',
	'subject' => 'Subject',
	'service' => 'phpmailer',
	'attachment' => ['filename.pdf', 'filelabel']
]

And in
site/plugins/kirby-phpmailer/kirby-phpmailer.php
you check like this:

if ($email->attachment != null) {
	$mail->addAttachment($email->attachment);
}

To make it more flexible you can pass the filename and filelabel via a hidden or similar.

Sorry but was there a question? (no offense :smile:)

:slight_smile: Should the code above work?

'attachment' => ['filename.pdf', 'filelabel']

No, email service options are passed on through the service-options like this:

[
//...
   'service' => 'phpmailer',
   'service-options' => [
      'attachment' => $_FILES['file'],
   ],
]

Where 'file' is the name of the file form field. Note that this doesn’t verify if the file was uploaded correctly (see above).

Then in the phpmailer email service you can do this:

if (array_key_exists('attachment', $email->options)) {
   $mail->addAttachment($email->options['attachment']['tmp_name'], $email->options['attachment']['name']);
}

See Handling file uploads for more information on $_FILES.

I am trying to add attachments to my uniform form (which i am trying to transition to)
and i am looking have an optional file field and i am having a few questions…

// so in my controller i am creating a new form with name, email, message, and filefield where the file field part looks like that as given in the docs...
// ....
           'message' => [],
           'filefield' => [
            'rules' => [
                'file',
                'mime' => ['application/pdf'],
                'filesize' => 5000,
            ],
            'message' => [
                'Please choose a file.',
                'Please choose a file.',
                'Please choose a PDF.',
                'Please choose a file that is smaller than 5 MB.',
            ],

// i then check if there's a post action
        if (r::is('POST')) {
            if($_FILES){
                $form->uploadAction(['fields' => [
                    'filefield' => [
                        'target' => kirby()->roots()->content()
                    ],
                ]]);
            }
               $form->emailAction([
                'to' => $to,
                'from' => $from, 
                'subject' => 'Contact from website {name}',
                'receive-copy' => true,
                'service' => 'phpmailer',
                'service-options' => [
                    'attachment' => $_FILES['filefield'],
                 ],
            ]);
        }

Is it correct that there always has to be a seperate uploadAction and emailAction? It uploads the file just fine, but the attachment will not get attached to the phpmailer email which I added to the phpmailer service class

    if (array_key_exists('attachment', $email->options)) {
        $mail->addAttachment($email->options['attachment']['tmp_name'], $email->options['attachment']['name']);
     }

i haven’t really tried it, since we are moving the uploaded file to a new location, aren’t we supposed to handle the new url in our 'service-options' => '???' value?

if so, what’s the way to call it? and still leave it optional… When using $_files[‘filefield’] it returns some values within the default email template, but the attachment is not available.

i have been using attachments with phpmail (without uniform quite fine - not while uploading though…)

Name: some-name Message: some-message Filefield: filename-whatever.pdf, application/pdf, C:\Users\username\AppData\Local\Temp\phpBAE8.tmp, 0, 8794

within the email header, it shows being send via phpmailer.
above returned in the default template… i am also using the recaptcha plugin extension (if that matters)

Is it correct that there always has to be a seperate uploadAction and emailAction?

Not if you just want to use the uploaded file as attachment to the email. The upload action is for storing the uploaded file persistently somewhere. If you just need it as attachment you can ditch the upload action. If you do this, your $mail->addAttachment code should start working. The upload action moves the uploaded file away from the temporary location, so it was gone from tmp_name when the email action was executed.