diff --git a/README.md b/README.md index 20955ef..f13e42d 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,27 @@ MailTracker will hook into all outgoing emails from Laravel/Lumen and inject a tracking code into it. It will also store the rendered email in the database. There is also an interface to view sent emails. +## Upgrade from 1.x + +First, upgrade to version 2.0 by running: + +``` bash +$ composer require jdavidbakr/mail-tracker ~2.0 +``` + +Version 2.0 contains a new model that tracks the links that were clicked on. This requires a migration to create the table. There are also additional changes to the config file. For best results, make a backup copy of config/mail-tracker.php to restore any values you have customized, then delete that file and run + +``` bash +$ php artisan vendor:publish +$ php artisan migrate +``` + ## Install (Laravel) Via Composer ``` bash -$ composer require jdavidbakr/mail-tracker +$ composer require jdavidbakr/mail-tracker ~2.0 ``` Add the following to the providers array in config/app.php: @@ -22,7 +37,7 @@ jdavidbakr\MailTracker\MailTrackerServiceProvider::class, Publish the config file and migration ``` bash -$ php artisan vendor:publish +$ php artisan vendor:publish --provider='jdavidbakr\MailTracker\MailTrackerServiceProvider' ``` Run the migration @@ -35,7 +50,7 @@ $ php artisan migrate Via Composer ``` bash -$ composer require jdavidbakr/mail-tracker +$ composer require jdavidbakr/mail-tracker ~2.0 ``` Register the following service provider in bootstrap/app.php @@ -55,11 +70,14 @@ $ php artisan migrate Once installed, all outgoing mail will be logged to the database. The following config options are available in config/mail-tracker.php: +* **name**: set your App Name. * **inject-pixel**: set to true to inject a tracking pixel into all outgoing html emails. * **track-links**: set to true to rewrite all anchor href links to include a tracking link. The link will take the user back to your website which will then redirect them to the final destination after logging the click. * **expire-days**: How long in days that an email should be retained in your database. If you are sending a lot of mail, you probably want it to eventually expire. Set it to zero to never purge old emails from the database. * **route**: The route information for the tracking URLs. Set the prefix and middlware as desired. -* **admin-route**: The route information for the admin. Set the prefix and middleware. *Note that this is not yet built.* +* **admin-route**: The route information for the admin. Set the prefix and middleware. +* **admin-template**: The params for the Admin Panel and Views. You can integrate your existing Admin Panel with the MailTracker admin panel. +* **date-format**: You can define the format to show dates in the Admin Panel. ## Events @@ -116,10 +134,18 @@ protected $listen = [ ], ]; ``` +## Views + +When you do the php artisan vendor:publish simple views will add to your resources/views/vendor/emailTrakingViews and you can customize. -## TODO +## Admin Panel -Currently this plugin is only tracking the outgoing mail. There is no view yet to explore the existing data. +Config your admin-route in the config file. Set the prefix and middlware. +The route name is 'mailTracker_Index'. The standard admin panel route is located at /email-manager. +You can use route names to include them into your existing admin menu. +You can customize your route in the config file. +You can see all sent emails, total opens, total urls clicks, show individuals emails and show the urls clicked details. +All views (email tamplates, panel) can be customized in resources/views/vendor/emailTrakingViews. ## Contributing diff --git a/config/mail-tracker.php b/config/mail-tracker.php index c9b559c..aa234f9 100644 --- a/config/mail-tracker.php +++ b/config/mail-tracker.php @@ -1,27 +1,27 @@ true, - - /** - * To disable injecting tracking links, set this to false. - */ - 'track-links'=>true, - - /** - * Optionally expire old emails, set to 0 to keep forever. - */ - 'expire-days'=>60, - - /** - * Where should the pingback URL route be? - */ + /** + * To disable the pixel injection, set this to false. + */ + 'inject-pixel'=>true, + + /** + * To disable injecting tracking links, set this to false. + */ + 'track-links'=>true, + + /** + * Optionally expire old emails, set to 0 to keep forever. + */ + 'expire-days'=>60, + + /** + * Where should the pingback URL route be? + */ 'route' => [ 'prefix' => 'email', - 'middleware' => [], + 'middleware' => ['web'], ], /** @@ -29,7 +29,29 @@ */ 'admin-route' => [ 'prefix' => 'email-manager', - 'middleware' => 'super', + 'middleware' => ['web'], ], + /** + * Admin Tamplate + * example + * 'name' => 'layouts.app' for Default emailTraking use 'emailTrakingViews::layouts.app' + * 'section' => 'content' for Default emailTraking use 'content' + * 'styles_section' => 'styles' for Default emailTraking use 'styles' + */ + 'admin-template' => [ + 'name' => 'emailTrakingViews::layouts.app', + 'section' => 'content', + ], + + /** + * Number of emails per page in the admin view + */ + 'emails-per-page'=>5, + + /** + * Date Format + */ + 'date-format' => 'm/d/Y g:i a', + ]; diff --git a/migrations/2016_03_01_193027_create_sent_emails_table.php b/migrations/2016_03_01_193027_create_sent_emails_table.php index 96daa7a..35b1194 100644 --- a/migrations/2016_03_01_193027_create_sent_emails_table.php +++ b/migrations/2016_03_01_193027_create_sent_emails_table.php @@ -14,7 +14,6 @@ public function up() { Schema::create('sent_emails', function (Blueprint $table) { $table->increments('id'); - $table->timestamps(); $table->char('hash',32)->unique(); $table->text('headers'); $table->string('sender'); @@ -23,6 +22,7 @@ public function up() $table->text('content'); $table->integer('opens'); $table->integer('clicks'); + $table->timestamps(); }); } diff --git a/migrations/2016_09_07_193027_create_sent_emails_Url_Clicked_table.php b/migrations/2016_09_07_193027_create_sent_emails_Url_Clicked_table.php new file mode 100644 index 0000000..1b161b0 --- /dev/null +++ b/migrations/2016_09_07_193027_create_sent_emails_Url_Clicked_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->integer('sent_email_id')->unsigned(); + $table->foreign('sent_email_id')->references('id')->on('sent_emails')->onDelete('cascade'); + $table->string('url'); + $table->char('hash',32); + $table->integer('clicks')->default('1'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('sent_emails_url_clicked'); + } +} diff --git a/src/AdminController.php b/src/AdminController.php new file mode 100644 index 0000000..bfae571 --- /dev/null +++ b/src/AdminController.php @@ -0,0 +1,86 @@ +$request->search]); + return redirect(route('mailTracker_Index')); + } + + /** + * Clear search + */ + public function clearSearch() + { + session(['mail-tracker-index-search'=>null]); + return redirect(route('mailTracker_Index')); + } + + /** + * Index. + * + * @return \Illuminate\Http\Response + */ + public function getIndex() + { + session(['mail-tracker-index-page'=>request()->page]); + $search = session('mail-tracker-index-search'); + + $query = SentEmail::query(); + + if($search) { + $terms = explode(" ",$search); + foreach($terms as $term) { + $query->where(function($q) use($term) { + $q->where('sender','like','%'.$term.'%') + ->orWhere('recipient','like','%'.$term.'%') + ->orWhere('subject','like','%'.$term.'%'); + }); + } + } + + $emails = $query->paginate(config('mail-tracker.emails-per-page')); + + return \View('emailTrakingViews::index')->with('emails', $emails); + } + + /** + * Show Email. + * + * @return \Illuminate\Http\Response + */ + public function getShowEmail($id) + { + $email = SentEmail::where('id',$id)->first(); + return \View('emailTrakingViews::show')->with('email', $email); + } + + /** + * Url Detail. + * + * @return \Illuminate\Http\Response + */ + public function getUrlDetail($id) + { + $detalle = SentEmailUrlClicked::where('sent_email_id',$id)->get(); + return \View('emailTrakingViews::url_detail')->with('details', $detalle); + } +} diff --git a/src/MailTrackerController.php b/src/MailTrackerController.php index f99107d..f84fcd7 100644 --- a/src/MailTrackerController.php +++ b/src/MailTrackerController.php @@ -45,6 +45,17 @@ public function getL($url, $hash) if($tracker) { $tracker->clicks++; $tracker->save(); + $url_clicked = Model\SentEmailUrlClicked::where('url',$url)->where('hash', $hash)->first(); + if ($url_clicked) { + $url_clicked->clicks++; + $url_clicked->save(); + } else { + $url_clicked = Model\SentEmailUrlClicked::create([ + 'sent_email_id' => $tracker->id, + 'url' => $url, + 'hash' => $tracker->hash, + ]); + } Event::fire(new LinkClickedEvent($tracker)); } diff --git a/src/MailTrackerServiceProvider.php b/src/MailTrackerServiceProvider.php index 42e31ad..8dc006d 100644 --- a/src/MailTrackerServiceProvider.php +++ b/src/MailTrackerServiceProvider.php @@ -9,10 +9,10 @@ class MailTrackerServiceProvider extends ServiceProvider { /** * Check to see if we're using lumen or laravel. - * + * * @return bool */ - public function isLumen() + public function isLumen() { $lumenClass = 'Laravel\Lumen\Application'; return ($this->app instanceof $lumenClass); @@ -33,6 +33,13 @@ public function boot() $this->publishes([ __DIR__.'/../migrations/2016_03_01_193027_create_sent_emails_table.php' => database_path('migrations/2016_03_01_193027_create_sent_emails_table.php') ], 'config'); + $this->publishes([ + __DIR__.'/../migrations/2016_09_07_193027_create_sent_emails_Url_Clicked_table.php' => database_path('migrations/2016_09_07_193027_create_sent_emails_Url_Clicked_table.php') + ], 'config'); + $this->loadViewsFrom(__DIR__.'/views', 'emailTrakingViews'); + $this->publishes([ + __DIR__.'/views' => base_path('resources/views/vendor/emailTrakingViews'), + ]); } // Hook into the mailer @@ -45,14 +52,39 @@ public function boot() if (!$this->isLumen()) { Route::group($config, function() { - Route::get('t/{hash}', 'MailTrackerController@getT'); - Route::get('l/{url}/{hash}', 'MailTrackerController@getL'); + Route::get('t/{hash}', 'MailTrackerController@getT')->name('mailTracker_t'); + Route::get('l/{url}/{hash}', 'MailTrackerController@getL')->name('mailTracker_l'); }); } else { $app = $this->app; $app->group($config, function () use ($app) { - $app->get('t', 'MailTrackerController@getT'); - $app->get('l', 'MailTrackerController@getL'); + $app->get('t', 'MailTrackerController@getT')->name('mailTracker_t'); + $app->get('l', 'MailTrackerController@getL')->name('mailTracker_l'); + }); + } + // Install the Admin routes + $config_admin = $this->app['config']->get('mail-tracker.admin-route', []); + $config_admin['namespace'] = 'jdavidbakr\MailTracker'; + + if (!$this->isLumen()) { + Route::group($config_admin, function() + { + Route::get('/', 'AdminController@getIndex')->name('mailTracker_Index'); + Route::post('search', 'AdminController@postSearch')->name('mailTracker_Search'); + Route::get('clear-search', 'AdminController@clearSearch')->name('mailTracker_ClearSearch'); + Route::get('show-email/{id}', 'AdminController@getShowEmail')->name('mailTracker_ShowEmail'); + Route::get('url-detail/{id}', 'AdminController@getUrlDetail')->name('mailTracker_UrlDetail'); + Route::get('send-email', 'AdminController@getSendEmail')->name('mailTracker_SendEmail'); + }); + } else { + $app = $this->app; + $app->group($config_admin, function () use ($app) { + $app->get('/', 'AdminController@getIndex')->name('mailTracker_Index'); + $app->post('search', 'AdminController@postSearch')->name('mailTracker_Search'); + $app->get('clear-search', 'AdminController@clearSearch')->name('mailTracker_ClearSearch'); + $app->get('show-email/{id}', 'AdminController@getShowEmail')->name('mailTracker_ShowEmail'); + $app->get('url-detail/{id}', 'AdminController@getUrlDetail')->name('mailTracker_UrlDetail'); + $app->get('send-email', 'AdminController@getSendEmail')->name('mailTracker_SendEmail'); }); } } diff --git a/src/Model/SentEmailUrlClicked.php b/src/Model/SentEmailUrlClicked.php new file mode 100644 index 0000000..d5bea01 --- /dev/null +++ b/src/Model/SentEmailUrlClicked.php @@ -0,0 +1,23 @@ +belongsTo(SentEmail::class,'sent_email_id'); + } +} diff --git a/src/views/emails/mensaje.blade.php b/src/views/emails/mensaje.blade.php new file mode 100644 index 0000000..1e4dd69 --- /dev/null +++ b/src/views/emails/mensaje.blade.php @@ -0,0 +1,30 @@ +@extends('emailTrakingViews::emails/mensaje_layout') +@section('title') + Message from {{config('mail-tracker.name')}} +@endsection + +@section('preheader') + Message from {{config('mail-tracker.name')}}
+@endsection +@section('nombre_destinatario') + {{ $data['name'] }} +@endsection +@section('mensaje') +

Static Email Title

+

+ Static Email Content +

+ {{ $data['message'] }} +@endsection +@section('href_call_to_action') + {{env('APP_URL')}} +@endsection +@section('txt_call_to_action') + Call To Action +@endsection +@section('txt_extra') + This email comes from {{config('mail-tracker.name')}} +@endsection +@section('saludo_final') + Regards +@endsection diff --git a/src/views/emails/mensaje_layout.blade.php b/src/views/emails/mensaje_layout.blade.php new file mode 100644 index 0000000..1d1f0c4 --- /dev/null +++ b/src/views/emails/mensaje_layout.blade.php @@ -0,0 +1,195 @@ + + + + + + @yield('title') + + + + + + + + + +
  +
+ + + + + + + + + + + +
+ + + + + + + +
+ + {{config('mail-tracker.name')}} + +
+

Hi @yield('nombre_destinatario')

+

@yield('mensaje')

+ + + + + + +
+ + + + + + +
+ @yield('txt_call_to_action') + +
+
+

@yield('txt_extra')

+

@yield('saludo_final')

+
+
+ + + + + + +
+
 
+ + diff --git a/src/views/index.blade.php b/src/views/index.blade.php new file mode 100644 index 0000000..db11c62 --- /dev/null +++ b/src/views/index.blade.php @@ -0,0 +1,71 @@ +@extends(config('mail-tracker.admin-template.name')) +@section(config('mail-tracker.admin-template.section')) +
+
+
+

Mail Tracker

+
+
+
+
+
+ {!! csrf_field() !!} +
+ + +
+ + +
+
+
+
+
+
+ + + + + + + + + + + @foreach($emails as $email) + + + + + + + + + + @endforeach +
RecipientSubjectOpensClicksSend AtView EmailClicks
{{$email->recipient}}{{$email->subject}}{{$email->opens}}{{$email->clicks}}{{$email->created_at->format(config('mail-tracker.date-format'))}} + View + + @if($email->clicks > 0) + Url Report + @else + No Clicks + @endif +
+
+
+
+
+ {!! $emails->render() !!} +
+
+
+@endsection diff --git a/src/views/layouts/app.blade.php b/src/views/layouts/app.blade.php new file mode 100644 index 0000000..9d55d24 --- /dev/null +++ b/src/views/layouts/app.blade.php @@ -0,0 +1,13 @@ + + + + + + + Mail Tracker + + + + @yield('content') + + \ No newline at end of file diff --git a/src/views/show.blade.php b/src/views/show.blade.php new file mode 100644 index 0000000..9c4e553 --- /dev/null +++ b/src/views/show.blade.php @@ -0,0 +1 @@ +{!!$email->content!!} diff --git a/src/views/url_detail.blade.php b/src/views/url_detail.blade.php new file mode 100644 index 0000000..45e3449 --- /dev/null +++ b/src/views/url_detail.blade.php @@ -0,0 +1,50 @@ +@extends(config('mail-tracker.admin-template.name')) +@section(config('mail-tracker.admin-template.section')) +
+
+
+

Mail Tracker

+
+
+

+ All Sent Emails +

+
+
+
+
+

+ Clicked URLs for Email ID {{$details->first()->email->id}} +

+ + View Message + +
+
+
+
+ Recipient: {{$details->first()->email->recipient}}
+ Subject: {{$details->first()->email->subject}}
+ Sent At: {{$details->first()->email->created_at->format(config('mail-tracker.date-format'))}} +
+
+
+
+ + + + + + @foreach($details as $detail) + + + + + + + @endforeach +
UrlClicksFirst Click AtLast Click At
{{$detail->url}}{{$detail->clicks}}{{$detail->created_at->format(config('mail-tracker.date-format'))}}{{$detail->updated_at->format(config('mail-tracker.date-format'))}}
+
+
+
+@endsection diff --git a/tests/MailTrackerTest.php b/tests/MailTrackerTest.php index 2bef622..4b1ae6d 100644 --- a/tests/MailTrackerTest.php +++ b/tests/MailTrackerTest.php @@ -4,22 +4,30 @@ class AddressVerificationTest extends TestCase { public function testSendMessage() { + $faker = Faker\Factory::create(); + $email = $faker->email; + $subject = $faker->sentence; + $name = $faker->firstName . ' ' .$faker->lastName; \View::addLocation(__DIR__); - \Mail::send('email.test', [], function ($message) { + \Mail::send('email.test', [], function ($message) use($email, $subject, $name) { $message->from('from@johndoe.com', 'From Name'); $message->sender('sender@johndoe.com', 'Sender Name'); - $message->to('to@johndoe.com', 'To Name'); + $message->to($email, $name); $message->cc('cc@johndoe.com', 'CC Name'); $message->bcc('bcc@johndoe.com', 'BCC Name'); $message->replyTo('reply-to@johndoe.com', 'Reply-To Name'); - $message->subject('Subject'); + $message->subject($subject); $message->priority(3); }); + $this->seeInDatabase('sent_emails',[ + 'recipient'=>$name.' <'.$email.'>', + 'subject'=>$subject, + ]); } public function testPing() @@ -49,7 +57,7 @@ public function testLink() $clicks = $track->clicks; $clicks++; - $redirect = 'http://mfn1.myfootballnow.com/community/thread/2/1636?page=4&x=tRnYCfp9mT#10111'; + $redirect = 'http://'.str_random(15).'.com/'.str_random(10).'/'.str_random(10).'/'.rand(0,100).'/'.rand(0,100).'?page='.rand(0,100).'&x='.str_random(32); $url = action('\jdavidbakr\MailTracker\MailTrackerController@getL',[ \jdavidbakr\MailTracker\MailTracker::hash_url($redirect), // Replace slash with dollar sign @@ -58,6 +66,13 @@ public function testLink() $this->call('GET',$url); $this->assertRedirectedTo($redirect); + Event::assertFired(jdavidbakr\MailTracker\Events\LinkClickedEvent::class); + + $this->seeInDatabase('sent_emails_url_clicked',[ + 'url'=>$redirect, + 'clicks'=>1, + ]); + $track = $track->fresh(); $this->assertEquals($clicks, $track->clicks); @@ -68,7 +83,5 @@ public function testLink() ]); $this->call('GET',$url); $this->assertRedirectedTo($redirect); - - Event::assertFired(jdavidbakr\MailTracker\Events\LinkClickedEvent::class); } }