How to send files as attachments via email with Uniform?

Hm, maybe something wrong with the syntax here, but I’m not sure what we have to provide here, @mzur?

Just to give context on why the above is not really working (because I went through the same):

The validation of file fields with kirby-uniform fails in this example, because you rename the *.tmp files, before kirby-form can validate them. So once you hit $form->emailAction the uploaded $_FILES don’t exist anymore (because renamed) and the validation fails.

Unfortunately, it’s not possible to send attachments with the comfort of automatic validation as provided by kirby-uniform (as far as I can tell)

Right now I’m working on a Custom Action that will rename and email the attachments as expected:

Edit: kirby-uniform will validate only one file per field! <input type="file" multiple> won’t work!

	if ($kirby->request()->is('POST')) { //this handles the validation

		$form->emailAttachmentAction([
			'to' => 'me@example.com',
			'from' => 'info@example.com',
			'attachments' => [
				'fields' => [
					'bewerbung' => [
						// these aren't really used yet
						'rename' => true,
						'multiple' => false,
					],
				],
			],
		])->logAction([
			'file' => kirby()->roots()->site() . '/messages.log',
		]);
	}

And the Custom Action:

<?php

namespace Uniform\Actions;

use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\F;

class EmailAttachmentAction extends EmailAction
{
	public function perform()
	{
		if (empty($this->options['attachments'])) {
			return parent::perform();
		}

		$this->options['attachments'] = $this->getAttachments();

		return parent::perform();
	}

	protected function getAttachments()
	{

		$options = A::get($this->options, 'attachments.fields');

		$attachments = [];

		foreach ($options as $key => $option) {

			// ddd($option,$this->form->data($key));

			if (A::isAssociative($this->form->data($key))) {
				// single file === multiple:false
				$data[] = $this->form->data($key);
			} else {
				$data = $this->form->data($key);
			}

			foreach ($data as $file) {
				// TODO: Rename File? A::get($option,'rename')
				$attachments[] = $this->renameUploadedFile($file);
			}

			// get the data
			//d($data, $attachments, $option, A::isAssociative($data), A::get($option,'multiple'), A::get($option,'rename'));

		}

		return $attachments;
	}

	protected function renameUploadedFile(array $file): string
	{
		$tmp = pathinfo($file['tmp_name']);
		$newFilename = $tmp['dirname'] . '/' . F::safeName($file['name']);

		if (rename($file['tmp_name'], $newFilename)) {
			return $newFilename;
		}
		return $file['tmp_name'];
	}

}

This is just do give you an idea, it’s my work in progress.

If I’m wrong here, please someone correct me, that’s just what I came up with searching github and the forum up and down :slight_smile:
(Reference: https://github.com/mzur/kirby-uniform/issues/169#issuecomment-456061638)

Edit: I’m not convinced that this is the right approach, it just explains why the above approach isn’t working.
But you should handle the uploaded files yourself (move, rename, attach and delete later)

Sorry, i posted the wrong template. I have to look it up first.

I couldn’t do it with uniform, so in the end it was a ‘normal’ Kirby form.

Template (the script at the end allows you to delete the files you entered):

    <?php $session = $kirby->session();
	
	
	if(isset($_GET['thema'])) {

  
$session->set('betreff', $_GET['thema']);
	

}	//else    {
	
 
//$session->set('betreff', '');
	
			//}
    	
?>


<?php snippet('header') ?>
    
    <div class="container-fluid">
	    <div class="container">
		    <div class="row">
			    <div class="col-12">

        <?php
        // if the form input is not valid, show a list of alerts
        if ($alerts) : ?>
        <div class="alert">
            <ul>
            <?php foreach ($alerts as $message): ?>
            <li><?= kirbytext($message) ?></li>
            <?php endforeach ?>
            </ul>
        </div>
        <?php endif ?>
        
        <form class="application-form" method="post" action="<?= $page->url() ?>" enctype="multipart/form-data">
    <div class="honeypot">
        <label for="website">Website <abbr title="required">*</abbr></label>
        <input type="website" id="website" name="website">
    </div>
    <div class="form-element">
        <label for="titel">
            <?= $page->labelbewerbungals() ?> <abbr title="required">*</abbr>
        </label>
        <input type="text" id="titel" name="titel" value="<?php echo $session->get('betreff'); ?>" >
    </div>
    <div class="form-element">
        <label for="name">
            <?= $page->labelname() ?> <abbr title="required">*</abbr>
        </label>
        <input type="text" id="name" name="name" value="<?= $data['name'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="strasse">
            <?= $page->labelstrasse() ?> <abbr title="required">*</abbr>
        </label>
        <input type="text" id="strasse" name="strasse" value="<?= $data['strasse'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="plzort">
            <?= $page->labelplzort() ?> <abbr title="required">*</abbr>
        </label>
        <input type="text" id="plzort" name="plzort" value="<?= $data['plzort'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="telefon">
            <?= $page->labeltelefon() ?> <abbr title="required">*</abbr>
        </label>
        <input type="text" id="telefon" name="telefon" value="<?= $data['telefon'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="fax">
            <?= $page->labelfax() ?> 
        </label>
        <input type="text" id="fax" name="fax" value="<?= $data['fax'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="email">
            Email <abbr title="required">*</abbr>
        </label>
        <input type="email" id="email" name="email" value="<?= $data['email'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="geburtsdatum">
            <?= $page->labelgeburtsdatum() ?> 
        </label>
        <input type="text" id="geburtsdatum" name="geburtsdatum" value="<?= $data['geburtsdatum'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="geburtsort">
            <?= $page->labelgeburtsort() ?> 
        </label>
        <input type="text" id="geburtsort" name="geburtsort" value="<?= $data['geburtsort'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="schulabschluss">
            <?= $page->labelschulabschluss() ?> 
        </label>
        <input type="text" id="schulabschluss" name="schulabschluss" value="<?= $data['schulabschluss'] ?? null ?>" >
    </div>
    <div class="form-element">
        <label for="ausbildung">
            <?= $page->labelausbildung() ?> 
        </label>
        <textarea type="text" id="ausbildung" name="ausbildung"><?= $data['ausbildung'] ?? null ?></textarea> 
    </div>
    <div class="form-element">
        <label for="taetigkeiten">
            <?= $page->labeltaetigkeiten() ?> 
        </label>
        <textarea type="text" id="taetigkeiten" name="taetigkeiten"><?= $data['taetigkeiten'] ?? null ?></textarea> 
    </div>
    <div class="form-element">
        
        
        
	      <label><?= $page->labelfuehrerschein() ?> 
    <input type="radio" name="fuehrerschein" style="margin-left:10px; margin-right:5px; width:15px;" value="ja"<?php if($form->$data['fuehrerschein'] ?? null == 'ja'): ?>  checked<?php endif ?> /><?= $page->labelfuehrerschein1() ?>
    		</label>
    		<label>
<input type="radio" name="fuehrerschein" style="margin-left:10px; margin-right:5px; width:15px;" value="nein"<?php if($form->$data['fuehrerschein'] ?? null == 'nein'): ?> checked<?php endif ?>  /><?= $page->labelfuehrerschein2() ?>
			</label>
			
			
    </div>
    <div class="form-element">
        
        <?php $value = $form->$data['eigenesauto'] ?? null ?>
        
	      <label><?= $page->labeleigenesauto() ?> 
    <input type="radio" name="eigenesauto" style="margin-left:10px; margin-right:5px; width:15px;" value="ja"<?php if($value == 'ja'): ?> checked<?php endif ?> /><?= $page->labeleigenesauto1() ?>
    		</label>
    		<label>
<input type="radio" name="eigenesauto" style="margin-left:10px; margin-right:5px; width:15px;" value="nein"<?php if($value == 'nein'): ?> checked<?php endif ?> /><?= $page->labeleigenesauto2() ?>
			</label>
			
    </div>
    <div class="form-element">
        <label for="weiterefragen">
            <?= $page->labelweiterefragen() ?> 
        </label>
        <textarea type="text" id="weiterefragen" name="weiterefragen"><?= $data['weiterefragen'] ?? null ?></textarea> 
    </div>
    
    
    <div class="form-element">
				 
				 
				 <?php $value = $form->$data['datenschutz'] ?? null ?>
				 
	      <div class="datenschutztext"><?= $page->labeldatenschutztxt()->blocks() ?> </div>
          <label class="custom-control fill-checkbox">
			<input type="checkbox" id="datenschutz" name="datenschutz" class="fill-control-input" value="true"<?php if($value == 'true'): ?> checked<?php endif ?> >
			<span class="fill-control-indicator"></span>
		</label>
		

      </div>

    <div class="form-element">
      Upload your documents
        <span class="help">Max. 3 PDF files (max. file size 2MB each)</span>
      
      
      <input type="file" name="filefield1" id="datei1"  /><input type="Button" id="loeschbutt1" name="cmdClear" onClick="" value="<?= t('loeschen') ?>"></br> 
	   <input type="file" name="filefield2" id="datei2"  /><input type="Button" id="loeschbutt2" name="cmdClear" onClick="" value="<?= t('loeschen') ?>"></br>  
      <input type="file" name="filefield3" id="datei3"  /><input type="Button" id="loeschbutt3" name="cmdClear" onClick="" value="<?= t('loeschen') ?>"></br></br>  
    </div>
    <input type="submit" name="submit" value="Submit">
</form>


			    </div></div></div></div>

<?php snippet('footer') ?>

   <script>
		      $("#loeschbutt1").click(function(){
  document.getElementById("datei1").value = "";
});
		      $("#loeschbutt2").click(function(){
  document.getElementById("datei2").value = "";
});
		      $("#loeschbutt3").click(function(){
  document.getElementById("datei3").value = "";
});
		      
		      </script>

The controller:

    <?php
return function($kirby, $page) {

    if ($kirby->request()->is('POST') && get('submit')) {

        // initialize variables
        $alerts      = null;
        $attachments = [];

        // check the honeypot
        if (empty(get('website')) === false) {
            go($page->url());
            exit;
        }

        // get the data and validate the other form fields
        $data = [
            'titel'      => get('titel'),
            'name'      => get('name'),
            'strasse'      => get('strasse'),
            'plzort'      => get('plzort'),
            'telefon'      => get('telefon'),
            'fax'      => get('fax'),
            'email'     => get('email'),
            'geburtsdatum'      => get('geburtsdatum'),
            'geburtsort'      => get('geburtsort'),
            'schulabschluss'      => get('schulabschluss'),
            'ausbildung'      => get('ausbildung'),
            'taetigkeiten'      => get('taetigkeiten'),
            'fuehrerschein'      => get('fuehrerschein'),
            'eigenesauto'      => get('eigenesauto'),
            'weiterefragen'      => get('weiterefragen'),
            'datenschutz'      => get('datenschutz')
        ];

        $rules = [
            'titel'      => ['required'],
            'name'      => ['required'],
            'strasse'      => ['required'],
            'plzort'      => ['required'],
            'telefon'      => ['required'],
            'email'     => ['required', 'email'],
            'datenschutz'      => ['required']
        ];

        $messages = [
            'titel'      => 'Bitte geben Sie einen Jobtitel ein.',
            'name'      => 'Bitte geben Sie Ihren Namen ein',
            'strasse'      => 'Bitte geben Sie Ihre Strasse ein',
            'plzort'      => 'Bitte geben Sie Ihre Postleitzahl und Ihren Ort ein.',
            'telefon'      => 'Bitte geben Sie Ihre Telefonnummer ein.',
            'email'     => 'Bitte geben Sie Ihre Emailadresse ein.',
            'datenschutz'     => 'Bitte bestätigen Sie, dass Sie die Datenschutz-Erklärung gelesen haben.'
        ];

        // some of the data is invalid
        if ($invalid = invalid($data, $rules, $messages)) {
            $alerts = $invalid;
        }

         // get the uploads
        $upload1 = $kirby->request()->files()->get('filefield1');
        $upload2 = $kirby->request()->files()->get('filefield2');
        $upload3 = $kirby->request()->files()->get('filefield3');
	
        $uploads = array($upload1, $upload2, $upload3);

        

        // loop through uploads and check if they are valid
        foreach ($uploads as $upload) {
            // make sure the user uploads at least one file
            if ($upload['error'] === 4) {
                //$alerts[] = 'You have to attach at least one file';
            //  make sure there are no other errors  
            } elseif ($upload['error'] !== 0) {
                $alerts[] = 'Die Datei konnte nicht hochgeladen werden.';
            // make sure the file is not larger than 2MB…    
            } elseif ($upload['size'] > 2000000)  {
                $alerts[] = $upload['name'] . ' ist grösser als 2 MB';
            // …and the file is a PDF
            } elseif ($upload['type'] !== 'application/pdf' && $upload['type'] !== 'image/jpeg' && $upload['type'] !== 'application/msword' && $upload['type'] !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
                $alerts[] = $upload['name'] . ' ist keine PDF-, Jpeg- oder Word-Datei. Bitte geben Sie alle Dateien erneut ein.';
            // all valid, try to rename the temporary file
            } else {
                $name     = $upload['tmp_name'];
                $tmpName  = pathinfo($name);
                // sanitize the original filename
                $filename = $tmpName['dirname']. '/'. F::safeName($upload['name']);

                if (rename($upload['tmp_name'], $filename)) {
                    $name = $filename;
                }
                // add the files to the attachments array
                $attachments[] = $name;
            }  
        }

        // the data is fine, let's send the email with attachments
        if (empty($alerts)) {
            try {
                $kirby->email([
                    'template' => 'email2',
                    'from'     => 'bewerbung@company.de',
                    'replyTo'  => $data['email'],
                    'to'       => 'info@company.de',
                    'subject'     => esc($data['name']) . ' hat sich für den Job ' . esc($data['titel'] . ' beworben.'),
                    'data'        => [
                        'titel'   => esc($data['titel']),
                        'name'      => esc($data['name']),
                        'strasse' => esc($data['strasse']),
                        'plzort'   => esc($data['plzort']),
                        'telefon'      => esc($data['telefon']),
                        'fax' => esc($data['fax']),
                        'email'   => esc($data['email']),
                        'geburtsdatum'      => esc($data['geburtsdatum']),
                        'geburtsort' => esc($data['geburtsort']),
                        'schulabschluss'   => esc($data['schulabschluss']),
                        'ausbildung'      => esc($data['ausbildung']),
                        'taetigkeiten' => esc($data['taetigkeiten']),
                        'fuehrerschein'   => esc($data['fuehrerschein']),
                        'eigenesauto'      => esc($data['eigenesauto']),
                        'weiterefragen' => esc($data['weiterefragen']),
                        'datenschutz' => esc($data['datenschutz'])
                    ],
                    'attachments' => $attachments
                ]);
            } catch (Exception $error) {
                // we only display a general error message, for debugging use `$error->getMessage()`
                $alerts[] = "Die Email konnte nicht gesendet werden.";
            }

            // no exception occurred, let's send a success message
            if (empty($alerts) === true) {
                // store reference and name in the session for use on the success page
                $kirby->session()->set([
                    'titel' => esc($data['titel']),
                    'name'  => esc($data['name'])
                ]);
                // redirect to the success page
                go('erfolg-bewerbung');
            }
        }
    }

    // return data to template
    return [
        'alerts' => $alerts ?? null,
        'data'   => $data   ?? false,
    ];
};

Thanks for getting back.

I ended up writing two action methods for kirby-uniform. One modified the uploadAction that moves the files to a temporary folder and saves a reference in the $form object and then move on to a modified emailAction that attaches the files to the mail.

<?php
	if ($kirby->request()->is('POST')) { //this handles the validation
		$form
			->attachmentUploadAction(['fields' => [
					'bewerbung' => [
						'target' => $path 
					],
				]
			])
			->emailAttachmentAction(
				[
					'to' => 'me@example.com',
					'from' => 'info@example.com',
					'subject' => 'Besucheranmeldung für {{werk}}',
					'template' => 'standard',
					'attachments' => null
				]
			)
			->logAction([
				'file' => $path . '/messages.log',
			])
			// TODO: remove attachements after sending
			//->removeAttachmentAction([
			//])
			;
	}

I used the kirby-uniform uploadAction and added this

if ($success) {			
  $this->form->attachments[] = $path;
}
<?php

namespace Uniform\Actions;

use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\F;

/**
 * Action to set a recipient email address and send the form data via email.
 */
class AttachmentUploadAction extends EmailAction
{
	/**
	 * Paths of directories that were created by this action.
	 *
	 * @var array
	 */
	protected $createdDirectories = [];

	/**
	 * Paths of files that were created by this action.
	 *
	 * @var array
	 */
	protected $createdFiles = [];

	/**
	 * Move uploaded files to their target directory.
	 *
	 */
	public function perform()
	{
	    $fields = $this->requireOption('fields');

	    foreach ($fields as $field => $options) {
	        $this->handleFile($field, $options);
	    }
	}

	/**
	 * Move a single uploaded file.
	 *
	 * @param string $field Form field name.
	 * @param array $options
	 */
	protected function handleFile($field, $options)
	{
	    $file = $this->form->data($field);

	    if (!is_array($file) || !isset($file['error']) || intval($file['error']) !== UPLOAD_ERR_OK) {
	        // If this is an array, kirby-form already recognized and validated the
	        // uploaded file. If the file is required, this should have been checked
	        // during validation.
	        return;
	    }

	    if (!array_key_exists('target', $options)) {
	        // No translation because this is a developer error.
	        $this->fail("The target directory is missing for field {$field}.");
	    }

	    $target = $options['target'];

	    if (!is_dir($target)) {
	        if (@mkdir($target, 0755, true)) {
	            $this->createdDirectories[] = $target;
	        } else {
	            $this->fail(I18n::translate('uniform-upload-mkdir-fail'), $field);
	        }
	    }

	    $name = $file['name'];
	    $prefix = A::get($options, 'prefix');

	    if (is_null($prefix)) {
	        $name = $this->getRandomPrefix($name);
	    } elseif ($prefix !== false) {
	        $name = $prefix.$name;
	    }

	    $path = $target.DIRECTORY_SEPARATOR.$name;
	    if (is_file($path)) {
	        $this->fail(I18n::translate('uniform-upload-exists'), $field);
	    }

	    $success = $this->moveFile($file['tmp_name'], $path);

	    if ($success) {
			$this->createdFiles[] = $path;
			
			$this->form->attachments[] = $path;
	
	    } else {
	        $this->fail(I18n::translate('uniform-upload-failed'), $field);
	    }
	}

	/**
	 * {@inheritdoc}
	 */
	protected function fail($message = null, $key = null)
	{
	    array_map('unlink', $this->createdFiles);
	    array_map('rmdir', $this->createdDirectories);
	    parent::fail($message, $key);
	}

	/**
	 * Move the uploaded file
	 *
	 * @param string $source
	 * @param string $target
	 *
	 * @return bool
	 */
	protected function moveFile($source, $target)
	{
	    return move_uploaded_file($source, $target);
	}

	/**
	 * Adds a random prefix to the name
	 *
	 * @param string $name The name
	 * @param integer $length Length of the prefix
	 *
	 * @return Name with prefix, sepatated by a '_'
	 */
	protected function getRandomPrefix($name, $length = 10)
	{
	    $prefix = bin2hex(random_bytes(intval($length / 2)));

	    return "{$prefix}_{$name}";
	}
}
<?php

namespace Uniform\Actions;

use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\F;

/**
 * Action to set a recipient email address and send the form data via email.
 */
class EmailAttachmentAction extends EmailAction
{

	/**
	 * Set the chosen recipient email address and send the form data via email.
	 */
	public function perform()
	{
		if (empty($this->form->attachments)) {
			unset($this->options['attachments']);
			return parent::perform();
		}
		// put those attachments back into the options.
		$this->options['attachments'] = $this->form->attachments;

		return parent::perform();
	}
}

Hope that helps future problem solving with that task :slight_smile:

2 Likes

Just as a follow-up: Did you improve this workflow in the meantime, @marcus-at-localhost? I’m about to do something similar, and don’t want to invent the wheel anew … :slight_smile:

I think I used it as described, and you should be able to reuse the code I posted or build on it. Would love to hear if you have made any improvements. :slight_smile:

Edit: Sorry for the late response :confused:

No sweat! If you like, have a look here: refbw.de/jobs.upload.php at main - refbw.de - Codeberg.org

1 Like