Jump to content

New module type: WireMail


ryan

Recommended Posts

Lots of people have been asking for a way for ProcessWire to support sending of email, outside of just using PHP's mail() function. I haven't really wanted to expand the scope of ProcessWire that deep into email sending, but I have been wanting to setup a way so that people could override how emails are sent, with modules. For people that are interested in making other ways of sending email in ProcessWire, I've setup a new module base class called WireMail, and a new function called wireMail(). The wireMail() function will use whatever WireMail module is installed. If none is installed, then it will use PW's default WireMail implementation (based on PHP's default mail function). 

The wireMail() function replaces all instances of PHP's mail() function in ProcessWire's source. It works in a similar way as PHP's mail() except that supports a few different usages. Standard usage would be this:

// to, from, subject, body
wireMail('user@domain.com', 'ryan@runs.pw', 'Mail Subject', 'Mail Body'); 

Another usage would be to give it no arguments, and it'll return whatever WireMail module is installed for you to use yourself. If no WireMail module is installed, then it returns ProcessWire's WireMail.

$mail = wireMail();
$mail->to('user@hi.com')->from('ryan@runs.pw'); // all calls can be chained
$mail->subject('Mail Subject'); 
$mail->body('Mail Body');
$mail->bodyHTML('<html><body><h1>Mail Body</h1></body></html>'); 
$mail->send(); 

Since all of this stuff is only on the PW 2.4 dev branch at present, I'm posting this primarily for people that are interested in creating WireMail modules. For instance, I know that both Teppo and Horst (and perhaps others?) have put together such modules and ideas, so this is all aimed at coming up with a way for those ideas to be easily integrated into PW by way of modules. To make your own WireMail module, you simply create a module that extends WireMail and provide your own implementation for the send() method. Of course, you can go further than that, but that's all that is technically necessary. I've attached an example module called WireMailTest that demonstrates a WireMail module. When installed, it is used rather than PW's WireMail. This WireMailTest module includes lots of comments for you in the code of it, and you may find it helpful to use it as your starting point. 

WireMailTest.module

For you guys developing modules, please throw any questions my way and I'm happy to help. Likewise, let me know if you think I'm missing anything important in the base interface that the modules are based upon and we can update it. 

  • Like 29
Link to comment
Share on other sites

@ryan: just noticed that use case 3, "specify both $body and $bodyHTML as arguments, but no $options", doesn't seem to work as described. Later $options is used as an argument of array_merge(), which means that it has to be an array itself.

Link to comment
Share on other sites

I can't seem to break it anymore myself, so I've just pushed Swift Mailer module to GitHub. It's still far from complete and probably lacking lots of important stuff, but if anyone wants to give it a try already please do so.. and let me know how it went. I'll create an "official" thread when it feels slightly more polished :)

  • Like 5
Link to comment
Share on other sites

Ryan, I would like to have the possibility not to use only the Emailaddress but also the Recipients Names with the TO-array. Actually it accepts only a single emailaddress or an array with emailaddresses, but no names.

When building that on my own I would have to break compatibility. Don't like that.

Could we not use something like:

	public function to($email) {
		if(!is_array($email)) $email = explode(',', $email);
		$this->mail['to'] = array(); // clear
		// check for key-value pairs containing name=>email
		if(array_keys($email) !== range(0, count($email) - 1)) {
			foreach($email as $n=>$e) {
				$this->mail['to'][$n] = $this->sanitizeEmail($e);
			}
		} else {
			foreach($email as $e) {
				$this->mail['to'][] = $this->sanitizeEmail($e);
			}
		}
		return $this;
	}

// also within the send() function we have to check if we have array with name=>email or string only with email

I'm not happy with my code example. A better way is much appreciated, but I really would like to have that possibility.

----------------------------------------------------------

- EDIT

----------------------------------------------------------

Now after some testing I came up with this code what seems to be better:

	public function to($email) {
		if(!is_array($email)) $email = explode(',', $email);
		$this->mail['to'] = array(); // clear
		foreach($email as $n=>$e) {
			// check for associative key-value pairs containing name=>email
			if(is_string($n)) {
				$this->mail[$type][$this->sanitizeHeader($n)] = $this->sanitizeEmail($e);
			}
			else {
				$this->mail['to'][] = $this->sanitizeEmail($e);
			}
		}
		return $this;
	}

And in the send() function we need to loop like this:

		$numSent = 0;
		foreach($this->to as $n=>$to) {
			$to = !is_string($n) ? $to : ( $n . ' <' . $to . '>' );
			if(mail($to, $this->subject, $body, $header, $param)) $numSent++;
		}
Edited by horst
  • Like 4
Link to comment
Share on other sites

Does it also mean that when this module is installed, the FormBuilder will be able to send mail through this module 'out of the box'?

FormBuilder needs to be updated to use wireMail() instead of mail() first, so not exactly out of the box, but I'm sure Ryan will take care of this :)

  • Like 1
Link to comment
Share on other sites

@ryan: just noticed that use case 3, "specify both $body and $bodyHTML as arguments, but no $options", doesn't seem to work as described. Later $options is used as an argument of array_merge(), which means that it has to be an array itself.

Thanks, just pushed a fix for this. 

Ryan, I would like to have the possibility not to use only the Emailaddress but also the Recipients Names with the TO-array. Actually it accepts only a single emailaddress or an array with emailaddresses, but no names.

Good point! While I almost never use a "TO: name" (and many email clients, including gmail, don't even display it), it does make sense to support it.  I've just updated it to support this. I went a little bit different route than you mentioned though, because there's always a chance two recipients might have the same name, but not likely they would have the same email. So I didn't think name would be workable as an array index. I also wanted to maintain consistency with how it's storing 'from' and 'fromName' separately, so the 'to' and 'toName' are stored in separate arrays. The 'toName' array is indexed by the email address. So now if you want to support use of 'toName' in your send() method, you'd do this:

foreach($this->to as $email) {
  $name = $this->toName[$email]; 
  if($name) {
    // send to: Name <$email>
  } else {
    // send to: $email
  }
}

If you don't need/want to support the "TO: name" (as in WireMailTest) then you don't have to do anything, as the means by which you access and iterate $this->to has not changed. 

As for how to supply to "TO: name" from the API side, you can do it any of these ways: 

// you can also use a string (or CSV string for multiple), like you would with PHP mail
wireMail('User Name <user@example.com>', $from, $subject, $body);

// array may be simplest if sending to multiple email addresses
wireMail(array('user@example.com' => 'User Name'), $from, $subject, $body); 

// from the object side you can supply it as an optional 2nd argument to the to() method
$mail = wireMail(); 
$mail->to('user@example.com', 'User Name'); 

// or in an array
$mail->to(array('user@example.com' => 'User Name'));

// or as a string (or CSV string for multiple)
$mail->to('User Name <user@example.com>'); 

// the WireMail::from has also been updated to support a second argument or string
$mail->from('sender@example.com', 'Sender Name');  
$mail->from('Sender Name <sender@example.com>'); 

One other change is that the to() method no longer clears out any existing to() addresses on every call. So you don't have to supply all of your to: addresses in the single function call, you can call it multiple times, each with a separate email address if preferred. But if you do want to clear out the list, then just call the to() method with no arguments and it'll clear them. 

Does it also mean that when this module is installed, the FormBuilder will be able to send mail through this module 'out of the box'?

Yes, once we've merged the WireMail class into the master branch (stable), then I'll put out an updated FormBuilder that uses wireMail() rather than mail(). Though if anyone needs it sooner, I'll be happy to post an updated FormBuilderEmail class that uses wireMail(). 

  • Like 4
Link to comment
Share on other sites

Yes, once we've merged the WireMail class into the master branch (stable), then I'll put out an updated FormBuilder that uses wireMail() rather than mail(). Though if anyone needs it sooner, I'll be happy to post an updated FormBuilderEmail class that uses wireMail().

Can you please post an updated FormBuilderEmail class that uses wireMail() sooner rather than later.  Thanks.

  • Like 1
Link to comment
Share on other sites

  • 3 weeks later...

@Ryan: what do you think of adding a method to the WireMail class that tells the user if the currently active Module can send attachments?

if(wireMail()->canSendAttachments()) { wireMail()->attachment($file)->to($to)->send(); }

Link to comment
Share on other sites

  • 4 weeks later...

Can we have options for CC and BCC please ryan (and then Teppo in SwiftMailer :))? It's not mega urgent, but these do come in handy in a few situations.

@Pete: you can ask Teppo directly. He (technically *) can implement it in SwiftMailer without any change in WIreMail base class. I have done it for bcc and cc. It is not more than to add these two as public functions to the SwiftMailer module and make them handle the according SwiftMailer calls for that headers. Simple!

* don't know about his time budget nor if he ever want it  :lol:

Edited by horst
  • Like 1
Link to comment
Share on other sites

The problem with just implementing it in Swiftmailer (or in your class) is this from ryan's first post:

If none is installed, then it will use PW's default WireMail implementation (based on PHP's default mail function).

So I can't count on the user not just using the default WireMail class and certain functionality not being available. I think that the WireMail base class needs to have these functions set up before subclasses (is that the correct term?) implement features or we'll have issues with things working consistently or we'll have to do some checks to see which Mail module is installed.

My usage in this case is writing modules that use the WireMail class, so I need the functionality in the base class to be sure that the emails will definitely send :)

  • Like 1
Link to comment
Share on other sites

I haven't had much (enough) time to work on my mailer module and haven't looked at how WireMailSMTP handles these particular things, but in general I'd have to agree with Pete. For things that are commonly used, it'd be best if there was a "standard" way to do that. One that doesn't depend on which module extending WireMail is installed at the time.

I believe that we're talking about interfaces here, though that's not technically what it's going to be.. or what it is at the moment, at least.

Then again, if @horst has implemented this feature already, I'll probably take a look at his implementation anyway and use similar public methods if possible to provide some consistency :)

  • Like 2
Link to comment
Share on other sites

I think Ryans initial intention was to support the common basic usage and give us an easy to use possibilty to cretae submodules that can _extend_ the base class. :)

But I am definitly not against extending the base class with cc, bcc and attachment

  • Like 1
Link to comment
Share on other sites

I'm going to highlight this again horst as it's the important bit :P:)

If none is installed, then it will use PW's default WireMail implementation (based on PHP's default mail function).

So whether yours or Teppo's module is installed or neither, emails can still be sent using the default Wiremail class.

Therefore if I'm writing a module that requires email sending functionality, it needs to be able to use the same functions whether it's ryan's base module, the SwiftMailer class or your own module or module authors won't know what to expect. I think these WireMail modules need to be coordinated somewhat so that whatever the base class supports, the other classes also support so there are no surprises.

  • Like 2
Link to comment
Share on other sites

@Pete: we are on the same side ;)  (in german: "Du rennst bei mir offene Türen ein")
 

But I am definitly not against extending the base class with cc, bcc and attachment

+1 for implementing cc, bcc and attachment into the WireMail base class!

Regarding attachments: https://processwire.com/talk/topic/5704-module-wiremailsmtp/#entry56631

  • Like 2
Link to comment
Share on other sites

  • 3 months later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...