Contact form with phpmailer + Uniform


#1

If someone is interested, and as I can’t find any documentation on this implementation, here is my solution for a simple (or less simple) contact form with uniform + a phpmailer driver. (I’m a graphic designer so I don’t tell you that it’s the best solution, but it’s the only one I found online, feel free to add corrections)

Why phpmailer? Because as they say themselves:

Formatting email correctly is surprisingly difficult. There are myriad overlapping RFCs, requiring tight adherence to horribly complicated formatting and encoding rules - the vast majority of code that you’ll find online that uses the mail() function directly is just plain wrong! Please don’t be tempted to do it yourself - if you don’t use PHPMailer, there are many other excellent libraries that you should look at before rolling your own - try SwiftMailer, Zend_Mail, eZcomponents etc.

Kirby (and uniform) are based on the simple mail() function, so after a lot of research (I’m not a developper) I ended to do this:

  1. Install the uniform plugin
  2. Install the phpmailer driver
  3. set up my controller
  4. set up my template

In /site/plugins create a phpmailer-driver folder, and copy the official class.phpmailer.php you can found it on the official phpmailer github repo. This give you the base functions of phpmailer.

add a phpmailer-driver.php with this content:

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

	$mail->CharSet = 'UTF-8';
	$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);
	}
}
?>

Then, add a controller to your contact page, in /site/controllers (a php file with the same name of the template you put the form in). The content of the controller depends on what you want to do, read the Uniform documentation. You can put this code directly in your template file but a controller is cleaner.

A simple example of contact form with copy sended to sender:

<?php

use Uniform\Form;

return function ($site, $pages, $page) {
    $to = page('about')->email();  //or your email in a string 'your@email.com', or the email set in config.php
    $subject = 'Contact from website '.$_POST['name'];
    $from_email = $_POST['email'];
    $message = $_POST['message'];
    if(isset($_POST['receive_copy'])):
    $receivecopy = true;
    endif;
    $form = new Form([
        'email' => [
            'rules' => ['required', 'email'],
            'message' => "Email seems invalid.",
        ],
        'name' => [],
        'message' => [
            'rules' => ['required'],
            'message' => "The message is missing",
        ],
        'receive_copy' => [],
    ]);

    if (r::is('POST')) {
        $form->emailAction([
            'to' => $to,
            'from' => $to, //use an email of your company, here the same email is used
            'subject' => $subject,
            'receive-copy' => $receivecopy,
            'service' => 'phpmailer'
        ]);
    }

    return compact('form');
};

And the form himself in html/php, you can embed this directly in your template too, but a snippet is cleaner.

<form id="contact" method="post" action="#contact">
  <fieldset>
    <h3>Contact me</h3>

<?php if ($form->success()): ?>
    <p class="success">Message sent</p>
<?php else: ?>
    <?php snippet('uniform/errors', ['form' => $form]) ?>
<?php endif; ?>

    <p>
      <label for="name">Enter your name (optional) :</label>
      <input id="name" <?php if ($form->error('name')): ?> class="error"
      <?php endif; ?> name="name" type="text" value="<?php echo $form->old('name') ?>">
    </p>
    <p>
      <label for="email">Enter your email* :</label>
      <input id="email" <?php if ($form->error('email')): ?> class="error"
      <?php endif; ?> name="email" type="email" value="<?php echo $form->old('email') ?>">
    </p>
    <p>
      <label for="message">Write your message* :</label>
      <textarea id="message" <?php if ($form->error('message')): ?> class="error"<?php endif; ?> name="message"><?php echo $form->old('message') ?></textarea>
    </p>
    <!-- The following field is for robots only, invisible to humans: -->
    <p aria-hidden=”true” class="visually-hidden" id="pot">
      <?php echo csrf_field() ?>
      <?php echo honeypot_field() ?>
    </p>
    <p>
      <label for="copy"><input id="copy" name="receive_copy" type="checkbox" value="true" checked="true">Receive a copy of your message.</label>
      *Mandatory fields
    </p>
    <p>
      <input type="submit" value="Send !" class="submit" />
    </p>
  </fieldset>
</form>

In your CSS, to hide the honeypot and csrf fields:

.visually-hidden, .uniform__potty {
	position: absolute !important;
	border: 0 !important;
	height: 1px !important;
	width: 1px !important;
	padding: 0 !important;
	overflow: hidden !important;
	clip: rect(0, 0, 0, 0) !important;
}

I hope it can help some people who try to achieve this. Next step: do this without uniform, and implement advanced phpmailer functions as SMTP.


What are your go-to kirby add-ons?
Easiest most secure way to send plain-text emails through kirby Uniform
Upload Attachment for Mail-Form
Cartkit & Uniform payment system
#2

Just some comments on your controller code:

  • You shouldn’t have to set the receive-copy option manually to true or false for each request. Instead, just enable the option with true and the email action will decide by itself wether to send a copy or not.
  • You can use templates for the email subjects to conveniently include form data.

Take a look at this controller:

<?php

use Uniform\Form;

return function ($site, $pages, $page) {

    $form = new Form([
        'email' => [
            'rules' => ['required', 'email'],
            'message' => 'Email seems invalid.',
        ],
        'name' => [],
        'message' => [
            'rules' => ['required'],
            'message' => 'The message is missing',
        ],
        'receive_copy' => [],
    ]);

    if (r::is('POST')) {
        $to = page('about')->email();

        $form->emailAction([
            'to' => $to,
            'from' => $to,
            'subject' => 'Contact from website {name}',
            'receive-copy' => true,
            'service' => 'phpmailer',
        ]);
    }

    return compact('form');
};

#3

Thank you, I will test it. I don’t understand the “receive copy” part : what if the sender set it to false ? How the email action decide the right action as it’s not linked in the code ?


#4

The email action looks for the receive_copy field. If it is there it will send the copy (the user checked the checkbox), if not it won’t (the user didn’t check the checkbox). The receive-copy option is just for enabling this feature in the first place.


#5

Update:
class.phpmailer.php became PHPMailer.php and you can find it here.

So you have to replace the part about it in my tutorial by:


In /site/plugins create a phpmailer-driver folder, and copy the official PHPMailer.php you can found it on the official phpmailer github repo. This give you the base functions of phpmailer.

add a phpmailer-driver.php with this content:

<?php
use PHPMailer\PHPMailer\PHPMailer;

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

    $mail = new PHPMailer();

    $mail->CharSet = 'UTF-8';
    $mail->setFrom($email->from);
    $mail->addReplyTo($email->replyTo);
    $mail->addAddress($email->to);


    $mail->isHTML(true);
    $mail->Subject = $email->subject;
    $mail->Body = $email->body;

    if (!$mail->send()) {
        throw new Error('PHPMailer error: ' . $mail->ErrorInfo);
    }
}
?>

And there is a little mistake in the form snippet, with wrong quote in <p aria-hidden="true" class="visually-hidden" id="pot"> causing accessibility issues:

<form id="contact" method="post" action="#contact">
  <fieldset>
    <h3>Contact me</h3>

<?php if ($form->success()): ?>
    <p class="success">Message sent</p>
<?php else: ?>
    <?php snippet('uniform/errors', ['form' => $form]) ?>
<?php endif; ?>

    <p>
      <label for="name">Enter your name (optional) :</label>
      <input id="name" <?php if ($form->error('name')): ?> class="error"
      <?php endif; ?> name="name" type="text" value="<?php echo $form->old('name') ?>">
    </p>
    <p>
      <label for="email">Enter your email* :</label>
      <input id="email" <?php if ($form->error('email')): ?> class="error"
      <?php endif; ?> name="email" type="email" value="<?php echo $form->old('email') ?>">
    </p>
    <p>
      <label for="message">Write your message* :</label>
      <textarea id="message" <?php if ($form->error('message')): ?> class="error"<?php endif; ?> name="message"><?php echo $form->old('message') ?></textarea>
    </p>
    <!-- The following field is for robots only, invisible to humans: -->
    <p aria-hidden="true" class="visually-hidden" id="pot">
      <?php echo csrf_field() ?>
      <?php echo honeypot_field() ?>
    </p>
    <p>
      <label for="copy"><input id="copy" name="receive_copy" type="checkbox" value="true" checked="true">Receive a copy of your message.</label>
      *Mandatory fields
    </p>
    <p>
      <input type="submit" value="Send !" class="submit" />
    </p>
  </fieldset>
</form>

and rename the phpmailer-driver folder to PHPMailer-driver


Uniform and PHPMailer works on localhost but not on server
#6

Tips: Don’t make the same dumb error i made by accident. If you want to custom your errors message, in your Contact template replace:
<?php snippet('uniform/errors', ['form' => $form]) ?>
by:
<?php snippet('form-errors', ['form' => $form]) ?>
And then copy paste the uniform snippet to your new one and now you can edit what you want.

(Yes i really made this mistake).

Muchas gracias Señor judbd.


#7

Another Tips:
If you use the kirby cache you have to disable it for the contact page.
When enabled, the CSRF token is cached and stay always the same. You also get this error “The CSRF token was invalid” if you enable the debuging (if not, nothing happen).

// Ignore page for cache
c::set('cache.ignore', array(
    'contact',
));

:v: