├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Makefile.PL ├── README.md ├── SendGrid-1.3.tar.gz ├── lib └── Email │ ├── SendGrid.pm │ └── SendGrid │ ├── Header.pm │ └── Transport │ ├── REST.pm │ ├── SMTP.pm │ └── Sendmail.pm └── t ├── lib ├── Email │ └── SendGrid │ │ ├── Header │ │ └── Test.pm │ │ ├── Test.pm │ │ └── Transport │ │ ├── REST │ │ └── Test.pm │ │ └── SMTP │ │ └── Test.pm └── Mock │ └── Net │ └── SMTP │ └── TLS.pm └── test.t /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | MANIFEST.skip 3 | META.yml 4 | MYMETA.yml 5 | MYMETA.json 6 | Makefile.old 7 | Makefile 8 | pm_to_blib 9 | blib/ 10 | inc/ 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - '5.20' 4 | - '5.18' 5 | - '5.16' 6 | - '5.14' 7 | - '5.8' 8 | notifications: 9 | hipchat: 10 | rooms: 11 | secure: nGaR7tHknJd4k0dDSR3mK4BuD1Amt8rLVNyUW5cPxNzPcJMMHQcEZ22zk5qzOW8cA9n3JgjjyesfRVSr/wiLnNjurIy8el3EIqXGp3qfQ4WNtVTJpR6QLqwd+ukwHSn+jloXbLCgkXwWXN7bEQXSyF8D9nOwTF/waZDqzryf/PU= 12 | template: 13 | - '%{repository} 14 | Build %{build_number} on branch %{branch} by %{author}: %{message} 15 | View on GitHub' 16 | format: html 17 | notify: true 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [1.3] - 2015-4-29 5 | ### Added 6 | - Support for API keys 7 | 8 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | use inc::Module::Install; 2 | 3 | # Define metadata 4 | name 'SendGrid'; 5 | all_from 'lib/Email/SendGrid.pm'; 6 | license 'MIT'; 7 | 8 | # Specific dependencies 9 | test_requires 'Test::More'; 10 | test_requires 'Test::MockObject'; 11 | test_requires 'Test::MockModule'; 12 | test_requires 'Test::Class::Load'; 13 | 14 | requires 'Net::SMTP::TLS'; 15 | requires 'Sys::Hostname'; 16 | requires 'Carp'; 17 | requires 'LWP::UserAgent'; 18 | requires 'LWP::Protocol::https'; 19 | requires 'Mail::Address'; 20 | requires 'Sys::Hostname'; 21 | requires 'URI::Escape'; 22 | requires 'JSON'; 23 | requires 'Encode'; 24 | requires 'MIME::Entity'; 25 | 26 | WriteAll; 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **IMPORTANT ANNOUNCEMENT** 2 | 3 | **As of May 1, 2016, SendGrid will no longer support this library.** 4 | 5 | Please direct any questions to the [Developer Experience](mailto:dx@sendgrid.com) team. 6 | 7 | [![Build Status](https://travis-ci.org/sendgrid/sendgrid-perl.png?branch=master)](https://travis-ci.org/sendgrid/sendgrid-perl) 8 | 9 | #sendgrid-perl 10 | Send emails via SendGrid. Provides wrapper for custom SMTP API fields 11 | and allows for easy manipulation of filter/app settings. 12 | 13 | Written by Tim Jenkins. 14 | 15 | #License 16 | Licensed under the MIT License. 17 | 18 | #Install 19 | We are currently working on getting this module on CPAN. In the 20 | meantime, you can install from the included archive. 21 | 22 | git clone https://github.com/sendgrid/sendgrid-perl.git 23 | cd sendgrid-perl 24 | sudo cpanm SendGrid-1.3.tar.gz 25 | 26 | You can also build the archive yourself: 27 | 28 | perl Makefile.PL 29 | make 30 | make test 31 | make dist 32 | 33 | #Basic usage 34 | 35 | For authentication, please use either your SendGrid credentials or an [API key](https://sendgrid.com/docs/User_Guide/Account/api_keys.html). 36 | 37 | ```perl 38 | use warnings; 39 | use strict; 40 | 41 | use Email::SendGrid; 42 | use Email::SendGrid::Transport::REST; 43 | 44 | my $sg = Email::SendGrid->new( from => 'from@example.com', 45 | to => 'to@example.com', 46 | subject => 'Testing', 47 | text => "Some text http://sendgrid.com/\n", 48 | html => 'Some html 49 | SG 50 | ' ); 51 | 52 | #disable click tracking filter for this request 53 | $sg->disableClickTracking(); 54 | 55 | #turn on the unsubscribe filter here with custom values 56 | $sg->enableUnsubscribe( text => "Unsubscribe here: <% %>", html => "Unsubscribe <% here %>" ); 57 | 58 | #set a category 59 | $sg->header->setCategory('first contact'); 60 | 61 | #add unique arguments 62 | $sg->header->addUniqueIdentifier( customer => '12345', location => 'somewhere' ); 63 | 64 | my $trans = Email::SendGrid::Transport::REST->new( username => 'sendgrid_username', password => 'sendgrid_password' ); 65 | 66 | my $error = $trans->deliver($sg); 67 | die $error if ( $error ); 68 | ``` 69 | 70 | #Advanced Usage 71 | For more detailed information, please refer to the perldocs. 72 | -------------------------------------------------------------------------------- /SendGrid-1.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sendgrid/sendgrid-perl/77553bfe7adf289fea539de07094ff48a6ffa8b6/SendGrid-1.3.tar.gz -------------------------------------------------------------------------------- /lib/Email/SendGrid.pm: -------------------------------------------------------------------------------- 1 | # Copyright(c) 2010 SendGrid 2 | 3 | package Email::SendGrid; 4 | 5 | use strict; 6 | use vars qw($VERSION); 7 | 8 | $VERSION = '1.3'; 9 | 10 | use Email::SendGrid::Header; 11 | use Mail::Address; 12 | use Encode; 13 | use MIME::Words qw(encode_mimeword decode_mimewords); 14 | 15 | use Carp; 16 | 17 | sub new 18 | { 19 | my $class = shift; 20 | 21 | my $self = bless { header => Email::SendGrid::Header->new(), 22 | rcpts => [], 23 | to => [], 24 | cc => [], 25 | bcc => [], 26 | attachments => [], 27 | encoding => 'quoted-printable', 28 | charset => 'UTF-8', 29 | @_ }, $class; 30 | 31 | return $self; 32 | } 33 | 34 | 35 | sub header 36 | { 37 | my $self = shift; 38 | return $self->{header}; 39 | } 40 | 41 | sub addTo 42 | { 43 | my $self = shift; 44 | my @rcpts = @_; 45 | push(@{$self->{to}}, @rcpts); 46 | } 47 | 48 | sub addCc 49 | { 50 | my $self = shift; 51 | my @rcpts = @_; 52 | push(@{$self->{cc}}, @rcpts); 53 | } 54 | 55 | sub addBcc 56 | { 57 | my $self = shift; 58 | my @rcpts = @_; 59 | push(@{$self->{bcc}}, @rcpts); 60 | } 61 | 62 | sub addRcpts 63 | { 64 | my $self = shift; 65 | my @rcpts = @_; 66 | 67 | foreach my $rcpt (@rcpts) 68 | { 69 | my ($addr) = Mail::Address->parse($rcpt); 70 | my $to = $addr->address(); 71 | push(@{$self->{rcpts}}, $to); 72 | } 73 | } 74 | 75 | sub get 76 | { 77 | my $self = shift; 78 | my $field = shift; 79 | my %args = @_; 80 | 81 | my $fields = { date => 1, text => 1, html => 1, subject => 1, from => 1, 'reply-to' => 1, 82 | encoding => 1, charset => 1, attachments => 1, mail_from => 1, 83 | to => 1, cc => 1, bcc => 1, 'message-id' => 1 }; 84 | 85 | croak "Unknown field '$field'" if ( !defined($fields->{$field}) ); 86 | 87 | my $ret = $self->{$field}; 88 | $ret = encode($self->{charset}, $ret) if ( $args{'encode'} ); 89 | 90 | return $ret; 91 | } 92 | 93 | sub set 94 | { 95 | my $self = shift; 96 | my $field = shift; 97 | my $val = shift; 98 | 99 | my $fields = { date => 1, text => 1, html => 1, subject => 1, from => 1, 'reply-to' => 1, 100 | encoding => 1, charset => 1, attachments => 1, 'message-id' => 1 }; 101 | 102 | croak "Unknown field '$field'" if ( !defined($fields->{$field}) ); 103 | 104 | $self->{$field} = $val; 105 | } 106 | 107 | sub addAttachment 108 | { 109 | my $self = shift; 110 | my $attach = shift; 111 | push(@{$self->{attachments}}, { data => $attach, @_ }); 112 | } 113 | 114 | sub getMailFrom 115 | { 116 | my $self = shift; 117 | my ($addr) = Mail::Address->parse($self->{from}); 118 | my $mailFrom = $addr->address(); 119 | return $mailFrom 120 | } 121 | 122 | sub createMimeMessage 123 | { 124 | my $self = shift; 125 | 126 | my $mime; 127 | 128 | if ( !defined($self->{text}) && !defined($self->{html}) ) 129 | { 130 | croak "No message data specified"; 131 | } 132 | 133 | my $text = $self->{text}; 134 | my $html = $self->{html}; 135 | 136 | $text = encode($self->{charset}, $self->{text}) if ( $self->{text} && utf8::is_utf8($self->{text}) ); 137 | $html = encode($self->{charset}, $self->{html}) if ( $self->{html} && utf8::is_utf8($self->{html}) ); 138 | 139 | if ( defined($text) && defined($html) ) 140 | { 141 | $mime = MIME::Entity->build( Type => 'multipart/alternative' ); 142 | 143 | $mime->attach(Type => 'text/plain', 144 | Encoding => $self->{encoding}, 145 | Charset => $self->{charset}, 146 | Data => $text); 147 | 148 | $mime->attach(Type => 'text/html', 149 | Encoding => $self->{encoding}, 150 | Charset => $self->{charset}, 151 | Data => $html); 152 | } 153 | elsif ( defined($self->{text}) ) 154 | { 155 | $mime = MIME::Entity->build(Type => 'text/plain', 156 | Encoding => $self->{encoding}, 157 | Charset => $self->{charset}, 158 | Data => $text); 159 | } 160 | else 161 | { 162 | $mime = MIME::Entity->build(Type => 'text/html', 163 | Encoding => $self->{encoding}, 164 | Charset => $self->{charset}, 165 | Data => $html); 166 | } 167 | 168 | foreach my $attach ( @{$self->{attachments}} ) 169 | { 170 | my $data = $attach->{data}; 171 | my %params; 172 | 173 | if ( -f $data ) 174 | { 175 | $params{Path} = $data; 176 | } 177 | else 178 | { 179 | $params{Data} = $data; 180 | } 181 | $params{Type} = $attach->{type} if ( defined($attach->{type}) ); 182 | $params{Encoding} = $attach->{encoding} if ( defined($attach->{encoding}) ); 183 | 184 | $mime->attach( %params ); 185 | } 186 | 187 | if ( defined($self->{subject}) ) 188 | { 189 | $mime->head->replace('subject', $self->encodeHeader($self->{subject})); 190 | } 191 | 192 | if ( defined($self->{from}) ) 193 | { 194 | $mime->head->replace('from', $self->encodeHeader($self->{from})); 195 | } 196 | 197 | if ( defined($self->{date}) ) 198 | { 199 | $mime->head->replace('date', $self->encodeHeader($self->{date})); 200 | } 201 | 202 | if ( defined($self->{'message-id'}) ) 203 | { 204 | $mime->head->replace('message-id', $self->{'message-id'}); 205 | } 206 | 207 | if ( defined($self->{'reply-to'}) ) 208 | { 209 | $mime->head->replace('reply-to', $self->encodeHeader($self->{'reply-to'})); 210 | } 211 | 212 | $self->mergeAddresses($mime, 'to'); 213 | $self->mergeAddresses($mime, 'cc'); 214 | 215 | if ( keys(%{$self->header->{data}}) ) 216 | { 217 | $mime->head->replace('x-smtpapi', $self->header->asJSON( fold => 72 )); 218 | } 219 | 220 | return $mime; 221 | } 222 | 223 | sub mergeAddresses 224 | { 225 | my $self = shift; 226 | my $mime = shift; 227 | my $field = shift; 228 | 229 | if ( defined($self->{$field}) ) 230 | { 231 | my $str = $self->{$field}; 232 | 233 | if ( ref($self->{$field}) eq "ARRAY" ) 234 | { 235 | $str = ''; 236 | foreach my $addr (@{$self->{$field}}) 237 | { 238 | $str .= "$addr, "; 239 | } 240 | # Remove the trailing , on the last entry 241 | chop($str); 242 | chop($str); 243 | } 244 | 245 | # If we had any addresses to put here, do so 246 | if ( $str ) 247 | { 248 | $mime->head->replace($field, $self->encodeHeader($str)); 249 | } 250 | } 251 | } 252 | 253 | sub getRecipients 254 | { 255 | my $self = shift; 256 | my @rcpts = $self->extractRecipients('to'); 257 | push(@rcpts, $self->extractRecipients('cc')); 258 | push(@rcpts, $self->extractRecipients('bcc')); 259 | return @rcpts; 260 | } 261 | 262 | sub extractRecipients 263 | { 264 | my $self = shift; 265 | my $field = shift; 266 | 267 | my @list; 268 | if ( ref($self->{$field}) ne "ARRAY" ) 269 | { 270 | $self->{$field} = [ $self->{$field} ]; 271 | } 272 | 273 | foreach my $addr ( @{$self->{$field}} ) 274 | { 275 | my @addrs = Mail::Address->parse($addr); 276 | 277 | foreach my $ad ( @addrs ) 278 | { 279 | my $a = $ad->address(); 280 | if ( $a ) 281 | { 282 | push(@list, $a); 283 | } 284 | } 285 | } 286 | 287 | return @list; 288 | } 289 | 290 | # This returns a string with proper MIME header encoding 291 | sub encodeHeader 292 | { 293 | my $self = shift; 294 | my $header = shift; 295 | 296 | my $str = $header; 297 | 298 | # First, if the thing is unicode, downgrade it 299 | if ( utf8::is_utf8($header) ) 300 | { 301 | $str = encode($self->{charset}, $str); 302 | } 303 | 304 | # If the string is not 7bit clean, encode the header. 305 | if ( my $count = () = $str =~ /[^\x00-\x7f]/g ) 306 | { 307 | my $type = 'q'; 308 | # If the number of characters to be encoded is over 6, use base 64 309 | $type = 'b' if ( $count > 6 ); 310 | 311 | $str = encode_mimeword($str, $type, $self->{charset}); 312 | } 313 | 314 | return $str; 315 | } 316 | 317 | 318 | ############################################################# 319 | # Convienience functions for working with the api 320 | 321 | # Make the filter map something that can be exported so new filters can be added on the fly 322 | our $filterMap = { 'Gravatar' => { filter => 'gravatar' }, 323 | 'OpenTracking' => { filter => 'opentrack' }, 324 | 'ClickTracking' => { filter => 'clicktrack', 325 | settings => { 326 | shorten => { 327 | setting => 'shorten', 328 | }, 329 | text => { 330 | setting => 'enable_text' 331 | } 332 | } 333 | }, 334 | 'SpamCheck' => { filter => 'spamcheck', 335 | settings => { 336 | score => { 337 | setting => 'maxscore', 338 | }, 339 | url => { 340 | setting => 'url' 341 | } 342 | } 343 | }, 344 | 'Unsubscribe' => { 345 | filter => 'subscriptiontrack', 346 | settings => { 347 | text => { 348 | setting => 'text/plain', 349 | validation => sub { 350 | croak "Missing substitution tag in text" if ( $_[0] !~ /<\%\s*\%>/ ); 351 | } 352 | }, 353 | html => { 354 | setting => 'text/html', 355 | validation => sub { 356 | croak "Missing substitution tag in html" if ( $_[0] !~ /<\%\s*[^\s]+\s*\%>/ ); 357 | } 358 | }, 359 | replace => { 360 | setting => 'replace', 361 | }, 362 | }, 363 | }, 364 | 'Footer' => { 365 | filter => 'footer', 366 | settings => { 367 | text => { setting => 'text/plain' }, 368 | html => { setting => 'text/html' }, 369 | }, 370 | }, 371 | 'GoogleAnalytics' => { 372 | filter => 'ganalytics', 373 | settings => { 374 | source => { setting => 'utm_source' }, 375 | medium => { setting => 'utm_medium' }, 376 | term => { setting => 'utm_term' }, 377 | content => { setting => 'utm_content' }, 378 | campaign => { setting => 'utm_campaign' }, 379 | }, 380 | }, 381 | 'DomainKeys' => { 382 | filter => 'domainkeys', 383 | settings => { 384 | domain => { setting => 'domain' }, 385 | sender => { setting => 'sender' }, 386 | }, 387 | }, 388 | 'DKIM' => { 389 | filter => 'dkim', 390 | settings => { 391 | domain => { setting => 'domain' }, 392 | use_from => { setting => 'use_from' }, 393 | }, 394 | }, 395 | 'Template' => { 396 | filter => 'template', 397 | validation => sub { 398 | my %args = @_; 399 | croak 'Missing html template' if ( !defined($args{html}) ); 400 | }, 401 | settings => { 402 | html => { 403 | setting => 'text/html', 404 | validation => sub { 405 | croak "Missing body substitution tag in template" if ( $_[0] !~ /<\%\s*\%>/ ); 406 | }, 407 | }, 408 | }, 409 | }, 410 | 'Twitter' => { 411 | filter => 'twitter', 412 | validation => sub { 413 | my %args = @_; 414 | croak 'Missing twitter username' if ( !defined($args{username}) ); 415 | croak 'Missing twitter password' if ( !defined($args{password}) ); 416 | }, 417 | settings => { 418 | username => { setting => 'username' }, 419 | password => { setting => 'password' }, 420 | }, 421 | }, 422 | 'Bcc' => { 423 | filter => 'bcc', 424 | validation => sub { 425 | my %args = @_; 426 | croak 'Missing bcc email' if ( !defined($args{email}) ); 427 | }, 428 | settings => { 429 | email => { setting => 'email' }, 430 | }, 431 | }, 432 | 'BypassListManagement' => { 433 | filter => 'bypass_list_management', 434 | }, 435 | 'Hold' => { 436 | filter => 'hold', 437 | }, 438 | 'Drop' => { 439 | filter => 'drop', 440 | } 441 | }; 442 | 443 | our $AUTOLOAD; 444 | 445 | # We autoload all of the filters and how their settings map to the smtpapi via the map above 446 | sub AUTOLOAD 447 | { 448 | my $self = shift; 449 | 450 | my $type = ref ($self) || croak "$self is not an object"; 451 | my $func = $AUTOLOAD; 452 | $func =~ s/.*://; 453 | 454 | if ( $func =~ /^disable(.*)/ ) 455 | { 456 | my $filterFunc = $1; 457 | 458 | my $filter = $filterMap->{$filterFunc}->{filter}; 459 | 460 | croak "Unknown filter function '$func'" if ( !defined($filter) ); 461 | $self->header->disable($filter); 462 | } 463 | elsif ( $func =~ /^enable(.*)/ ) 464 | { 465 | my $filterFunc = $1; 466 | my %args = @_; 467 | 468 | my $filterData = $filterMap->{$filterFunc}; 469 | $filterData->{validation}(%args) if ( defined($filterData->{validation}) ); 470 | 471 | my $filter = $filterData->{filter}; 472 | croak "Unknown filter function '$func'" if ( !defined($filter) ); 473 | 474 | $self->header->enable($filter); 475 | foreach my $set ( keys(%args) ) 476 | { 477 | my $filtSet = $filterData->{settings}->{$set}; 478 | croak "Unknown filter setting '$set'" if ( !defined($filtSet) ); 479 | $filtSet->{validation}($args{$set}) if ( defined($filtSet->{validation}) ); 480 | 481 | $self->header->addFilterSetting($filter, $filtSet->{setting}, $args{$set}); 482 | } 483 | } 484 | else 485 | { 486 | croak "Unknown subroutine '$func'"; 487 | } 488 | } 489 | 490 | # Have to define so that AUTOLOAD doesn't eat this 491 | sub DESTROY 492 | { 493 | } 494 | 495 | =head1 NAME 496 | 497 | Email::SendGrid - Class for building a message to be sent through the SendGrid 498 | mail service 499 | 500 | =head1 SYNOPSIS 501 | 502 | use Email::SendGrid 503 | 504 | my $sg = Email::SendGrid->new( from => $from, 505 | to => $to, 506 | subject => 'Testing', 507 | text => "Some text http://sendgrid.com/\n", 508 | html => 'Some html 509 | SG 510 | ', 511 | ); 512 | 513 | $sg->disableClickTracking(); 514 | $sg->enableUnsubscribe( text => "Unsubscribe here: <% %>", html => "Unsubscribe <% here %>" ); 515 | 516 | =head1 DESCRIPTION 517 | 518 | This module allows for easy integration with the SendGrid email distribution 519 | service and its SMTP API. It allows you to build up the pieces that make the 520 | email itself, and then pass this object to the Email::SendGrid::Transport class 521 | that you wish to use for final delivery. 522 | 523 | =head1 CLASS METHODS 524 | 525 | =head2 Creation 526 | 527 | =head3 new[ARGS] 528 | 529 | This creates the object and optionally populates some of the data fields. Available fields are: 530 | 531 | =over 532 | 533 | =item from 534 | 535 | The From address to use in the email 536 | 537 | from => 'Your Name ' 538 | 539 | =item to 540 | 541 | Either a string or ARRAYREF containing the addresses to send to 542 | 543 | to => 'Your Customer , Another Customer ' 544 | 545 | to => [ them@theircompany.com, someone@somewhereelse.com ] 546 | 547 | =item cc 548 | 549 | Adds additional recipients for the email and sets the CC field of the email 550 | 551 | =item bcc 552 | 553 | Adds additional recipients whose addresses will not appear in the headers of 554 | the email 555 | 556 | =item subject 557 | 558 | Sets the subject for the message 559 | 560 | =item date 561 | 562 | Sets the date header for the message. If this is not specified, the current 563 | date will be used 564 | 565 | =item message-id 566 | 567 | Sets the message id header. If this is not specified, one will be randomly 568 | generated 569 | 570 | =item encoding 571 | 572 | Sets the encoding for the email. Options are 7bit, base64, binary, and 573 | quoted-printable. 574 | Quoted printable is the default encoding 575 | 576 | =item charset 577 | 578 | Sets the default character set. iso-8859-1 (latin1) is the default 579 | If you will be using wide characters, it is expected that the strings 580 | passed in will already be encoded with this charset 581 | 582 | =item text 583 | 584 | Sets the data for the plain text portion of the email 585 | 586 | =item html 587 | 588 | Sets the data for the html portion of the email 589 | 590 | =back 591 | 592 | =head2 Methods for setting data 593 | 594 | =head3 addTo(LIST) 595 | 596 | =head3 addCc(LIST) 597 | 598 | =head3 addBcc(LIST) 599 | 600 | Inserts additional recipients for the message 601 | 602 | =head3 get(PARAMETER) 603 | 604 | =head3 set(PARAMETER, VALUE) 605 | 606 | Returns / sets a parameter for this message. Parameters are the same as those 607 | in the new method 608 | 609 | =head3 addAttachment(DATA, [OPTIONS]) 610 | 611 | Specifies an attachment for the email. This can either be the data for the 612 | attachment, or a file to attach 613 | 614 | While the underlying MIME::Entity that will be created will try to determine 615 | the best encoding and MIME type, you can also specify it in the options 616 | 617 | $sg->addAttachment('/tmp/my.pdf', 618 | type => 'application/pdf', 619 | encoding => 'base64'); 620 | 621 | =head2 SMTP API functions 622 | 623 | This class contains a number of methods for working with the SMTP API. The 624 | SMTP API allows for customization of SendGrid filters on an individual 625 | basis for emails. For more information about the api, please visit 626 | http://wiki.sendgrid.com/doku.php?id=smtp_api 627 | 628 | =head3 header 629 | 630 | Returns a reference to the Email::SendGrid::Header object used to communicate 631 | with the SMTP API. Useful if you want to set unique identifiers or use the 632 | mail merge functionality 633 | 634 | =head3 enableGravatar 635 | 636 | =head3 disableGravatar 637 | 638 | Enable / disable the addition of a gravatar in the email 639 | 640 | =head3 enableClickTracking 641 | 642 | =head3 disableClickTracking 643 | 644 | Enables / disables click tracking for emails. By default this will only 645 | enable click tracking for the html portion of emails. If you want to 646 | also do click tracking on plain text, you can use the addition prameter 'text' 647 | $sg->enableClickTracking( text => 1 ); 648 | 649 | =head3 enableUnsubscribe 650 | 651 | =head3 disableUnsubscribe 652 | 653 | Enables / disables the addition of subscription management headers / links 654 | in the email. 655 | 656 | If no arguments are specified for enable, only a list-unsubscribe header 657 | will be added to the email. Additional parameters are: 658 | 659 | =over 660 | 661 | =item text 662 | 663 | The string to be added to the plain text portion of the email. This must 664 | contain the string <% %>, which is where the link will be placed 665 | 666 | =item html 667 | 668 | The string to be added to the html portion of the email. This must contain a 669 | tag with the format "<% link text %>", which will be replaced with the link 670 | 671 | =item replace 672 | 673 | A string inside of the email body to replace with the unsubscribe link. If 674 | this tag is not found in the body of the email, then the appropriate text or 675 | html setting will be used. If none of these are found, a link will not be 676 | inserted 677 | 678 | $sg->enableUnsubscribe( text => 'Unsubscribe here: <% %>', 679 | html => 'Unsubscribe <% here %>', 680 | replace => 'MyUnsubscribeTag' ); 681 | 682 | =back 683 | 684 | =head3 enableOpenTracking 685 | 686 | =head3 disableOpenTracking 687 | 688 | Enables / disables open tracking 689 | 690 | =head3 enableFooter 691 | 692 | =head3 disableFooter 693 | 694 | Enables / disables the insertion of a footer in this email. 695 | 696 | Parameters are: 697 | 698 | =over 699 | 700 | =item text 701 | 702 | String to insert into the plain text portion of the email 703 | 704 | =item html 705 | 706 | String to insert into the html portion of the email 707 | 708 | $sg->enableFooter( text => 'Text footer', html => 'Html footer' ); 709 | 710 | =back 711 | 712 | =head3 enableSpamCheck 713 | 714 | =head3 disableSpamCheck 715 | 716 | Enables / disables the checking of email for spam content. This is useful when 717 | you are sending content that is generated by your users, such as a forum 718 | 719 | Parameters are: 720 | 721 | =over 722 | 723 | =item score 724 | 725 | Spam Assassin score at which point the email will be flagged as spam and 726 | dropped. If this is not specified, 5 will be used 727 | 728 | =item url 729 | 730 | A url to post to in the event that the message is flagged as spam and dropped. 731 | 732 | =back 733 | 734 | =head3 enableGoogleAnalytics 735 | 736 | =head3 disabelGoogleAnalytics 737 | 738 | Enables / disables link rewrites to support Google Analytics. 739 | 740 | Paramters are: 741 | 742 | =over 743 | 744 | =item source 745 | 746 | Sets the string for the utm_source field 747 | 748 | =item medium 749 | 750 | Sets the string for the utm_medium field 751 | 752 | =item term 753 | 754 | Sets the string for the utm_term field 755 | 756 | =item content 757 | 758 | Sets the string for the utm_content field 759 | 760 | =item campaign 761 | 762 | Sets the string for the utm_campaign field 763 | 764 | =back 765 | 766 | =head3 enableDomainKeys 767 | 768 | =head3 disableDomainKeys 769 | 770 | Enables / disables Domain Keys signatures for the message. 771 | 772 | Domain Keys is a digitial signature method, primarily used by Yahoo. 773 | 774 | Parameters are: 775 | 776 | =over 777 | 778 | =item domain 779 | 780 | The domain to sign the messages as. This domain must be set up with the proper 781 | DNS records, which can be found when going through the whitelabel wizard 782 | 783 | =item sender 784 | 785 | Sets if SendGrid will add a Sender header if the From email address does not 786 | match the specified domain. This allows for messages to still have a valid 787 | DomainKeys signature if the email address in the From field does not have 788 | whitelabeling set up, however it means that some email clients will display 789 | the message as "on behalf of" the From address 790 | 791 | =back 792 | 793 | =head3 enableTemplate 794 | 795 | =head3 disableTemplate 796 | 797 | Enables / disables the insertion of an email template. If you are enabling a 798 | template, you must specify the text of it in the html parameter. This 799 | text must contain the string "<% %>", which will be replaced with the body 800 | of the message 801 | 802 | =head3 enableTwitter 803 | 804 | Allows you to set twitter username / password information for this email. 805 | The Twitter filter can be used to send either status updates or direct 806 | messages, based on the recipient email address 807 | 808 | my $sg = Email::SendGrid->new( to => 'sendgrid@twitter', text => "My update" ); 809 | 810 | $sg->enableTwitter( username => 'myusername', password => 'mypassword' ); 811 | 812 | =head3 enableBcc 813 | 814 | =head3 disableBcc 815 | 816 | Enables / disables the automatic blind carbon copy of the email to a specific 817 | email address. While it makes little sense to enable a bcc here, versus adding 818 | it as a Bcc with the methods discussed previously, you can specify an email 819 | paramter which is the address to send to 820 | 821 | =head3 enableBypassListManagement 822 | 823 | Allows you to specify that this email should not be suppressed for any reason, 824 | including the address appearing on the bounce, unsubscribe, or spam report 825 | suppression lists. This is useful for urgent emails, such as password reset 826 | notifications, that should always be attempted for delivery. 827 | 828 | =head2 Transport methods 829 | 830 | If you don't want to use one of the Email::SendGrid::Transport classes, or 831 | want to build your own, these methods are used to get the data for transport 832 | 833 | =head3 createMimeMessage 834 | 835 | Returns a MIME::Entity for the email 836 | 837 | =head3 getMailFrom 838 | 839 | Returns the address to be used in the MAIL FROM portion of the SMTP protocol 840 | 841 | =head3 getRecipients 842 | 843 | Returns an array of the recipient address for the email, to be used in the 844 | RCPT TO portion of the SMTP protocol 845 | 846 | =head1 AUTHOR 847 | 848 | Tim Jenkins 849 | 850 | =head1 COPYRIGHT 851 | 852 | Copyright (c) 2010 SendGrid. All rights reserved. 853 | This program is free software; you can redistribute it and/or modify 854 | it under the same terms as Perl itself. 855 | 856 | =cut 857 | 858 | 859 | 1; 860 | -------------------------------------------------------------------------------- /lib/Email/SendGrid/Header.pm: -------------------------------------------------------------------------------- 1 | package Email::SendGrid::Header; 2 | 3 | use strict; 4 | 5 | use JSON; 6 | use MIME::Entity; 7 | 8 | sub new 9 | { 10 | my $class = shift; 11 | 12 | my $self = bless { 'data' => { }, 13 | @_ 14 | }, $class; 15 | 16 | return $self; 17 | } 18 | 19 | sub addTo 20 | { 21 | my $self = shift; 22 | my @to = @_; 23 | push(@{$self->{data}->{to}}, @to); 24 | } 25 | 26 | sub addSubVal 27 | { 28 | my $self = shift; 29 | my $var = shift; 30 | my @val = @_; 31 | 32 | if (!defined($self->{data}->{sub}->{$var})) 33 | { 34 | $self->{data}->{sub}->{$var} = []; 35 | } 36 | push(@{$self->{data}->{sub}->{$var}}, @val); 37 | } 38 | 39 | sub addUniqueIdentifier 40 | { 41 | my $self = shift; 42 | my $args; 43 | if ( scalar(@_) == 1 && ref($_[0]) eq "HASH" ) 44 | { 45 | $args = $_[0]; 46 | } 47 | else 48 | { 49 | $args = { @_ }; 50 | } 51 | 52 | foreach my $arg ( keys(%{$args}) ) 53 | { 54 | $self->{data}->{unique_args}->{$arg} = $args->{$arg}; 55 | } 56 | } 57 | 58 | sub addSection 59 | { 60 | my $self = shift; 61 | my %sections = @_; 62 | foreach my $sec ( keys(%sections) ) 63 | { 64 | $self->{data}->{section}->{$sec} = $sections{$sec}; 65 | } 66 | } 67 | 68 | sub setCategory 69 | { 70 | my $self = shift; 71 | my $cat = shift; 72 | $self->{data}->{category} = $cat; 73 | } 74 | 75 | sub enable 76 | { 77 | my $self = shift; 78 | my $filter = shift; 79 | 80 | $self->addFilterSetting($filter, 'enable', 1); 81 | } 82 | 83 | sub disable 84 | { 85 | my $self = shift; 86 | my $filter = shift; 87 | 88 | $self->addFilterSetting($filter, 'enable', 0); 89 | } 90 | 91 | sub addFilterSetting 92 | { 93 | my $self = shift; 94 | my $filter = shift; 95 | 96 | my $val = pop; 97 | 98 | if (!defined($self->{data}->{filters}->{$filter})) 99 | { 100 | $self->{data}->{filters}->{$filter} = {}; 101 | } 102 | if (!defined($self->{data}->{filters}->{$filter}->{settings})) 103 | { 104 | $self->{data}->{filters}->{$filter}->{settings} = {}; 105 | } 106 | 107 | my $set = $self->{data}->{filters}->{$filter}->{settings}; 108 | 109 | while ( my $setting = shift(@_) ) 110 | { 111 | if ( scalar(@_ ) ) 112 | { 113 | $set->{$setting} = {} if ( !defined($set->{$setting}) ); 114 | die "Attempt to overwrite setting" if ( ref($set->{$setting}) ne "HASH" ); 115 | $set = $set->{$setting}; 116 | } 117 | else 118 | { 119 | die "Attempt to overwrite hash" if ( ref($set->{$setting}) ); 120 | $set->{$setting} = $val; 121 | } 122 | } 123 | } 124 | 125 | sub setASMGroupID 126 | { 127 | my $self = shift; 128 | my $asmGroupId = shift; 129 | $self->{data}->{asm_group_id} = $asmGroupId; 130 | } 131 | 132 | sub addHeader 133 | { 134 | my $self = shift; 135 | my $mime = shift; 136 | 137 | $mime->head->replace('x-smtpapi', $self->asJSON()); 138 | $mime->head->fold('x-smtpapi'); 139 | } 140 | 141 | sub asJSON 142 | { 143 | my $self = shift; 144 | my %args = @_; 145 | 146 | my $json = JSON->new; 147 | $json->space_before(1); 148 | $json->space_after(1); 149 | $json->ascii(1); 150 | my $str = $json->encode($self->{data}); 151 | if ( $args{fold} ) 152 | { 153 | my $length = $args{fold}; 154 | 155 | $str =~ s/(.{1,$length})(\s)/$1\n$2/g; 156 | } 157 | 158 | return $str; 159 | } 160 | 161 | =head1 NAME 162 | 163 | Email::SendGrid::Header - Functions for building the string necessary for 164 | working with SendGrid's SMTP API 165 | 166 | =head1 SYNOPSIS 167 | 168 | use Email::SendGrid::Header; 169 | 170 | my $hdr = Email::SendGrid::Header->new(); 171 | $hdr->setCategory('first contact'); 172 | $hdr->addUniqueIdentifier( customer_id => 4 ); 173 | my $str = $hdr->asJSON( fold => 72 ); 174 | 175 | =head1 DESCRIPTION 176 | 177 | This class handles setting the appropriate hash variables to work with 178 | SendGrid's SMTP API. With the SMTP API you can control filters that are 179 | applied to your email, supply additional parameters to identify the message, 180 | and make use of mail merge capabilities. 181 | 182 | =head1 CLASS METHODS 183 | 184 | =head2 new 185 | 186 | Creates a new instance of the class 187 | 188 | =head2 addFilterSetting 189 | 190 | Allows you to specify a filter setting. You can find a list of filters and 191 | their settings here: 192 | http://wiki.sendgrid.com/doku.php?id=filters 193 | 194 | $hdr->addFilterSetting('twitter', 'username', 'myusername'); 195 | 196 | =head2 enable 197 | 198 | =head2 disable 199 | 200 | These are shortcut methods for enabling / disabling a filter. They are 201 | the same as using addFilterSetting on the 'enable' setting. 202 | 203 | $hdr->enable('opentrack'); 204 | 205 | is the same as 206 | 207 | $hdr->addFilterSetting('opentrack', 'enable', 1) 208 | 209 | =head2 setCategory 210 | 211 | This sets the category for this email. Statistics are stored on a per 212 | category basis, so this can be useful for tracking on a per group 213 | basis 214 | 215 | =head2 addUniqueIdentifier 216 | 217 | This adds parameters and values that will be passed back through SendGrid's 218 | Event API if an event notification is triggered by this email 219 | 220 | $hdr->addUniqueIdentifier( customer => 'someone', location => 'somewhere' ); 221 | 222 | =head2 addSection 223 | 224 | This adds sections of text that will be replaced, allowing for common text that will be reused 225 | on multiple recipients to be combined in one location. 226 | 227 | $hdr->addSection( String-in-Substitutions => String-To-replace ) 228 | 229 | An example of this would look like the following: 230 | 231 | $hdr->addSubVal('%body', '%body1%', '%body2%'); 232 | $hdr->addSubVal('%name%', 'Tim', 'Joe'); 233 | $hdr->addSection('%body1' => "Body text specific unspecific for %name%"); 234 | $hdr->addSection('%body2' => "Some other body text, customized for %name%"); 235 | 236 | =head2 addTo(ARRAY) 237 | 238 | This adds recipients for the mail merge functionality. Recipients can be 239 | specified as simply an email address, such as 240 | 241 | 'someone@somewhere.com' 242 | 243 | or as a full name and address, like 244 | 245 | 'Someone Special ' 246 | 247 | This value will be substituted into the To header of the email when it is merged 248 | 249 | This functionality can also be used to help decrease the latency caused by the 250 | SMTP protocol when sending the same message to a large number of recipients 251 | 252 | =head2 addSubVal(TAG, ARRAY) 253 | 254 | This adds a substitution value to be used during the mail merge. Substitutions 255 | will happen in order added, so calls to this should match calls to addTo 256 | 257 | $hdr->addTo('me@myplace.com'); 258 | $hdr->addTo('you@myplace.com'); 259 | 260 | $hdr->addSubVal('%name%', 'me', 'you'); 261 | 262 | =head2 setASMGroupID 263 | 264 | This sets the ASM Group ID for this email. Please read the documentation here: 265 | https://sendgrid.com/docs/API_Reference/Web_API_v3/Advanced_Suppression_Manager/index.html 266 | 267 | $hdr->setASMGroupID(123); 268 | 269 | =head2 asJSON 270 | 271 | Returns the JSON encoded string used to communicate with the SMTP API. It 272 | takes an optional fold paramter specifying the length at which to fold the 273 | header. It is important to fold the header, since most mail servers will 274 | break lines up to keep them shorter than 1,000 characters, which will 275 | introduce spaces into values in the string 276 | 277 | =head2 addHeader(MIME Entity) 278 | 279 | Shortcut function for adding the x-smtpapi header into a mime entity 280 | 281 | =head1 AUTHOR 282 | 283 | Tim Jenkins 284 | 285 | =cut 286 | 287 | 1; 288 | 289 | -------------------------------------------------------------------------------- /lib/Email/SendGrid/Transport/REST.pm: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 SendGrid 2 | 3 | package Email::SendGrid::Transport::REST; 4 | 5 | use strict; 6 | use vars qw($VERSION); 7 | 8 | $VERSION = '1.3'; 9 | 10 | use LWP::UserAgent; 11 | use Mail::Address; 12 | use Sys::Hostname; 13 | use URI::Escape; 14 | use JSON; 15 | use Encode; 16 | use Carp; 17 | use English qw( -no_match_vars ); 18 | use Data::Dumper qw(Dumper); 19 | 20 | sub new 21 | { 22 | my $class = shift; 23 | 24 | my $self = bless { server => 'api.sendgrid.com', 25 | path => '/api/mail.send.json', 26 | timeout => 30, 27 | @_, 28 | }, $class; 29 | 30 | if ( defined($self->{username}) && defined($self->{api_key}) ) 31 | { 32 | die "Must only specify username/password or api key, not both"; 33 | } 34 | 35 | if ( !(defined($self->{username}) && defined($self->{password})) && !defined($self->{api_key}) ) 36 | { 37 | die "Must speicfy username/password or api key"; 38 | } 39 | 40 | return $self; 41 | } 42 | 43 | sub deliver 44 | { 45 | my $self = shift; 46 | my $sg = shift; 47 | 48 | # Get the character set for encoding 49 | my $charset = $sg->get('charset'); 50 | 51 | # Get the from fields 52 | my $from = $sg->get('from'); 53 | 54 | my ($addr) = Mail::Address->parse($from); 55 | 56 | my $fromName = $addr->name(); 57 | $fromName = encode($charset, $fromName) if ( utf8::is_utf8($fromName) ); 58 | 59 | my $fromAddr = $addr->address(); 60 | 61 | # Get the To fields. 62 | my ($toAddr, $toName) = $self->splitAddresses($sg->get('to')); 63 | 64 | # Do a sanity check on the argument lengths 65 | if ( scalar(@$toName) && scalar(@$toName) != scalar(@$toAddr) ) 66 | { 67 | croak "There are an inconsistant number of recipients in the to field" . 68 | "that have names, which is incompatible with the REST API"; 69 | } 70 | 71 | my $subject = $sg->get('subject', encode => 1); 72 | my $text = $sg->get('text', encode => 1); 73 | my $html = $sg->get('html', encode => 1); 74 | my $date = $sg->get('date', encode => 1); 75 | my $messageId = $sg->get('message-id', encode => 0); 76 | my $reply = $sg->get('reply-to', encode => 1); 77 | my $attachments = $sg->get('attachments', encode => 0); 78 | 79 | # Build the query 80 | 81 | my $query = 'https://' . $self->{server} . $self->{path} . "?"; 82 | if ( defined($self->{username}) ) 83 | { 84 | $query .= "api_user=" . uri_escape($self->{username}) . "&api_key=" . uri_escape($self->{password}); 85 | } 86 | 87 | # Add recipients 88 | foreach my $i ( 0..(scalar(@$toAddr)-1) ) 89 | { 90 | $query .= "&to[]=$toAddr->[$i]"; 91 | my $name = $toName->[$i]; 92 | $name = encode($charset, $name) if ( utf8::is_utf8($name) ); 93 | $query .= "&toname[]=" . uri_escape($name) if ( $name ); 94 | } 95 | 96 | # Add the from 97 | $query .= "&from=$fromAddr"; 98 | $query .= "&fromname=" . uri_escape($fromName) if ( defined($fromName) ); 99 | 100 | # Add the subject 101 | $query .= "&subject=" . uri_escape($subject); 102 | 103 | # Add the reply to 104 | $query .= "&replyto=" . uri_escape($reply) if ( defined($reply) ); 105 | 106 | # Date 107 | $query .= "&date=" . uri_escape($date) if ( defined($date) ); 108 | 109 | # smtp api header 110 | my $hdr = $sg->header()->asJSON(); 111 | $hdr = encode($charset, $hdr) if ( utf8::is_utf8($hdr) ); 112 | 113 | $query .= "&x-smtpapi=" . uri_escape($hdr) if ( $hdr ne "{}" ); 114 | 115 | # Text 116 | $query .= "&text=" . uri_escape($text) if ( defined($text) ); 117 | 118 | # html 119 | $query .= "&html=" . uri_escape($html) if ( defined($html) ); 120 | 121 | my $i = 0; 122 | # Attachments 123 | foreach my $attach ( @$attachments ) 124 | { 125 | my $filename = "attachment" . ++$i; 126 | my $data = $attach->{data}; 127 | my %params; 128 | 129 | if ( -f $data ) 130 | { 131 | $filename = $data; 132 | $data = q{}; 133 | { 134 | local $RS = undef; # this makes it just read the whole thing, 135 | my $fh; 136 | croak "Can't open $filename: $!\n" if not open $fh, '<', $filename; 137 | $data = <$fh>; 138 | croak 'Some Error During Close :/ ' if not close $fh; 139 | } 140 | } 141 | my @path = split('/', $filename); 142 | my $file = $path[$#path]; 143 | $query .= "&files[" . uri_escape(encode('utf8', $file)) . "]=" . uri_escape(encode('utf8', $data)); 144 | } 145 | 146 | # Other headers (currently just message-id) 147 | my $additionalHeaders = {}; 148 | 149 | $additionalHeaders->{'message-id'} = $messageId if ( defined($messageId) ); 150 | 151 | $query .= "&headers=" . uri_escape(to_json($additionalHeaders, { ascii => 1})) if ( keys(%$additionalHeaders) ); 152 | my $resp = $self->send($query); 153 | 154 | return undef if ( $resp->{message} eq "success" ); 155 | 156 | return $resp->{errors}->[0]; 157 | } 158 | 159 | sub send 160 | { 161 | my $self = shift; 162 | my $query = shift; 163 | 164 | my $ua = LWP::UserAgent->new( timeout => $self->{timeout}, agent => 'sendgrid/' . $VERSION . ';perl' ); 165 | 166 | if ( defined($self->{api_key}) ) 167 | { 168 | $ua->default_header('Authorization' => "Bearer $self->{api_key}"); 169 | } 170 | my $response = $ua->get($query); 171 | 172 | return { errors => [ $response->status_line() ] } if ( !$response->is_success ); 173 | 174 | my $content = $response->decoded_content(); 175 | 176 | my $resp; 177 | 178 | eval { 179 | $resp = from_json($content); 180 | }; 181 | if ( $@ ) 182 | { 183 | croak "malformed json response: $@"; 184 | } 185 | 186 | return $resp; 187 | } 188 | 189 | sub splitAddresses 190 | { 191 | my $self = shift; 192 | my $field = shift; 193 | 194 | my $str = $field; 195 | 196 | my @ad; 197 | my @name; 198 | 199 | if ( ref($field) eq "ARRAY" ) 200 | { 201 | $str = ''; 202 | foreach my $addr (@$field) 203 | { 204 | $str .= "$addr, "; 205 | } 206 | # Remove the trailing , on the last entry 207 | chop($str); 208 | chop($str); 209 | } 210 | 211 | my @addrs = Mail::Address->parse($str); 212 | foreach my $addr ( @addrs ) 213 | { 214 | push(@ad, $addr->address()); 215 | push(@name, $addr->name()) if ( $addr->name() ); 216 | } 217 | 218 | return (\@ad, \@name); 219 | } 220 | 221 | 222 | =head1 NAME 223 | 224 | Email::SendGrid::Transport::REST - REST Transport class to SendGrid 225 | 226 | =head1 SYNOPSIS 227 | 228 | use Email::SendGrid::Transport::REST; 229 | use Email::SendGrid; 230 | 231 | my $sg = Email::SendGrid->new(); 232 | ... 233 | my $trans = Email::SendGrid::Transport::REST->new( username => 'mysuername', 234 | password => 'mypassword' ); 235 | 236 | my $error = $trans->deliver($sg); 237 | die $error if ( $error ); 238 | 239 | =head1 DESCRIPTION 240 | 241 | This is a transport module for sending messages through the SendGrid email 242 | distribution system via a REST API. After you have completed building your 243 | Email::SendGrid object, use this class to make a connection to SendGrid's 244 | web servers and deliver the message. 245 | 246 | =head1 CLASS METHODS 247 | 248 | =head2 new[ARGS] 249 | 250 | Creates the instance of the class. At a minimum you must specify the username 251 | and password used to connect. 252 | 253 | Parameters are: 254 | 255 | =over 256 | 257 | =item username 258 | 259 | Your SendGrid username 260 | 261 | =item password 262 | 263 | Your SendGrid password 264 | 265 | =item api_key 266 | 267 | Your SendGrid API key 268 | 269 | =item server 270 | 271 | The server to connect to (default is sendgrid.com) 272 | 273 | =item path 274 | 275 | The path portion of the url (default is /api/mail.send.json) 276 | 277 | =item timeout 278 | 279 | Connection timeout, in seconds (default is 30) 280 | 281 | =back 282 | 283 | =head2 deliver(object) 284 | 285 | Delivers the Email::SendGrid object specified. This will return undef on 286 | success, and the failure reason on error 287 | 288 | =head1 AUTHOR 289 | 290 | Tim Jenkins 291 | 292 | =head1 COPYRIGHT 293 | 294 | Copyright (c) 2010 SendGrid. All rights reserved. 295 | This program is free software; you can redistribute it and/or modify 296 | it under the same terms as Perl itself. 297 | 298 | =cut 299 | 300 | 1; 301 | -------------------------------------------------------------------------------- /lib/Email/SendGrid/Transport/SMTP.pm: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 SendGrid 2 | 3 | package Email::SendGrid::Transport::SMTP; 4 | 5 | use strict; 6 | use warnings; 7 | use vars qw($VERSION); 8 | 9 | $VERSION = '1.3'; 10 | 11 | use Net::SMTP::TLS; 12 | use Sys::Hostname; 13 | use Carp; 14 | 15 | sub new 16 | { 17 | my $class = shift; 18 | 19 | my $self = bless { server => 'smtp.sendgrid.net', 20 | timeout => 30, 21 | port => 587, 22 | tls => 1, 23 | smtp_class => 'Net::SMTP::TLS', 24 | @_ }, $class; 25 | 26 | if ( !defined($self->{domain}) ) 27 | { 28 | $self->{domain} = hostname(); 29 | } 30 | 31 | croak "TLS is required for port 587" if ( !$self->{tls} && $self->{port} == 587 ); 32 | 33 | if ( defined($self->{username}) && defined($self->{api_key}) ) 34 | { 35 | die "Must only specify username/password or api key, not both"; 36 | } 37 | 38 | if ( !(defined($self->{username}) && defined($self->{password})) && !defined($self->{api_key}) ) 39 | { 40 | die "Must speicfy username/password or api key"; 41 | } 42 | 43 | if ( defined($self->{api_key}) ) 44 | { 45 | $self->{username} = "apikey"; 46 | $self->{password} = delete $self->{api_key}; 47 | } 48 | return $self; 49 | } 50 | 51 | sub deliver 52 | { 53 | my $self = shift; 54 | my $sg = shift; 55 | 56 | my $mime = $sg->createMimeMessage(); 57 | my $msg = $mime->stringify(); 58 | my @rcpts = $sg->getRecipients(); 59 | my $from = $sg->getMailFrom(); 60 | 61 | croak "Must specify a from address" if ( !defined($from) ); 62 | croak "Must specify at least one recipient" if ( scalar(@rcpts) == 0 ); 63 | 64 | eval { 65 | 66 | my $smtp = $self->{smtp_class}->new($self->{server}, 67 | Port => $self->{port}, 68 | NoTLS => !$self->{tls}, 69 | Debug => 0, 70 | Timeout => $self->{timeout}, 71 | User => $self->{username}, 72 | Password => $self->{password}, 73 | Hello => $self->{domain}, 74 | %{$self->{smtp_params}}); 75 | 76 | $smtp->mail($from); 77 | foreach my $rcpt ( @rcpts ) 78 | { 79 | $smtp->to($rcpt); 80 | } 81 | 82 | $smtp->data(); 83 | $smtp->datasend($msg); 84 | $smtp->dataend(); 85 | # We'll try to be nice and shutdown the correct way, but if there is an error ignore it 86 | eval { 87 | $smtp->quit(); 88 | } 89 | }; 90 | if ( $@ ) 91 | { 92 | return ($@); 93 | } 94 | 95 | return undef; 96 | } 97 | 98 | =head1 NAME 99 | 100 | Email::SendGrid::Transport::SMTP - SMTP Transport class to SendGrid 101 | 102 | =head1 SYNOPSIS 103 | 104 | use Email::SendGrid::Transport::SMTP; 105 | use Email::SendGrid; 106 | 107 | my $sg = Email::SendGrid->new(); 108 | ... 109 | my $trans = Email::SendGrid::Transport::SMTP->new( username => 'mysuername', 110 | password => 'mypassword' ); 111 | 112 | my $error = $trans->deliver($sg); 113 | die $error if ( $error ); 114 | 115 | =head1 DESCRIPTION 116 | 117 | This is a transport module for sending messages through the SendGrid email 118 | distribution system via SMTP. After you have completed building your 119 | Email::SendGrid object, use this class to make a connection to SendGrid's 120 | SMTP servers and deliver the message. 121 | 122 | =head1 CLASS METHODS 123 | 124 | =head2 new[ARGS] 125 | 126 | Creates the instance of the class. At a minimum you must specify the username 127 | and password used to connect. 128 | 129 | Parameters are: 130 | 131 | =over 132 | 133 | =item username 134 | 135 | Your SendGrid username 136 | 137 | =item password 138 | 139 | Your SendGrid password 140 | 141 | =item api_key 142 | 143 | Your SendGrid API key 144 | 145 | =item server 146 | 147 | The server to connect to (default is smtp.sendgrid.net) 148 | 149 | =item port 150 | 151 | The port to connect to (default is 587) 152 | 153 | =item tls 154 | 155 | If you want to use TLS encyption. Default is 1. If you do not want to use 156 | encyption, you should specify the port as 25. 157 | 158 | =item timeout 159 | 160 | Connection timeout, in seconds (default is 30) 161 | 162 | =item domain 163 | 164 | The domain to use during the HELO portion of the SMTP protocol. Default is 165 | to use the local system hostname 166 | 167 | =back 168 | 169 | =head2 deliver(object) 170 | 171 | Delivers the Email::SendGrid object specified. This will return undef on 172 | success, and the failure reason on error 173 | 174 | =head1 AUTHOR 175 | 176 | Tim Jenkins 177 | 178 | =head1 COPYRIGHT 179 | 180 | Copyright (c) 2010 SendGrid. All rights reserved. 181 | This program is free software; you can redistribute it and/or modify 182 | it under the same terms as Perl itself. 183 | =cut 184 | 185 | 1; 186 | -------------------------------------------------------------------------------- /lib/Email/SendGrid/Transport/Sendmail.pm: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 SendGrid 2 | 3 | package Email::SendGrid::Transport::Sendmail; 4 | 5 | use strict; 6 | use vars qw($VERSION); 7 | 8 | $VERSION = '1.3'; 9 | 10 | use Sys::Hostname; 11 | use Carp; 12 | 13 | sub new 14 | { 15 | my $class = shift; 16 | 17 | my $self = bless { 'sendmail' => 'sendmail', 18 | @_ 19 | }, $class; 20 | 21 | return $self; 22 | } 23 | 24 | sub deliver 25 | { 26 | my $self = shift; 27 | my $sg = shift; 28 | 29 | my $mime = $sg->createMimeMessage(); 30 | my $msg = $mime->stringify(); 31 | 32 | my $fh; 33 | eval { 34 | $fh = $self->openSendmail($sg); 35 | }; 36 | return $@ if ( $@ ); 37 | 38 | print $fh $msg; 39 | 40 | close($fh); 41 | 42 | return undef; 43 | } 44 | 45 | sub openSendmail 46 | { 47 | my $self = shift; 48 | my $sg = shift; 49 | 50 | my @rcpts = $sg->getRecipients(); 51 | my $from = $sg->getMailFrom(); 52 | 53 | open(my $fh, "|$self->{sendmail} -oi -f $from @rcpts") || die "Could not open sendmail: $!"; 54 | 55 | return $fh; 56 | } 57 | 58 | 59 | =head1 NAME 60 | 61 | Email::SendGrid::Transport::Sendmail - Transport class for SendGrid using local sendmail 62 | 63 | =head1 SYNOPSIS 64 | 65 | use Email::SendGrid::Transport::Sendmail; 66 | use Email::SendGrid; 67 | 68 | my $sg = Email::SendGrid->new(); 69 | ... 70 | my $trans = Email::SendGrid::Transport::Sendmail' ); 71 | 72 | my $error = $trans->deliver($sg); 73 | die $error if ( $error ); 74 | 75 | =head1 DESCRIPTION 76 | 77 | This is a transport module for sending messages through the SendGrid email 78 | distribution system via your local sendmail application. After you have 79 | completed building your Email::SendGrid object, use this class to queue 80 | the message to your local system, which should be set up to relay mail 81 | through SendGrid's servers 82 | 83 | Using a local queueing mechanism like this is the preferred method to 84 | use SendGrid, since it allows for network issues to be handled while 85 | not complicating your application 86 | 87 | Note that despite the name Sendmail, this will work with any MTA that has 88 | a sendmail binary, such as Postfix 89 | 90 | =head1 CLASS METHODS 91 | 92 | =head2 new[ARGS] 93 | 94 | Creates the instance of the class. 95 | 96 | If for some reason sendmail is not in your path, you can specify the sendmail 97 | as the sendmail paramter 98 | 99 | =head2 deliver(object) 100 | 101 | Delivers the Email::SendGrid object specified. This will return undef on 102 | success, and the failure reason on error 103 | 104 | =head1 AUTHOR 105 | 106 | Tim Jenkins 107 | 108 | =head1 COPYRIGHT 109 | 110 | Copyright (c) 2010 SendGrid. All rights reserved. 111 | This program is free software; you can redistribute it and/or modify 112 | it under the same terms as Perl itself. 113 | =cut 114 | -------------------------------------------------------------------------------- /t/lib/Email/SendGrid/Header/Test.pm: -------------------------------------------------------------------------------- 1 | package Email::SendGrid::Header::Test; 2 | 3 | use strict; 4 | use base qw(Test::Class); 5 | use Test::More; 6 | use Test::Deep; 7 | 8 | use Email::SendGrid::Header; 9 | use JSON; 10 | use Encode; 11 | use Data::Dumper qw(Dumper); 12 | 13 | sub uniqueAgs : Test(2) 14 | { 15 | my $hdr = Email::SendGrid::Header->new(); 16 | 17 | $hdr->addUniqueIdentifier( foo => 'bar' ); 18 | 19 | ok(scalar(keys(%{$hdr->{data}->{unique_args}})) == 1 && 20 | $hdr->{data}->{unique_args}->{foo} eq 'bar', "set unique args"); 21 | 22 | $hdr->addUniqueIdentifier( { bar => 'foo' } ); 23 | ok(scalar(keys(%{$hdr->{data}->{unique_args}})) == 2 && 24 | $hdr->{data}->{unique_args}->{bar} eq 'foo', "set unique args with hash"); 25 | 26 | } 27 | 28 | sub category : Test 29 | { 30 | my $category = "foo"; 31 | 32 | my $hdr = Email::SendGrid::Header->new(); 33 | 34 | $hdr->setCategory($category); 35 | 36 | is($hdr->{data}->{category}, $category, "set category"); 37 | } 38 | 39 | sub filterSettings : Test(7) 40 | { 41 | my $hdr = Email::SendGrid::Header->new(); 42 | 43 | $hdr->addFilterSetting('clicktrack', 'enable', 1); 44 | 45 | is($hdr->{data}->{filters}->{clicktrack}->{settings}->{enable}, 1, "create filter setting"); 46 | 47 | $hdr->addFilterSetting('clicktrack', 'enable_text', 1); 48 | 49 | is($hdr->{data}->{filters}->{clicktrack}->{settings}->{enable_text}, 1, "add filter setting"); 50 | 51 | # Deep filter settings 52 | $hdr->addFilterSetting('myf', 'a', 'b', 'c', 1); 53 | $hdr->addFilterSetting('myf', 'a', 'b', 'd', 2); 54 | 55 | is($hdr->{data}->{filters}->{myf}->{settings}->{a}->{b}->{c}, 1, "deep filter setting"); 56 | is($hdr->{data}->{filters}->{myf}->{settings}->{a}->{b}->{d}, 2, "deep filter setting addition"); 57 | 58 | # Try to add a setting for something deep 59 | eval { 60 | $hdr->addFilterSetting('myf', 'a', 2); 61 | }; 62 | ok($@ =~ 'overwrite hash', "attempt to overwrite setting hash"); 63 | 64 | # Try to add a hash over a setting 65 | eval { 66 | $hdr->addFilterSetting('clicktrack', 'enable', 'a', 1); 67 | }; 68 | ok($@ =~ 'overwrite setting', "attempt to overwrite hash with setting"); 69 | 70 | } 71 | 72 | sub mailmerge : Test(2) 73 | { 74 | my $to = 'tim@sendgrid.com'; 75 | my $tag = 'name'; 76 | my $val = 'Tim Jenkins'; 77 | 78 | my $hdr = Email::SendGrid::Header->new(); 79 | 80 | $hdr->addTo($to); 81 | 82 | is($hdr->{data}->{to}->[0], $to, 'set to address'); 83 | 84 | $hdr->addSubVal($tag, $val); 85 | 86 | is($hdr->{data}->{'sub'}->{$tag}->[0], $val, "set substitution value"); 87 | } 88 | 89 | sub enabledisable : Test(2) 90 | { 91 | my $hdr = Email::SendGrid::Header->new(); 92 | 93 | $hdr->enable('clicktrack'); 94 | 95 | is($hdr->{data}->{filters}->{clicktrack}->{settings}->{enable}, 1, "enable filter"); 96 | 97 | $hdr->disable('clicktrack'); 98 | 99 | is($hdr->{data}->{filters}->{clicktrack}->{settings}->{enable}, 0, "disable filter"); 100 | } 101 | 102 | sub json : Test(2) 103 | { 104 | my $filt = { filters => 105 | { 'a' => { 'settings' => {'a' => 1}}, 106 | 'b' => { 'settings' => {'b' => 2, 'a' => 'foobarrrrr'}}, 107 | 'c' => { 'settings' => {'str' => 'thisisaverylongstringthatdoesnothaveanyspacesinit'}}, 108 | 'u' => "Some unicode \x{f441}", 109 | } 110 | }; 111 | 112 | my $filtJson = to_json($filt); 113 | 114 | my $hdr = Email::SendGrid::Header->new( data => $filt); 115 | 116 | my $length = 30; 117 | 118 | my $json = $hdr->asJSON( fold => $length ); 119 | 120 | # Convert back to an object to compare 121 | my $jsonCopy = $json; 122 | $jsonCopy =~ s/\n//g; 123 | my $obj = from_json($jsonCopy); 124 | cmp_deeply($obj, $filt, "decodes to same object"); 125 | is($json, encode('utf8', $json), "json has no special characters"); 126 | 127 | my @lines = split('\n', $json); 128 | 129 | my $properFold = 1; 130 | 131 | foreach my $line ( @lines ) 132 | { 133 | $properFold = 0 if ( !(length($line) < $length+3 || 134 | $line =~ $filt->{filters}->{c}->{settings}->{str}) ); 135 | } 136 | 137 | is($properFold, 1, "proper folding"); 138 | } 139 | 140 | sub asmGroupId : Test 141 | { 142 | my $asmGroupId = 123; 143 | 144 | my $hdr = Email::SendGrid::Header->new(); 145 | 146 | $hdr->setASMGroupID($asmGroupId); 147 | 148 | is($hdr->{data}->{asm_group_id}, $asmGroupId, "set ASM Group ID"); 149 | } 150 | 151 | 152 | 153 | 1; 154 | 155 | -------------------------------------------------------------------------------- /t/lib/Email/SendGrid/Test.pm: -------------------------------------------------------------------------------- 1 | package Email::SendGrid::Test; 2 | 3 | use strict; 4 | use base qw(Test::Class); 5 | use Test::More; 6 | 7 | use Email::SendGrid; 8 | use Encode; 9 | 10 | use Data::Dumper qw(Dumper); 11 | 12 | sub addresses : Test(12) 13 | { 14 | my $fromAddr = 'tim@sendgrid.net'; 15 | my $toAddr = 'tim@sendgrid.com'; 16 | my $from = "Tim Jenkisn <$fromAddr>"; 17 | my $to = "Tim Jenkins <$toAddr>"; 18 | my $text = 'Some text'; 19 | my $html = 'Some html'; 20 | my $encoding = 'base64'; 21 | my $charset = 'utf-8'; 22 | 23 | my $sg = Email::SendGrid->new( from => $from, 24 | to => $to, 25 | encoding => $encoding, 26 | charset => $charset, 27 | text => $text, 28 | html => $html, 29 | ); 30 | 31 | my $mime = $sg->createMimeMessage(); 32 | 33 | my $mailFrom = $sg->getMailFrom(); 34 | is($mailFrom, $fromAddr, 'mail from'); 35 | 36 | # Check to address 37 | my @rcpts = $sg->getRecipients(); 38 | ok(scalar(@rcpts) == 1 && $rcpts[0] eq $toAddr, "to address from string"); 39 | 40 | $sg = Email::SendGrid->new( to => [ $to, $from ] ); 41 | 42 | @rcpts = $sg->getRecipients(); 43 | ok(scalar(@rcpts) == 2 && $rcpts[0] eq $toAddr && $rcpts[1] eq $fromAddr, "to address as array ref"); 44 | 45 | # Check cc address 46 | $sg = Email::SendGrid->new( cc => "$to, $from" ); 47 | 48 | @rcpts = $sg->getRecipients(); 49 | ok(scalar(@rcpts) == 2 && $rcpts[0] eq $toAddr && $rcpts[1] eq $fromAddr, "cc address as string"); 50 | 51 | 52 | $sg = Email::SendGrid->new( cc => [ $to, $from ] ); 53 | 54 | @rcpts = $sg->getRecipients(); 55 | ok(scalar(@rcpts) == 2 && $rcpts[0] eq $toAddr && $rcpts[1] eq $fromAddr, "cc address as array ref"); 56 | 57 | # Check bcc address 58 | $sg = Email::SendGrid->new( bcc => "$to, $from" ); 59 | 60 | @rcpts = $sg->getRecipients(); 61 | ok(scalar(@rcpts) == 2 && $rcpts[0] eq $toAddr && $rcpts[1] eq $fromAddr, "bcc address as string"); 62 | 63 | 64 | $sg = Email::SendGrid->new( bcc => [ $to, $from ] ); 65 | 66 | @rcpts = $sg->getRecipients(); 67 | ok(scalar(@rcpts) == 2 && $rcpts[0] eq $toAddr && $rcpts[1] eq $fromAddr, "bcc address as array ref"); 68 | 69 | # Check with all addresses in place 70 | my $toa = [ $to, $from ]; 71 | my $toad = [ $toAddr, $fromAddr ]; 72 | my $cca = [ $from, $to ]; 73 | my $ccad = [ $fromAddr, $toAddr ]; 74 | my $bcca = [ $to, $from ]; 75 | my $bccad = [ $toAddr, $fromAddr ]; 76 | 77 | my @realrcpts = @$toad; 78 | push(@realrcpts, @$ccad, @$bccad); 79 | 80 | $sg = Email::SendGrid->new( to => $toa, cc => $cca, bcc => $bcca, 81 | from => $from, 82 | text => $text, 83 | html => $html, 84 | ); 85 | 86 | @rcpts = $sg->getRecipients(); 87 | my $match = 0; 88 | 89 | foreach my $i (0..$#rcpts) 90 | { 91 | $match++ if ( $rcpts[$i] eq $realrcpts[$i] ); 92 | } 93 | 94 | is($match, scalar(@realrcpts), "to, cc, and bcc address merges"); 95 | 96 | # Now check that the mime entity is created properly from this 97 | $mime = $sg->createMimeMessage(); 98 | 99 | my $mimeFrom = $mime->head->get('from'); 100 | my $mimeTo = $mime->head->get('to'); 101 | my $mimeCc = $mime->head->get('cc'); 102 | my $mimeBcc = $mime->head->get('bcc'); 103 | 104 | chomp($mimeFrom); 105 | chomp($mimeTo); 106 | chomp($mimeCc); 107 | 108 | is($mimeFrom, $from, 'mime from field'); 109 | is($mimeTo, "$to, $from", 'mime to field'); 110 | is($mimeCc, "$from, $to", 'mime cc field'); 111 | is($mimeBcc, undef, 'mime bcc field undefined'); 112 | } 113 | 114 | sub multipart : Test(4) 115 | { 116 | my $fromAddr = 'tim@sendgrid.net'; 117 | my $toAddr = 'tim@sendgrid.com'; 118 | my $from = "Tim Jenkisn <$fromAddr>"; 119 | my $to = "Tim Jenkins <$toAddr>"; 120 | my $text = 'Some text'; 121 | my $html = 'Some html'; 122 | my $encoding = 'base64'; 123 | my $charset = 'utf-8'; 124 | 125 | my $sg = Email::SendGrid->new( from => $from, 126 | to => $to, 127 | encoding => $encoding, 128 | charset => $charset, 129 | text => $text, 130 | html => $html, 131 | ); 132 | 133 | my $mime = $sg->createMimeMessage(); 134 | my @parts = $mime->parts(); 135 | 136 | ok($parts[0]->bodyhandle->as_string() eq $text && $parts[0]->mime_type eq 'text/plain', 'multipart text portion correct content'); 137 | ok($parts[1]->bodyhandle->as_string() eq $html && $parts[1]->mime_type eq 'text/html', 'multipart html portion correct content'); 138 | ok($parts[0]->head->mime_attr("content-type.charset") eq $charset && 139 | $parts[1]->head->mime_attr("content-type.charset") eq $charset, 'multipart correct charset'); 140 | ok($parts[0]->head->mime_encoding() eq $encoding && 141 | $parts[1]->head->mime_encoding() eq $encoding, 'multipart correct encoding'); 142 | } 143 | 144 | sub textonly : Test(3) 145 | { 146 | my $fromAddr = 'tim@sendgrid.net'; 147 | my $toAddr = 'tim@sendgrid.com'; 148 | my $from = "Tim Jenkisn <$fromAddr>"; 149 | my $to = "Tim Jenkins <$toAddr>"; 150 | my $text = 'Some text'; 151 | my $html = 'Some html'; 152 | my $encoding = 'base64'; 153 | my $charset = 'utf-8'; 154 | 155 | my $sg = Email::SendGrid->new( from => $from, 156 | to => $to, 157 | encoding => $encoding, 158 | charset => $charset, 159 | text => $text, 160 | ); 161 | 162 | my $mime = $sg->createMimeMessage(); 163 | my @parts = $mime->parts(); 164 | 165 | ok($mime->bodyhandle->as_string() eq $text && $mime->mime_type eq 'text/plain', 'text only correct content'); 166 | is($mime->head->mime_attr("content-type.charset"), $charset, 'text only correct charset'); 167 | is($mime->head->mime_encoding(), $encoding, 'text only correct encoding'); 168 | } 169 | 170 | sub htmlonly : Test(3) 171 | { 172 | my $fromAddr = 'tim@sendgrid.net'; 173 | my $toAddr = 'tim@sendgrid.com'; 174 | my $from = "Tim Jenkisn <$fromAddr>"; 175 | my $to = "Tim Jenkins <$toAddr>"; 176 | my $text = 'Some text'; 177 | my $html = 'Some html'; 178 | my $encoding = 'base64'; 179 | my $charset = 'utf-8'; 180 | 181 | my $sg = Email::SendGrid->new( from => $from, 182 | to => $to, 183 | encoding => $encoding, 184 | charset => $charset, 185 | html => $html, 186 | ); 187 | 188 | my $mime = $sg->createMimeMessage(); 189 | my @parts = $mime->parts(); 190 | 191 | ok($mime->bodyhandle->as_string() eq $html && $mime->mime_type eq 'text/html', 'html only correct content'); 192 | is($mime->head->mime_attr("content-type.charset"), $charset, 'html only correct charset'); 193 | is($mime->head->mime_encoding(), $encoding, 'html only correct encoding'); 194 | } 195 | 196 | sub headers : Test(4) 197 | { 198 | my $subject = "subject test"; 199 | my $date = 'now'; 200 | my $msgId = 'msg-id'; 201 | my $text = "some text"; 202 | 203 | my $sg = Email::SendGrid->new( subject => $subject, 204 | date => $date, 205 | 'message-id' => $msgId, 206 | text => $text ); 207 | 208 | my $mime = $sg->createMimeMessage(); 209 | my $s = $mime->head->get('subject'); 210 | chomp($s); 211 | is($s, $subject, "subject as parameter"); 212 | 213 | my $d = $mime->head->get('date'); 214 | chomp($d); 215 | is($d, $date, 'date as parameter'); 216 | 217 | my $m = $mime->head->get('message-id'); 218 | chomp($m); 219 | is($m, $msgId, 'message id as paramter'); 220 | 221 | # Test subject set method 222 | $sg = Email::SendGrid->new( text => $text ); 223 | $sg->set('subject', $subject); 224 | 225 | $mime = $sg->createMimeMessage(); 226 | $s = $mime->head->get('subject'); 227 | chomp($s); 228 | is($s, $subject, "subject set with function"); 229 | } 230 | 231 | sub unicode : Test(7) 232 | { 233 | my $fromAddr = 'tim@sendgrid.net'; 234 | my $toAddr = 'tim@sendgrid.com'; 235 | my $from = "Tim\x{311} Jenkisn <$fromAddr>"; 236 | my $to = "Tim\x{312} Jenkins <$toAddr>"; 237 | my $text = "Some unicode\x{587} text"; 238 | my $html = "Some unicode \x{465} html"; 239 | my $encoding = 'base64'; 240 | my $charset = 'utf-8'; 241 | my $subject = "subject \x{f441}"; 242 | my $reply = "some reply \x{411}"; 243 | 244 | my $sg = Email::SendGrid->new( from => $from, 245 | to => $to, 246 | subject => $subject, 247 | encoding => $encoding, 248 | charset => $charset, 249 | 'reply-to' => $reply, 250 | text => $text, 251 | html => $html, 252 | ); 253 | 254 | my $mime = $sg->createMimeMessage(); 255 | my @parts = $mime->parts(); 256 | 257 | my $mText = $parts[0]->bodyhandle->as_string(); 258 | my $mHtml = $parts[1]->bodyhandle->as_string(); 259 | 260 | ok( $mText ne $text && decode($charset, $mText) eq $text, 'unicode text portion correctly enocded'); 261 | ok( $mHtml ne $html && decode($charset, $mHtml) eq $html, 'unicode html portion correctly encoded'); 262 | 263 | # Test unicode headers 264 | my $s = $mime->head->get('subject'); 265 | chomp($s); 266 | 267 | my $sd = decode('MIME-Header', $s); 268 | 269 | ok($s =~ /\?Q\?/ && $sd eq $subject, "unicode headers, qp encoded"); 270 | 271 | # Base64 encoding 272 | my $subj = "subject2 \x{f441}\x{443}\x{3423}\x{4322}\x{4333}\x{111}\x{465}"; 273 | $sg->set('subject', $subj); 274 | 275 | $mime = $sg->createMimeMessage(); 276 | 277 | $s = $mime->head->get('subject'); 278 | chomp($s); 279 | 280 | $sd = decode('MIME-Header', $s); 281 | 282 | ok($s =~ /\?B\?/ && $sd eq $subj, "unicode headers, base64 encoded"); 283 | 284 | # Check the other headers to be sure of proper encoding 285 | my $h = 'to'; 286 | my $mh = $mime->head->get($h); 287 | chomp($mh); 288 | ok($mh =~ /\?Q\?/, "unicode $h encoded"); 289 | 290 | $h = 'from'; 291 | $mh = $mime->head->get($h); 292 | chomp($mh); 293 | ok($mh =~ /\?Q\?/, "unicode $h encoded"); 294 | 295 | $h = 'reply-to'; 296 | $mh = $mime->head->get($h); 297 | chomp($mh); 298 | ok($mh =~ /\?Q\?/, "unicode $h encoded"); 299 | 300 | } 301 | 302 | 303 | sub attachments : Test(no_plan) 304 | { 305 | my $fromAddr = 'tim@sendgrid.net'; 306 | my $toAddr = 'tim@sendgrid.com'; 307 | my $from = "Tim Jenkisn <$fromAddr>"; 308 | my $to = "Tim Jenkins <$toAddr>"; 309 | my $text = 'Some text'; 310 | my $html = 'Some html'; 311 | my $encoding = 'quoted-printable'; 312 | my $charset = 'utf-8'; 313 | 314 | my $sg = Email::SendGrid->new( from => $from, 315 | to => $to, 316 | encoding => $encoding, 317 | charset => $charset, 318 | html => $html, 319 | ); 320 | 321 | my $attachEncoding = 'binary'; 322 | my $attachType = 'application/pdf'; 323 | 324 | $sg->addAttachment( $text, encoding => $attachEncoding, type => $attachType ); 325 | 326 | my $mime = $sg->createMimeMessage(); 327 | my @parts = $mime->parts(); 328 | is(scalar(@parts), 2, "Attachment"); 329 | 330 | is($parts[1]->bodyhandle->as_string(), $text, "attachment correct content"); 331 | is($parts[1]->head->mime_type(), $attachType, "attachment correct type"); 332 | is($parts[1]->head->mime_encoding(), $attachEncoding, "attachment correct encoding"); 333 | } 334 | 335 | sub filterShortcuts : Test(no_plan) 336 | { 337 | my $sg = Email::SendGrid->new(); 338 | 339 | # click tracking 340 | $sg->enableClickTracking( text => 1 ); 341 | is($sg->header->{data}->{filters}->{clicktrack}->{settings}->{enable}, 1, 'enable click tracking'); 342 | is($sg->header->{data}->{filters}->{clicktrack}->{settings}->{enable_text}, 1, 'enable text click tracking'); 343 | 344 | $sg->disableClickTracking(); 345 | is($sg->header->{data}->{filters}->{clicktrack}->{settings}->{enable}, 0, 'disable click tracking'); 346 | 347 | # Open tracking 348 | $sg->enableOpenTracking(); 349 | is($sg->header->{data}->{filters}->{opentrack}->{settings}->{enable}, 1, 'enable open tracking'); 350 | 351 | $sg->disableOpenTracking(); 352 | is($sg->header->{data}->{filters}->{opentrack}->{settings}->{enable}, 0, 'disable open tracking'); 353 | 354 | # Spam check 355 | $sg->enableSpamCheck( score => 4, url => 'foo' ); 356 | is($sg->header->{data}->{filters}->{spamcheck}->{settings}->{enable}, 1, 'enable spam check'); 357 | is($sg->header->{data}->{filters}->{spamcheck}->{settings}->{maxscore}, 4, 'set spam check score'); 358 | is($sg->header->{data}->{filters}->{spamcheck}->{settings}->{url}, 'foo', 'set spam check url'); 359 | 360 | $sg->disableSpamCheck(); 361 | is($sg->header->{data}->{filters}->{spamcheck}->{settings}->{enable}, 0, 'disable spam check'); 362 | 363 | # Gravatar 364 | $sg->enableGravatar(); 365 | is($sg->header->{data}->{filters}->{gravatar}->{settings}->{enable}, 1, 'enable gravatar'); 366 | 367 | $sg->disableGravatar(); 368 | is($sg->header->{data}->{filters}->{gravatar}->{settings}->{enable}, 0, 'disable gravatar'); 369 | 370 | # Subscription tracking 371 | $sg->enableUnsubscribe( text => '<% %>', 372 | html => '<% here %>', 373 | replace => 'foo', 374 | ); 375 | 376 | is($sg->header->{data}->{filters}->{subscriptiontrack}->{settings}->{enable}, 1, 377 | 'enable subscription tracking'); 378 | is($sg->header->{data}->{filters}->{subscriptiontrack}->{settings}->{'text/plain'}, '<% %>', 379 | 'subscription text replacement'); 380 | is($sg->header->{data}->{filters}->{subscriptiontrack}->{settings}->{'text/html'}, '<% here %>', 381 | 'subscription html replacement'); 382 | is($sg->header->{data}->{filters}->{subscriptiontrack}->{settings}->{replace}, 'foo', 383 | 'subscription tag replacement'); 384 | 385 | eval { 386 | $sg->enableUnsubscribe( text => 'unsubscribe' ); 387 | }; 388 | ok ( $@ =~ /tag in text/, "subscription text checking" ); 389 | 390 | eval { 391 | $sg->enableUnsubscribe( html => '<% %>' ); 392 | }; 393 | ok ( $@ =~ /tag in html/, 'subscription html checking' ); 394 | 395 | $sg->disableUnsubscribe(); 396 | is($sg->header->{data}->{filters}->{subscriptiontrack}->{settings}->{enable}, 0, 397 | 'disable subscription tracking'); 398 | 399 | # Footer 400 | $sg->enableFooter( text => 'text', html => 'html' ); 401 | is($sg->header->{data}->{filters}->{footer}->{settings}->{enable}, 1, 'enable footer'); 402 | is($sg->header->{data}->{filters}->{footer}->{settings}->{'text/plain'}, "text", 'footer text setting'); 403 | is($sg->header->{data}->{filters}->{footer}->{settings}->{'text/html'}, "html", 'footer html setting'); 404 | 405 | $sg->disableFooter(); 406 | is($sg->header->{data}->{filters}->{footer}->{settings}->{enable}, 0, 'disable footer'); 407 | 408 | # Google Analytics 409 | $sg->enableGoogleAnalytics( source => 'source', 410 | medium => 'medium', 411 | term => 'term', 412 | content => 'content', 413 | campaign => 'campaign' ); 414 | 415 | is($sg->header->{data}->{filters}->{ganalytics}->{settings}->{enable}, 1, 'enable ganalytics'); 416 | foreach my $field ( qw(source medium term content campaign) ) 417 | { 418 | is($sg->header->{data}->{filters}->{ganalytics}->{settings}->{"utm_$field"}, $field, 419 | "ganalytics $field"); 420 | } 421 | 422 | $sg->disableGoogleAnalytics(); 423 | is($sg->header->{data}->{filters}->{ganalytics}->{settings}->{enable}, 0, 'disable ganalytics'); 424 | 425 | # Domain Keys 426 | $sg->enableDomainKeys( domain => 'domain', sender => 1 ); 427 | is($sg->header->{data}->{filters}->{domainkeys}->{settings}->{enable}, 1, 'enable domainkeys'); 428 | is($sg->header->{data}->{filters}->{domainkeys}->{settings}->{domain}, 'domain', 'domainkeys domain'); 429 | is($sg->header->{data}->{filters}->{domainkeys}->{settings}->{sender}, 1, 'domainkeys sender'); 430 | 431 | $sg->disableDomainKeys(); 432 | is($sg->header->{data}->{filters}->{domainkeys}->{settings}->{enable}, 0, 'disable domainkeys'); 433 | 434 | # Template 435 | $sg = Email::SendGrid->new(); 436 | $sg->enableTemplate( html => 'html<% %>' ); 437 | is($sg->header->{data}->{filters}->{template}->{settings}->{enable}, 1, 'enable template'); 438 | is($sg->header->{data}->{filters}->{template}->{settings}->{'text/html'}, 'html<% %>', 'template html'); 439 | 440 | $sg->disableTemplate(); 441 | is($sg->header->{data}->{filters}->{template}->{settings}->{enable}, 0, 'disable template'); 442 | 443 | # Template argument validation 444 | eval { $sg->enableTemplate() }; 445 | ok($@ =~ /Missing html/, 'template html tag required'); 446 | 447 | eval { $sg->enableTemplate( html => 'foo' ) }; 448 | ok($@ =~ /Missing body/, 'template html tag validation'); 449 | 450 | # Twitter 451 | $sg = Email::SendGrid->new(); 452 | $sg->enableTwitter( username => 'user', password => 'pass' ); 453 | is($sg->header->{data}->{filters}->{twitter}->{settings}->{enable}, 1, 'enable twitter'); 454 | is($sg->header->{data}->{filters}->{twitter}->{settings}->{username}, 'user', 'twitter username'); 455 | is($sg->header->{data}->{filters}->{twitter}->{settings}->{password}, 'pass', 'twitter password'); 456 | 457 | # Twitter argument validation 458 | eval { $sg->enableTwitter( password => 'foo' ) }; 459 | ok($@ =~ /username/, 'twitter username required'); 460 | 461 | eval { $sg->enableTwitter( username => 'foo' ) }; 462 | ok($@ =~ /password/, 'twitter password required'); 463 | 464 | $sg->disableTwitter(); 465 | is($sg->header->{data}->{filters}->{twitter}->{settings}->{enable}, 0, 'disable twitter'); 466 | 467 | # BCC 468 | $sg = Email::SendGrid->new(); 469 | $sg->enableBcc( email => 'email' ); 470 | is($sg->header->{data}->{filters}->{bcc}->{settings}->{enable}, 1, 'enable bcc'); 471 | is($sg->header->{data}->{filters}->{bcc}->{settings}->{email}, 'email', 'bcc email'); 472 | 473 | # BCC argument validation 474 | eval { $sg->enableBcc() }; 475 | ok($@ =~ /email/, 'bcc email validation'); 476 | 477 | # Bypass list management 478 | $sg = Email::SendGrid->new(); 479 | $sg->enableBypassListManagement(); 480 | is($sg->header->{data}->{filters}->{bypass_list_management}->{settings}->{enable}, 1, 481 | 'enable bypass list management'); 482 | } 483 | 484 | 485 | 1; 486 | -------------------------------------------------------------------------------- /t/lib/Email/SendGrid/Transport/REST/Test.pm: -------------------------------------------------------------------------------- 1 | package Email::SendGrid::Transport::REST::Test; 2 | 3 | use strict; 4 | use base qw(Test::Class); 5 | use Test::More; 6 | use Test::Deep; 7 | 8 | use MIME::Entity; 9 | use Email::SendGrid; 10 | use Email::SendGrid::Transport::REST; 11 | use Test::MockObject::Extends; 12 | use Test::MockModule; 13 | use URI::Escape; 14 | use Encode; 15 | use JSON; 16 | use Data::Dumper qw(Dumper); 17 | 18 | sub getTransport 19 | { 20 | my %args = @_; 21 | 22 | my $obj = Email::SendGrid::Transport::REST->new( username => 'u', 23 | password => 'p' ); 24 | 25 | my $mock = Test::MockObject::Extends->new($obj); 26 | 27 | foreach my $arg ( keys(%args) ) 28 | { 29 | $mock->mock($arg, $args{$arg}); 30 | } 31 | 32 | return $mock; 33 | } 34 | 35 | my $fromAddr = 'tim@sendgrid.net'; 36 | my $fromName = 'Tim Jenkins'; 37 | my $toAddr = 'tim@sendgrid.com'; 38 | my $toName = 'Tim Jenkins'; 39 | my $from = "$fromName <$fromAddr>"; 40 | my $to = "$toName <$toAddr>"; 41 | my $text = 'Some text'; 42 | my $html = 'Some html'; 43 | my $subject = 'subject'; 44 | my $encoding = 'base64'; 45 | my $charset = 'utf-8'; 46 | my $date = '2010 08 18'; 47 | my $messageId = "1234"; 48 | 49 | sub getSGObject 50 | { 51 | my %args = @_; 52 | 53 | my $extra; 54 | 55 | $extra = $args{unicode} if ( $args{unicode} ); 56 | 57 | my $sg = Email::SendGrid->new( from => "$extra$from", 58 | to => "$extra$to", 59 | subject => "$extra$subject", 60 | encoding => $encoding, 61 | charset => $charset, 62 | text => "$extra$text", 63 | html => "$extra$html", 64 | 'reply-to' => "$extra$to", 65 | "message-id" => $messageId, 66 | date => $date, 67 | ); 68 | 69 | return $sg; 70 | } 71 | 72 | sub create : Test(no_plan) 73 | { 74 | eval { 75 | my $trans = Email::SendGrid::Transport::REST->new( username => 'u' ); 76 | }; 77 | ok($@ =~ /Must speicfy username\/password or api key at/, "only providing username generated error"); 78 | 79 | eval { 80 | my $trans = Email::SendGrid::Transport::REST->new( password => 'u' ); 81 | }; 82 | ok($@ =~ /Must speicfy username\/password or api key at/, "only providing password generated error"); 83 | 84 | eval { 85 | my $trans = Email::SendGrid::Transport::REST->new( username => 'u', api_key => 'k' ); 86 | }; 87 | ok($@ =~ /Must only specify username\/password or api key, not both at/, "providing username and api key generated error"); 88 | 89 | } 90 | 91 | sub deliver : Test(no_plan) 92 | { 93 | my $deliv; 94 | my $sg; 95 | my $attachData = "my attachment"; 96 | $deliv = getTransport( 'send' => sub { 97 | my $self = shift; 98 | my $query = shift; 99 | 100 | my ($url, $str) = $query =~ /^([^\?]+)\?(.*)$/; 101 | 102 | my @params = split('&', $str); 103 | my $p = {}; 104 | 105 | foreach my $param (@params) 106 | { 107 | my ($key, $value) = split('=', $param); 108 | $p->{$key} = uri_unescape($value); 109 | } 110 | 111 | is($p->{api_user}, $deliv->{username}, "username set"); 112 | is($p->{api_key}, $deliv->{password}, "password set"); 113 | is($p->{subject}, $sg->get('subject'), "subject set"); 114 | is($p->{fromname}, $fromName, "from name set"); 115 | is($p->{from}, $fromAddr, "from addr set"); 116 | is($p->{'x-smtpapi'}, $sg->header->asJSON(), "smtp api header set"); 117 | is($p->{html}, $sg->get('html'), "html set"); 118 | is($p->{text}, $sg->get('text'), "text set"); 119 | is($p->{'to[]'}, $toAddr, "to addr set"); 120 | is($p->{'toname[]'}, $toName, "to name set"); 121 | 122 | my $hdrs = from_json($p->{headers}); 123 | cmp_deeply($hdrs, { "message-id" => $messageId }, "headers are included"); 124 | is($p->{"files[attachment1]"}, $attachData, "attachment included"); 125 | return { "message" => "success" }; 126 | }); 127 | 128 | $sg = getSGObject(); 129 | $sg->addAttachment($attachData); 130 | $sg->enableClickTracking(); 131 | 132 | my $res = $deliv->deliver($sg); 133 | 134 | is($res, undef, 'normal delivery'); 135 | } 136 | 137 | sub unicode : Test(8) 138 | { 139 | my $deliv; 140 | my $sg; 141 | 142 | my $u = "\x{587}"; 143 | 144 | $deliv = getTransport( 'send' => sub { 145 | my $self = shift; 146 | my $query = shift; 147 | 148 | my ($url, $str) = $query =~ /^([^\?]+)\?(.*)$/; 149 | 150 | my @params = split('&', $str); 151 | my $p = {}; 152 | 153 | foreach my $param (@params) 154 | { 155 | my ($key, $value) = split('=', $param); 156 | $p->{$key} = uri_unescape($value); 157 | } 158 | 159 | is($p->{subject}, $sg->get('subject', encode => 1), "unicode subject set"); 160 | is($p->{fromname}, encode('utf-8', "$u$fromName"), "unicode from name set"); 161 | is($p->{from}, $fromAddr, "unicode from addr set"); 162 | is($p->{'x-smtpapi'}, encode('utf-8', $sg->header->asJSON()), "unicode smtp api header set"); 163 | is($p->{html}, $sg->get('html', encode => 1), "unicode html set"); 164 | is($p->{text}, $sg->get('text', encode => 1), "unicode text set"); 165 | is($p->{'to[]'}, $toAddr, "unicode to addr set"); 166 | is($p->{'toname[]'}, encode('utf-8', "$u$toName"), "unicode to name set"); 167 | 168 | return { "message" => "success" }; 169 | }); 170 | 171 | $sg = getSGObject(unicode => $u); 172 | 173 | $sg->header->setCategory("$u"); 174 | 175 | my $res = $deliv->deliver($sg); 176 | } 177 | 178 | sub deliveryError : Test 179 | { 180 | my $deliv = getTransport( 'send' => sub { 181 | return { message => "error", 182 | errors => [ "errormsg" ] }; 183 | }); 184 | 185 | my $sg = getSGObject(); 186 | 187 | $sg->enableClickTracking(); 188 | 189 | my $res = $deliv->deliver($sg); 190 | 191 | is($res, "errormsg", 'error handling'); 192 | } 193 | 194 | sub send_up : Test(no_plan) 195 | { 196 | my $deliv = Test::MockObject::Extends->new(Email::SendGrid::Transport::REST->new(username => 'u', password => 'p')); 197 | my $content = { value => 1 }; 198 | my $response = HTTP::Response->new('200', "ok", [], to_json($content)); 199 | my $mm = Test::MockModule->new('LWP::UserAgent'); 200 | my $obj; 201 | $mm->mock('new' => sub { 202 | $obj = Test::MockObject->new(); 203 | $obj->set_always('default_header' => 1); 204 | $obj->mock('get' => sub { return $response } ); 205 | return $obj; 206 | }); 207 | 208 | my $query = "query"; 209 | my $resp = $deliv->send($query); 210 | 211 | cmp_deeply($resp, $content, "sent"); 212 | my ($func, $args) = $obj->next_call(); 213 | is($func, 'get', "made call to get"); 214 | shift(@$args); 215 | cmp_deeply($args, [$query], " with proper args"); 216 | 217 | ($func, $args) = $obj->next_call(); 218 | is($func, undef, "all lwp calls accounted for"); 219 | 220 | $response = HTTP::Response->new('403', 'bad error'); 221 | $resp = $deliv->send($query); 222 | cmp_deeply($resp, {errors => ['403 bad error']}, "error returned" ); 223 | 224 | ($func, $args) = $obj->next_call(); 225 | is($func, 'get', "made call to get"); 226 | shift(@$args); 227 | cmp_deeply($args, [$query], " with proper args"); 228 | 229 | ($func, $args) = $obj->next_call(); 230 | is($func, undef, "all lwp calls accounted for"); 231 | 232 | } 233 | 234 | sub send_apikey : Test(no_plan) 235 | { 236 | my $deliv = Test::MockObject::Extends->new(Email::SendGrid::Transport::REST->new(api_key => 'k')); 237 | my $content = { value => 1 }; 238 | my $response = HTTP::Response->new('200', "ok", [], to_json($content)); 239 | my $mm = Test::MockModule->new('LWP::UserAgent'); 240 | my $obj; 241 | $mm->mock('new' => sub { 242 | $obj = Test::MockObject->new(); 243 | $obj->set_always('default_header' => 1); 244 | $obj->mock('get' => sub { return $response } ); 245 | return $obj; 246 | }); 247 | 248 | my $query = "query"; 249 | my $resp = $deliv->send($query); 250 | 251 | cmp_deeply($resp, $content, "sent"); 252 | my ($func, $args) = $obj->next_call(); 253 | is($func, 'default_header', "made call to set header"); 254 | shift(@$args); 255 | cmp_deeply($args,['Authorization','Bearer k'], " with proper api key header"); 256 | 257 | ($func, $args) = $obj->next_call(); 258 | is($func, 'get', "made call to get"); 259 | shift(@$args); 260 | cmp_deeply($args, [$query], " with proper args"); 261 | 262 | ($func, $args) = $obj->next_call(); 263 | is($func, undef, "all lwp calls accounted for"); 264 | 265 | $response = HTTP::Response->new('403', 'bad error'); 266 | $resp = $deliv->send($query); 267 | cmp_deeply($resp, {errors => ['403 bad error']}, "error returned" ); 268 | 269 | ($func, $args) = $obj->next_call(); 270 | is($func, 'default_header', "made call to set header"); 271 | shift(@$args); 272 | cmp_deeply($args,['Authorization','Bearer k'], " with proper api key header"); 273 | 274 | ($func, $args) = $obj->next_call(); 275 | is($func, 'get', "made call to get"); 276 | shift(@$args); 277 | cmp_deeply($args, [$query], " with proper args"); 278 | 279 | ($func, $args) = $obj->next_call(); 280 | is($func, undef, "all lwp calls accounted for"); 281 | 282 | } 283 | 1; 284 | -------------------------------------------------------------------------------- /t/lib/Email/SendGrid/Transport/SMTP/Test.pm: -------------------------------------------------------------------------------- 1 | package Email::SendGrid::Transport::SMTP::Test; 2 | 3 | use strict; 4 | use base qw(Test::Class); 5 | use Test::More; 6 | 7 | use MIME::Entity; 8 | use Email::SendGrid; 9 | use Email::SendGrid::Transport::SMTP; 10 | 11 | sub getSGObject 12 | { 13 | my $fromAddr = 'tim@sendgrid.net'; 14 | my $toAddr = 'tim@sendgrid.com'; 15 | my $from = "Tim Jenkins <$fromAddr>"; 16 | my $to = "Tim Jenkins <$toAddr>"; 17 | my $text = 'Some text'; 18 | my $html = 'Some html'; 19 | my $encoding = 'base64'; 20 | my $charset = 'utf-8'; 21 | 22 | my $sg = Email::SendGrid->new( from => $from, 23 | to => $to, 24 | encoding => $encoding, 25 | charset => $charset, 26 | text => $text, 27 | html => $html, 28 | ); 29 | 30 | return $sg; 31 | } 32 | 33 | sub delivery : Test() 34 | { 35 | my $smtp = Mock::Net::SMTP::TLS->create(); 36 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 37 | 'password' => 'testing', 38 | 'smtp_class' => $smtp 39 | ); 40 | 41 | 42 | my $sg = getSGObject(); 43 | 44 | my $res = $deliv->deliver($sg); 45 | 46 | is($res, undef, 'normal delivery'); 47 | } 48 | 49 | sub create : Test(no_plan) 50 | { 51 | eval { 52 | my $trans = Email::SendGrid::Transport::SMTP->new( username => 'u' ); 53 | }; 54 | ok($@ =~ /Must speicfy username\/password or api key at/, "only providing username generated error"); 55 | 56 | eval { 57 | my $trans = Email::SendGrid::Transport::SMTP->new( password => 'u' ); 58 | }; 59 | ok($@ =~ /Must speicfy username\/password or api key at/, "only providing password generated error"); 60 | 61 | eval { 62 | my $trans = Email::SendGrid::Transport::SMTP->new( username => 'u', api_key => 'k' ); 63 | }; 64 | ok($@ =~ /Must only specify username\/password or api key, not both at/, "providing username and api key generated error"); 65 | 66 | my $trans = Email::SendGrid::Transport::SMTP->new( api_key => 'k' ); 67 | is($trans->{username}, 'apikey', "username set to apikey"); 68 | is($trans->{password}, 'k', 'password set to apikey value') 69 | } 70 | ################################################################################################### 71 | # Tests for the SMTP transaction 72 | sub connection_refused : Test() 73 | { 74 | my $smtp = Mock::Net::SMTP::TLS->create( 'new' => 75 | sub { die "Connect failed :IO::Socket::INET: connect: Connection refused"; } 76 | ); 77 | 78 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 79 | 'password' => 'testing', 80 | 'smtp_class' => $smtp 81 | ); 82 | 83 | my $sg = getSGObject(); 84 | 85 | my $error = $deliv->deliver($sg); 86 | 87 | ok($error =~ /refused/, 'connection refused'); 88 | } 89 | 90 | sub unknown_host : Test 91 | { 92 | my $smtp = Mock::Net::SMTP::TLS->create( 'new' => 93 | sub { die "Connect failed :IO::Socket::INET: Bad hostname '$_[1]'"; } 94 | ); 95 | 96 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 97 | 'password' => 'testing', 98 | 'smtp_class' => $smtp 99 | ); 100 | 101 | my $sg = getSGObject(); 102 | 103 | my $error = $deliv->deliver($sg); 104 | 105 | ok($error =~ /Bad hostname/, 'unknown host'); 106 | } 107 | 108 | sub failed_authentication : Test 109 | { 110 | my $smtp = Mock::Net::SMTP::TLS->create( 'new' => 111 | sub { die "Auth failed: 535 5.7.8 Error: authentication failed: authentication failure"; } 112 | ); 113 | 114 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 115 | 'password' => 'testing', 116 | 'smtp_class' => $smtp 117 | ); 118 | 119 | my $sg = getSGObject(); 120 | 121 | my $error = $deliv->deliver($sg); 122 | 123 | ok($error =~ /Auth failed/, 'failed authentication'); 124 | } 125 | 126 | sub from_rejection : Test 127 | { 128 | my $smtp = Mock::Net::SMTP::TLS->create( 'mail' => sub { 129 | my $self = shift; 130 | my $from = shift; 131 | die "Couldn't send MAIL <$from> 5.7.1 Relay denied"; } 132 | ); 133 | 134 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 135 | 'password' => 'testing', 136 | 'smtp_class' => $smtp 137 | ); 138 | 139 | my $sg = getSGObject(); 140 | 141 | my $error = $deliv->deliver($sg); 142 | 143 | ok($error =~ /send MAIL.*tim\@sendgrid.net/, 'mail command rejected'); 144 | } 145 | 146 | sub to_rejection : Test 147 | { 148 | my $smtp = Mock::Net::SMTP::TLS->create( 'to' => sub { 149 | my $self = shift; 150 | my $to = shift; 151 | die "Couldn't send TO <$to>: 554 5.7.1 : Client host rejected: Access denied"; 152 | }); 153 | 154 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 155 | 'password' => 'testing', 156 | 'smtp_class' => $smtp 157 | ); 158 | 159 | my $sg = getSGObject(); 160 | 161 | my $error = $deliv->deliver($sg); 162 | 163 | ok($error =~ /send TO.*tim\@sendgrid.com/, 'to command rejected'); 164 | } 165 | 166 | sub data_error : Test 167 | { 168 | my $smtp = Mock::Net::SMTP::TLS->create( data => sub { 169 | die "An error occurred during DATA"; 170 | }); 171 | 172 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 173 | 'password' => 'testing', 174 | 'smtp_class' => $smtp 175 | ); 176 | 177 | my $sg = getSGObject(); 178 | 179 | my $error = $deliv->deliver($sg); 180 | 181 | ok($error =~ /during DATA\W/, 'data error'); 182 | } 183 | 184 | sub datasend_error : Test 185 | { 186 | my $smtp = Mock::Net::SMTP::TLS->create( datasend => sub { 187 | die "An error occurred during datasend"; 188 | }); 189 | 190 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 191 | 'password' => 'testing', 192 | 'smtp_class' => $smtp 193 | ); 194 | 195 | my $sg = getSGObject(); 196 | 197 | my $error = $deliv->deliver($sg); 198 | 199 | ok($error =~ /during datasend\W/, 'datasend error'); 200 | } 201 | 202 | sub dataend_error : Test 203 | { 204 | my $smtp = Mock::Net::SMTP::TLS->create( dataend => sub { 205 | die "An error occurred during dataend"; 206 | }); 207 | 208 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 209 | 'password' => 'testing', 210 | 'smtp_class' => $smtp 211 | ); 212 | 213 | my $sg = getSGObject(); 214 | 215 | my $error = $deliv->deliver($sg); 216 | 217 | ok($error =~ /during dataend\W/, 'dataend error'); 218 | } 219 | 220 | # Errors on quit should be ignored 221 | sub quit_error : Test 222 | { 223 | my $smtp = Mock::Net::SMTP::TLS->create( quit => sub { 224 | die "An error occurred disconnecting from the mail server"; 225 | }); 226 | 227 | my $deliv = Email::SendGrid::Transport::SMTP->new( 'username' => 'tim@sendgrid.net', 228 | 'password' => 'testing', 229 | 'smtp_class' => $smtp 230 | ); 231 | 232 | my $sg = getSGObject(); 233 | 234 | my $error = $deliv->deliver($sg); 235 | 236 | is($error, undef, 'handling of error on quit'); 237 | } 238 | 239 | 1; 240 | -------------------------------------------------------------------------------- /t/lib/Mock/Net/SMTP/TLS.pm: -------------------------------------------------------------------------------- 1 | package Mock::Net::SMTP::TLS; 2 | 3 | use strict; 4 | use Data::Dumper qw(Dumper); 5 | use Carp; 6 | 7 | our $AUTOLOAD; 8 | 9 | sub create 10 | { 11 | my $this = shift; 12 | my $class = ref($this) || $this; 13 | 14 | my $self = bless{ 15 | new => sub { return $_[0]; }, 16 | mail => sub { return 1; }, 17 | to => sub { return 1; }, 18 | data => sub { return 1; }, 19 | datasend => sub { return 1; }, 20 | dataend => sub { return 1; }, 21 | quit => sub { return 1; }, 22 | @_ }, $class; 23 | 24 | return $self; 25 | } 26 | 27 | # Autload functions for all the methods we have objects for 28 | sub AUTOLOAD 29 | { 30 | my $self = shift; 31 | 32 | my $type = ref ($self) || croak "$self is not an object"; 33 | my $field = $AUTOLOAD; 34 | $field =~ s/.*://; 35 | 36 | unless (exists $self->{$field}) 37 | { 38 | croak "$field does not exist in object/class $type"; 39 | } 40 | 41 | return $self->{$field}($self, @_); 42 | } 43 | 44 | # Keep autoload from pissing things off 45 | sub DESTROY 46 | { 47 | } 48 | 1; 49 | 50 | -------------------------------------------------------------------------------- /t/test.t: -------------------------------------------------------------------------------- 1 | #! /usr/bin/perl 2 | use Test::Class::Load '.'; 3 | 4 | $ENV{TEST_METHOD} = $ARGV[0] if defined($ARGV[0]); 5 | 6 | # run all the test methods in the libraries we've loaded 7 | Test::Class->runtests; 8 | --------------------------------------------------------------------------------