Hire the author: Ngei S
Introduction
In Laravel, sending multiple emails and attaching several documents is one of the key features in every application. Being able to write several lines of code and be able to send an email, has its own satisfaction. Well, Laravel just like any other framework today can do so. My aim in this project was to emulate what any email service provider today can do. For instance, be able to send an email, carbon copy (CC), have a subject and body, and attach several documents regardless of the type of the file (PDF, Image, SVG’s, etc).
See here to view my code. Image from cloudways.com.
Glossary
- Composer – is a tool for dependency management in PHP. It is a great tool to have mostly since you can do a lot with it
- Git – Git is a free and open-source distributed version control system designed to handle everything from small to very large projects with speed and efficiency. If you are using Windows git will help you like when you right-click you can open the terminal on any directory.
- Laravel – Laravel is a web application framework with an expressive, elegant syntax. They’ve already laid the foundation — freeing you to create without sweating the small things.
- PHP – PHP is a popular general-purpose scripting language especially suited for web development. Which is fast, flexible, and pragmatic. So, PHP powers everything from your blog to the most popular websites in the world.
- XAMPP – XAMPP is a completely free, easy to install Apache distribution containing MariaDB, PHP, and Perl. Because XAMPP is an open source package, is set up to be incredibly easy to install and to use.
- Browser – a computer program with a graphical user interface for displaying HTML files, it is used to navigate the World Wide Web.
NOTE: For this tutorial, I used a Xampp web server.
Step by Step Procedure
Setup
Laravel has its own server shipped with it. Since we are going to save all emails sent, we require a local server. Hence there is clear documentation online on how to install the above-mentioned software on their official sites depending on the operating system that you are using.
To check if you have installed xampp on:
Windows – Clicking the start button and search “XAMPP” xampp control panel is supposed to appear from your search history.
Linux – Run the following command. This command starts xampp web server
sudo /opt/lampp/lampp start
To check if you have composer installed, open your terminal and run the command, composer. The terminal outputs the following
______ | |
/ ____/___ ____ ___ ____ ____ ________ _____ | |
/ / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ | |
/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / | |
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ | |
/_/ | |
Composer version 2.1.5 2021-07-23 10:35:47 | |
Usage: | |
command [options] [arguments] | |
Options: | |
-h, --help Display this help message | |
-q, --quiet Do not output any message | |
-V, --version Display this application version | |
--ansi Force ANSI output | |
--no-ansi Disable ANSI output | |
-n, --no-interaction Do not ask any interactive question | |
--profile Display timing and memory usage information | |
--no-plugins Whether to disable plugins. | |
-d, --working-dir=WORKING-DIR If specified, use the given directory as working directory. | |
--no-cache Prevent use of the cache | |
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug | |
Available commands: | |
about Shows a short information about Composer. | |
archive Creates an archive of this composer package. | |
browse Opens the package's repository URL or homepage in your browser. | |
cc Clears composer's internal package cache. | |
check-platform-reqs Check that platform requirements are satisfied. | |
clear-cache Clears composer's internal package cache. | |
clearcache Clears composer's internal package cache. | |
config Sets config options. | |
create-project Creates new project from a package into given directory. | |
depends Shows which packages cause the given package to be installed. | |
diagnose Diagnoses the system to identify common errors. | |
dump-autoload Dumps the autoloader. | |
dumpautoload Dumps the autoloader. | |
exec Executes a vendored binary/script. | |
fund Discover how to help fund the maintenance of your dependencies. | |
global Allows running commands in the global composer dir ($COMPOSER_HOME). | |
help Displays help for a command | |
home Opens the package's repository URL or homepage in your browser. | |
i Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json. | |
info Shows information about packages. | |
init Creates a basic composer.json file in current directory. | |
install Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json. | |
licenses Shows information about licenses of dependencies. | |
list Lists commands | |
outdated Shows a list of installed packages that have updates available, including their latest version. | |
prohibits Shows which packages prevent the given package from being installed. | |
reinstall Uninstalls and reinstalls the given package names | |
remove Removes a package from the require or require-dev. | |
require Adds required packages to your composer.json and installs them. | |
run Runs the scripts defined in composer.json. | |
run-script Runs the scripts defined in composer.json. | |
search Searches for packages. | |
self-update Updates composer.phar to the latest version. | |
selfupdate Updates composer.phar to the latest version. | |
show Shows information about packages. | |
status Shows a list of locally modified packages. | |
suggests Shows package suggestions. | |
u Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file. | |
update Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file. | |
upgrade Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file. | |
validate Validates a composer.json and composer.lock. | |
why Shows which packages cause the given package to be installed. | |
why-not Shows which packages prevent the given package from being installed. |
Once done with the setup, we will be able to code our small app sending multiple emails with attachments using laravel. Sending an email from your own application is always interesting but for beginners, it’s always a real pain. For beginners, they need to figure out what to loop, configuration, and all that stuff.
Step 1: Installing Laravel
To install laravel just run the following command. projectName is the name of the project/folder.
composer create-project laravel/laravel projectName
Once laravel has been installed you can change the directory to the project folder. To access the newly installed laravel run the following command
php artisan serve
Another way to access is by moving your project to the htdocs folder.
127.0.0.1/projectName/public/
Step 2: Creating a Model, Migration and Controller.
To create the above files you only need to run the below command. Once created, we are going to start with the controller.
php artisan make:model Attachment -mc
Step 3: Creating Database and Updating Migration
Before we get started we need to create a database and update our .env file. Once you are done, head over to the database/migration and open your newly created migration. Update it as follows:
<?php | |
use Illuminate\Database\Migrations\Migration; | |
use Illuminate\Database\Schema\Blueprint; | |
use Illuminate\Support\Facades\Schema; | |
class CreateAttachmentsTable extends Migration | |
{ | |
/** | |
* Run the migrations. | |
* | |
* @return void | |
*/ | |
public function up() | |
{ | |
Schema::create('attachments', function (Blueprint $table) { | |
$table->id(); | |
$table->string('email'); | |
$table->string('cc'); | |
$table->string('subject'); | |
$table->string('message'); | |
$table->text('attachments'); | |
$table->string('send')->nullable(); | |
$table->timestamps(); | |
}); | |
} | |
/** | |
* Reverse the migrations. | |
* | |
* @return void | |
*/ | |
public function down() | |
{ | |
Schema::dropIfExists('attachments'); | |
} | |
} |
Once done run the following command
php artisan migrate.
This creates all unmigrated migrations and pushes them to your database.
Step 4: Updating already created controller.
Once done head over to your controllers. Our first method inside our controller will be the one to access our view file. So it will look as follows
<?php | |
public function index() | |
{ | |
$users = User::orderBy('id')->get(); | |
$emails = Attachment::orderBy('id')->get(); | |
return view('welcome', compact('users', 'emails')); | |
} |
In the above method, we are returning the view welcome (which is shipped with laravel). The next step is to create a send method. Its code will be as follows. Since we are saving all emails send, the index method is returning our view file, all users saved and emails stored.
<?php | |
namespace App\Http\Controllers; | |
use App\Models\Attachment; | |
use App\Models\User; | |
use Illuminate\Http\Request; | |
use Illuminate\Support\Facades\DB; | |
use Mail; | |
class AttachmentController extends Controller | |
{ | |
// | |
public function index() | |
{ | |
$users = User::orderBy('id')->get(); | |
$emails = Attachment::orderBy('id')->get(); | |
return view('welcome', compact('users', 'emails')); | |
} | |
// | |
public function send(Request $request) | |
{ | |
$data = request()->validate([ | |
'email' => 'required', | |
'cc' => 'required', | |
'subject' => 'min:2|required', | |
'message' => 'required', | |
'attachments' => 'max:5060', | |
]); | |
$message = new Attachment(); | |
$message->email = $data['email']; | |
$message->cc = $data['cc']; | |
$message->subject = $data['subject']; | |
$message->message = $data['message']; | |
foreach ($request->attachments as $file) { | |
$filename =$file->getClientOriginalName(); | |
$path = $file->store('emails', 'public'); | |
$message->attachments = $path; | |
} | |
$message->save(); | |
if ($message) { | |
$count = 1; | |
foreach ($data['cc'] as $cc) { | |
$count++; | |
} | |
DB::update('update attachments set send=? where id=?', [$count, $message->id]); | |
} | |
$emailData = array( | |
'email' => $data['email'], | |
'cc' => $data['cc'], | |
'subject' => $data['subject'], | |
'message' => $data['message'], | |
'attachments' => $path, | |
); | |
view()->share(compact('emailData')); | |
$files = $request->attachments; | |
\Mail::send('mails', function ($message) use ($data, $file,$files, $path) { | |
$message->to($data['email']); | |
$message->cc($data['cc']); | |
$message->from(env('MAIL_FROM_ADDRESS')); | |
$message->subject($data['subject']); | |
foreach ($files as $f){ | |
$message->attach( | |
$f->getRealPath(),array( | |
'as'=>$f->getClientOriginalName(), | |
'mime'=>$f->getMimeType(), | |
) | |
); | |
} | |
}); | |
return redirect()->back(); | |
} | |
} |
The send method is the one I am using to send emails and store the data. First I am validating all inputs. Then I save the data. I wanted to count all emails that are being sent, and that is where the count is coming in. For each loop, it’s counting one. It will count all emails in the cc input. I had set the count to be one because of the primary email.
After saving, we are now able to send emails. The view()->share() we are making the variable $emailData available globally. In Laravel, we use the Mail::send function to send emails. To be able to achieve our main goal which is to send all emails with attachments that have been selected, the most important part is to make sure to loop through all files. You might encounter an error when trying to send. I had previously written a blog about how to address that error. In case you encounter click this link. The model file is a connection to your migration. In ORM(object-relational mapper), each model represents a table which we call migrations.
Step 5: Updating .env File
Your .env file is supposed to look as follows. We are mostly updating settings for mail. I have used google mail to do the task. For Google to be able to send email from a third party, you need to authorize it. To do so, on your Gmail profile and click manage your google account, head over to security, and then turn on less secure app access.
APP_NAME=Laravel | |
APP_ENV=local | |
APP_KEY=base64:SxrEf+ScYiqJt9eiQ8cUUCJGE3CvUD96XsZO7n40lQ0= | |
APP_DEBUG=true | |
APP_URL=http://localhost | |
LOG_CHANNEL=stack | |
LOG_LEVEL=debug | |
DB_CONNECTION=mysql | |
DB_HOST=127.0.0.1 | |
DB_PORT=3306 | |
DB_DATABASE=multipleE | |
DB_USERNAME=root | |
DB_PASSWORD= | |
BROADCAST_DRIVER=log | |
CACHE_DRIVER=file | |
FILESYSTEM_DRIVER=local | |
QUEUE_CONNECTION=sync | |
SESSION_DRIVER=file | |
SESSION_LIFETIME=120 | |
MEMCACHED_HOST=127.0.0.1 | |
REDIS_HOST=127.0.0.1 | |
REDIS_PASSWORD=null | |
REDIS_PORT=6379 | |
MAIL_DRIVER=smtp | |
MAIL_HOST=smtp.googlemail.com | |
MAIL_PORT=465 | |
MAIL_USERNAME=yourmail | |
MAIL_PASSWORD=yourmailpassword | |
MAIL_FROM_ADDRESS=yourmail | |
MAIL_FROM_NAME=yourmail/or random name | |
MAIL_ENCRYPTION=ssl | |
AWS_ACCESS_KEY_ID= | |
AWS_SECRET_ACCESS_KEY= | |
AWS_DEFAULT_REGION=us-east-1 | |
AWS_BUCKET= | |
AWS_USE_PATH_STYLE_ENDPOINT=false | |
PUSHER_APP_ID= | |
PUSHER_APP_KEY= | |
PUSHER_APP_SECRET= | |
PUSHER_APP_CLUSTER=mt1 | |
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" | |
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" |
Step 6: Updating our welcome view file.
On your welcome.blade.php file update it as follows.
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"> | |
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script> | |
<script src = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/js/bootstrap-multiselect.min.js"> | |
</script> | |
<link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/css/bootstrap-multiselect.css"/> | |
<title>Mailer</title> | |
</head> | |
<body> | |
<div class="container"> | |
<h2>Send Multiple Mails and Attachments</h2> | |
@if(count($errors)>0) | |
@foreach($errors->all() as $error) | |
<li class="" role="alert"> | |
<strong>{{$error}}!</strong> | |
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> | |
<span aria-hidden="true">×</span> | |
</button> | |
</li> | |
@endforeach | |
@endif | |
<div class="card"> | |
<div class="card-body"> | |
<button type="button" class="btn btn-primary btn-sm mb-3" data-toggle="modal" data-target="#sendEmail"> | |
Send Email | |
</button> | |
<table class="table table-bordered table-stripped"> | |
<thead> | |
<tr> | |
<th>#</th> | |
<th>Email</th> | |
<th>CC</th> | |
<th>Subject</th> | |
<th>Message</th> | |
<th>Files</th> | |
</tr> | |
</thead> | |
<tbody> | |
<?php $count=1?> | |
@foreach($emails as $email) | |
<tr> | |
<td>{{$count++}}</td> | |
<td>{{$email->email}}</td> | |
<td> | |
@foreach($email->cc as $cc) | |
<li style="list-style: none">{{$cc}}</li> | |
@endforeach | |
</td> | |
<td>{{$email->subject}}</td> | |
<td>{{$email->message}}</td> | |
<td>{{$email->message}}</td> | |
</tr> | |
@endforeach | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<div class="modal fade" id="sendEmail" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<form method="POST" action="{{url('/send')}}" enctype="multipart/form-data"> | |
@csrf | |
<div class="modal-body"> | |
<div class="row"> | |
<div class="form-group"> | |
<label class="col-form-label">Email</label> | |
<select name="email" class="form-control"> | |
@forelse($users as $user) | |
<option >{{$user->email}}</option> | |
@empty | |
<option >No User</option> | |
@endforelse | |
</select> | |
</div> | |
<div class="form-group"> | |
<label class="col-form-label">CC</label> | |
<select id = "mltislct" name="cc[]" multiple = "multiple"> | |
@forelse($users as $user) | |
<option >{{$user->email}}</option> | |
@empty | |
<option >No User</option> | |
@endforelse | |
</select> | |
</div> | |
<div class="form-group"> | |
<label class="col-form-label">Subject</label> | |
<input type="text" class="form-control" name="subject"> | |
</div> | |
<div class="form-group"> | |
<label class="col-form-label">Message</label> | |
<textarea class="form-control" name="message"></textarea> | |
</div> | |
<div class="form-group"> | |
<label class="col-form-label">Attachment</label> | |
<input type="file" class="form-control" name="attachments[]" multiple> | |
</div> | |
</div> | |
</div> | |
<div class="modal-footer justify-content-center"> | |
<button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal"> | |
Close | |
</button> | |
<button type="submit" class="btn btn-sm btn-success"> | |
Send | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
$(document).ready(function() { | |
$('#mltislct').multiselect({ | |
includeSelectAllOption: true, | |
enableFiltering: true, | |
enableCaseInsensitiveFiltering: true, | |
filterPlaceholder:'Search Here..' | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Once done, create a mails.blade.php file since we require it to send to be able to send the emails. This view file will be just a simple HTML file. you can customize it to your liking though.
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" | |
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>Mailer</title> | |
</head> | |
<body> | |
{{$emailData->message}} | |
</body> | |
</html> |
Step 7: Routes
Update your routes as follows. Note that, laravel 8 routes are very different from other versions of laravel
<?php | |
use Illuminate\Support\Facades\Route; | |
use App\Http\Controllers\AttachmentController; | |
/* | |
|-------------------------------------------------------------------------- | |
| Web Routes | |
|-------------------------------------------------------------------------- | |
| | |
| Here is where you can register web routes for your application. These | |
| routes are loaded by the RouteServiceProvider within a group which | |
| contains the "web" middleware group. Now create something great! | |
| | |
*/ | |
Route::get('/',[AttachmentController::class,'index']); | |
Route::post('/send',[AttachmentController::class,'send']); | |
Auth::routes(); | |
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); |
Step 8: Serving our application.
To serve Laravel just run the following command
php artisan serve
This creates a default port 8000 and creates a clickable link for you which is http://localhost:8000/. Once you click the link you will be able to access our application. It is supposed to look as follows.

Now you can send emails. Make sure your user’s table has some valid emails.
Learning Tools
There are a lot of learning tools you can use to equip you which may include;
- Laravel Docs – The no 1 website stuck you supposed to reference first before searching elsewhere.
- PHP – For any PHP related problem always refer here
- Stack Overflow – This is a community of developers, you can find some answers and some may never work for you but its a good place to check out.
Leaning Strategy
To be able to complete this blog project, I used the above tools. I recommend the use of google search to the maximum level. If by any chance you’re experiencing errors it is okay to ask for help from colleagues or developers communities around the globe.
Reflective Analysis
Sending emails using laravel can sometimes be challenging if you’re a beginner. Once you completely understand how to do it, things become easier, and you become less of a beginner. If you are building that for production I would recommend you use cron jobs or queues. For more information, you can read on it on Laravel docs.
Conclusion
The above processes will show you how to attach multiple attachments, and carbon copy emails when sending an email to several users at once.
One of the Future directions of such a technology is that users apart from sending multiple emails and attachments, they can also be to choose a custom date to send notification/emails
Yes this is achievable and also a fun project to do. with vuejs the page doesn’t reload everything is sent to the backend to do the rest of the job. It optimizes your project hence 0 load time. very fun
I will try 🙂