SMS Reminder by Larry Lart
[openemr.git] / library / classes / smtp / smtp.php
blob4338aa9aa3a665f1236c0f6ae4c2e581b9defe55
1 <?php
2 /*
3 * smtp.php
5 * @(#) $Header$
7 */
9 class smtp_class
11 var $user="";
12 var $realm="";
13 var $password="";
14 var $workstation="";
15 var $authentication_mechanism="";
16 var $host_name="";
17 var $host_port=25;
18 var $ssl=0;
19 var $localhost="";
20 var $timeout=0;
21 var $data_timeout=0;
22 var $direct_delivery=0;
23 var $error="";
24 var $debug=0;
25 var $html_debug=0;
26 var $esmtp=1;
27 var $esmtp_host="";
28 var $esmtp_extensions=array();
29 var $maximum_piped_recipients=100;
30 var $exclude_address="";
31 var $getmxrr="GetMXRR";
32 var $pop3_auth_host="";
33 var $pop3_auth_port=110;
35 /* private variables - DO NOT ACCESS */
37 var $state="Disconnected";
38 var $connection=0;
39 var $pending_recipients=0;
40 var $next_token="";
41 var $direct_sender="";
42 var $connected_domain="";
43 var $result_code;
44 var $disconnected_error=0;
46 /* Private methods - DO NOT CALL */
48 Function Tokenize($string,$separator="")
50 if(!strcmp($separator,""))
52 $separator=$string;
53 $string=$this->next_token;
55 for($character=0;$character<strlen($separator);$character++)
57 if(GetType($position=strpos($string,$separator[$character]))=="integer")
58 $found=(IsSet($found) ? min($found,$position) : $position);
60 if(IsSet($found))
62 $this->next_token=substr($string,$found+1);
63 return(substr($string,0,$found));
65 else
67 $this->next_token="";
68 return($string);
72 Function OutputDebug($message)
74 $message.="\n";
75 if($this->html_debug)
76 $message=str_replace("\n","<br />\n",HtmlEntities($message));
77 echo $message;
78 flush();
81 Function SetDataAccessError($error)
83 $this->error=$error;
84 if(function_exists("socket_get_status"))
86 $status=socket_get_status($this->connection);
87 if($status["timed_out"])
88 $this->error.=": data access time out";
89 elseif($status["eof"])
91 $this->error.=": the server disconnected";
92 $this->disconnected_error=1;
97 Function GetLine()
99 for($line="";;)
101 if(feof($this->connection))
103 $this->error="reached the end of data while reading from the SMTP server conection";
104 return("");
106 if(GetType($data=@fgets($this->connection,100))!="string"
107 || strlen($data)==0)
109 $this->SetDataAccessError("it was not possible to read line from the SMTP server");
110 return("");
112 $line.=$data;
113 $length=strlen($line);
114 if($length>=2
115 && substr($line,$length-2,2)=="\r\n")
117 $line=substr($line,0,$length-2);
118 if($this->debug)
119 $this->OutputDebug("S $line");
120 return($line);
125 Function PutLine($line)
127 if($this->debug)
128 $this->OutputDebug("C $line");
129 if(!@fputs($this->connection,"$line\r\n"))
131 $this->SetDataAccessError("it was not possible to send a line to the SMTP server");
132 return(0);
134 return(1);
137 Function PutData(&$data)
139 if(strlen($data))
141 if($this->debug)
142 $this->OutputDebug("C $data");
143 if(!@fputs($this->connection,$data))
145 $this->SetDataAccessError("it was not possible to send data to the SMTP server");
146 return(0);
149 return(1);
152 Function VerifyResultLines($code,&$responses)
154 $responses=array();
155 Unset($this->result_code);
156 while(strlen($line=$this->GetLine($this->connection)))
158 if(IsSet($this->result_code))
160 if(strcmp($this->Tokenize($line," -"),$this->result_code))
162 $this->error=$line;
163 return(0);
166 else
168 $this->result_code=$this->Tokenize($line," -");
169 if(GetType($code)=="array")
171 for($codes=0;$codes<count($code) && strcmp($this->result_code,$code[$codes]);$codes++);
172 if($codes>=count($code))
174 $this->error=$line;
175 return(0);
178 else
180 if(strcmp($this->result_code,$code))
182 $this->error=$line;
183 return(0);
187 $responses[]=$this->Tokenize("");
188 if(!strcmp($this->result_code,$this->Tokenize($line," ")))
189 return(1);
191 return(-1);
194 Function FlushRecipients()
196 if($this->pending_sender)
198 if($this->VerifyResultLines("250",$responses)<=0)
199 return(0);
200 $this->pending_sender=0;
202 for(;$this->pending_recipients;$this->pending_recipients--)
204 if($this->VerifyResultLines(array("250","251"),$responses)<=0)
205 return(0);
207 return(1);
210 Function ConnectToHost($domain, $port, $resolve_message)
212 if($this->ssl)
214 $version=explode(".",function_exists("phpversion") ? phpversion() : "3.0.7");
215 $php_version=intval($version[0])*1000000+intval($version[1])*1000+intval($version[2]);
216 if($php_version<4003000)
217 return("establishing SSL connections requires at least PHP version 4.3.0");
218 if(!function_exists("extension_loaded")
219 || !extension_loaded("openssl"))
220 return("establishing SSL connections requires the OpenSSL extension enabled");
222 if(ereg('^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$',$domain))
223 $ip=$domain;
224 else
226 if($this->debug)
227 $this->OutputDebug($resolve_message);
228 if(!strcmp($ip=@gethostbyname($domain),$domain))
229 return("could not resolve host \"".$domain."\"");
231 if(strlen($this->exclude_address)
232 && !strcmp(@gethostbyname($this->exclude_address),$ip))
233 return("domain \"".$domain."\" resolved to an address excluded to be valid");
234 if($this->debug)
235 $this->OutputDebug("Connecting to host address \"".$ip."\" port ".$port."...");
236 if(($this->connection=($this->timeout ? @fsockopen(($this->ssl ? "ssl://" : "").$ip,$port,$errno,$error,$this->timeout) : @fsockopen(($this->ssl ? "ssl://" : "").$ip,$port))))
237 return("");
238 $error=($this->timeout ? strval($error) : "??");
239 switch($error)
241 case "-3":
242 return("-3 socket could not be created");
243 case "-4":
244 return("-4 dns lookup on hostname \"".$domain."\" failed");
245 case "-5":
246 return("-5 connection refused or timed out");
247 case "-6":
248 return("-6 fdopen() call failed");
249 case "-7":
250 return("-7 setvbuf() call failed");
252 return("could not connect to the host \"".$domain."\": ".$error);
255 Function SASLAuthenticate($mechanisms, $credentials, &$authenticated, &$mechanism)
257 $authenticated=0;
258 if(!function_exists("class_exists")
259 || !class_exists("sasl_client_class"))
261 $this->error="it is not possible to authenticate using the specified mechanism because the SASL library class is not loaded";
262 return(0);
264 $sasl=new sasl_client_class;
265 $sasl->SetCredential("user",$credentials["user"]);
266 $sasl->SetCredential("password",$credentials["password"]);
267 if(IsSet($credentials["realm"]))
268 $sasl->SetCredential("realm",$credentials["realm"]);
269 if(IsSet($credentials["workstation"]))
270 $sasl->SetCredential("workstation",$credentials["workstation"]);
271 if(IsSet($credentials["mode"]))
272 $sasl->SetCredential("mode",$credentials["mode"]);
275 $status=$sasl->Start($mechanisms,$message,$interactions);
277 while($status==SASL_INTERACT);
278 switch($status)
280 case SASL_CONTINUE:
281 break;
282 case SASL_NOMECH:
283 if(strlen($this->authentication_mechanism))
285 $this->error="authenticated mechanism ".$this->authentication_mechanism." may not be used: ".$sasl->error;
286 return(0);
288 break;
289 default:
290 $this->error="Could not start the SASL authentication client: ".$sasl->error;
291 return(0);
293 if(strlen($mechanism=$sasl->mechanism))
295 if($this->PutLine("AUTH ".$sasl->mechanism.(IsSet($message) ? " ".base64_encode($message) : ""))==0)
297 $this->error="Could not send the AUTH command";
298 return(0);
300 if(!$this->VerifyResultLines(array("235","334"),$responses))
301 return(0);
302 switch($this->result_code)
304 case "235":
305 $response="";
306 $authenticated=1;
307 break;
308 case "334":
309 $response=base64_decode($responses[0]);
310 break;
311 default:
312 $this->error="Authentication error: ".$responses[0];
313 return(0);
315 for(;!$authenticated;)
319 $status=$sasl->Step($response,$message,$interactions);
321 while($status==SASL_INTERACT);
322 switch($status)
324 case SASL_CONTINUE:
325 if($this->PutLine(base64_encode($message))==0)
327 $this->error="Could not send the authentication step message";
328 return(0);
330 if(!$this->VerifyResultLines(array("235","334"),$responses))
331 return(0);
332 switch($this->result_code)
334 case "235":
335 $response="";
336 $authenticated=1;
337 break;
338 case "334":
339 $response=base64_decode($responses[0]);
340 break;
341 default:
342 $this->error="Authentication error: ".$responses[0];
343 return(0);
345 break;
346 default:
347 $this->error="Could not process the SASL authentication step: ".$sasl->error;
348 return(0);
352 return(1);
355 /* Public methods */
357 Function Connect($domain="")
359 if(strcmp($this->state,"Disconnected"))
361 $this->error="connection is already established";
362 return(0);
364 $this->disconnected_error=0;
365 $this->error=$error="";
366 $this->esmtp_host="";
367 $this->esmtp_extensions=array();
368 $hosts=array();
369 if($this->direct_delivery)
371 if(strlen($domain)==0)
372 return(1);
373 $hosts=$weights=$mxhosts=array();
374 $getmxrr=$this->getmxrr;
375 if(function_exists($getmxrr)
376 && $getmxrr($domain,$hosts,$weights))
378 for($host=0;$host<count($hosts);$host++)
379 $mxhosts[$weights[$host]]=$hosts[$host];
380 KSort($mxhosts);
381 for(Reset($mxhosts),$host=0;$host<count($mxhosts);Next($mxhosts),$host++)
382 $hosts[$host]=$mxhosts[Key($mxhosts)];
384 else
386 if(strcmp(@gethostbyname($domain),$domain)!=0)
387 $hosts[]=$domain;
390 else
392 if(strlen($this->host_name))
393 $hosts[]=$this->host_name;
394 if(strlen($this->pop3_auth_host))
396 $user=$this->user;
397 if(strlen($user)==0)
399 $this->error="it was not specified the POP3 authentication user";
400 return(0);
402 $password=$this->password;
403 if(strlen($password)==0)
405 $this->error="it was not specified the POP3 authentication password";
406 return(0);
408 $domain=$this->pop3_auth_host;
409 $this->error=$this->ConnectToHost($domain, $this->pop3_auth_port, "Resolving POP3 authentication host \"".$domain."\"...");
410 if(strlen($this->error))
411 return(0);
412 if(strlen($response=$this->GetLine())==0)
413 return(0);
414 if(strcmp($this->Tokenize($response," "),"+OK"))
416 $this->error="POP3 authentication server greeting was not found";
417 return(0);
419 if(!$this->PutLine("USER ".$this->user)
420 || strlen($response=$this->GetLine())==0)
421 return(0);
422 if(strcmp($this->Tokenize($response," "),"+OK"))
424 $this->error="POP3 authentication user was not accepted: ".$this->Tokenize("\r\n");
425 return(0);
427 if(!$this->PutLine("PASS ".$password)
428 || strlen($response=$this->GetLine())==0)
429 return(0);
430 if(strcmp($this->Tokenize($response," "),"+OK"))
432 $this->error="POP3 authentication password was not accepted: ".$this->Tokenize("\r\n");
433 return(0);
435 fclose($this->connection);
436 $this->connection=0;
439 if(count($hosts)==0)
441 $this->error="could not determine the SMTP to connect";
442 return(0);
444 for($host=0, $error="not connected";strlen($error) && $host<count($hosts);$host++)
446 $domain=$hosts[$host];
447 $error=$this->ConnectToHost($domain, $this->host_port, "Resolving SMTP server domain \"$domain\"...");
449 if(strlen($error))
451 $this->error=$error;
452 return(0);
454 $timeout=($this->data_timeout ? $this->data_timeout : $this->timeout);
455 if($timeout
456 && function_exists("socket_set_timeout"))
457 socket_set_timeout($this->connection,$timeout,0);
458 if($this->debug)
459 $this->OutputDebug("Connected to SMTP server \"".$domain."\".");
460 if(!strcmp($localhost=$this->localhost,"")
461 && !strcmp($localhost=getenv("SERVER_NAME"),"")
462 && !strcmp($localhost=getenv("HOST"),""))
463 $localhost="localhost";
464 $success=0;
465 if($this->VerifyResultLines("220",$responses)>0)
467 $fallback=1;
468 if($this->esmtp
469 || strlen($this->user))
471 if($this->PutLine("EHLO $localhost"))
473 if(($success_code=$this->VerifyResultLines("250",$responses))>0)
475 $this->esmtp_host=$this->Tokenize($responses[0]," ");
476 for($response=1;$response<count($responses);$response++)
478 $extension=strtoupper($this->Tokenize($responses[$response]," "));
479 $this->esmtp_extensions[$extension]=$this->Tokenize("");
481 $success=1;
482 $fallback=0;
484 else
486 if($success_code==0)
488 $code=$this->Tokenize($this->error," -");
489 switch($code)
491 case "421":
492 $fallback=0;
493 break;
498 else
499 $fallback=0;
501 if($fallback)
503 if($this->PutLine("HELO $localhost")
504 && $this->VerifyResultLines("250",$responses)>0)
505 $success=1;
507 if($success
508 && strlen($this->user)
509 && strlen($this->pop3_auth_host)==0)
511 if(!IsSet($this->esmtp_extensions["AUTH"]))
513 $this->error="server does not require authentication";
514 $success=0;
516 else
518 if(strlen($this->authentication_mechanism))
519 $mechanisms=array($this->authentication_mechanism);
520 else
522 $mechanisms=array();
523 for($authentication=$this->Tokenize($this->esmtp_extensions["AUTH"]," ");strlen($authentication);$authentication=$this->Tokenize(" "))
524 $mechanisms[]=$authentication;
526 $credentials=array(
527 "user"=>$this->user,
528 "password"=>$this->password
530 if(strlen($this->realm))
531 $credentials["realm"]=$this->realm;
532 if(strlen($this->workstation))
533 $credentials["workstation"]=$this->workstation;
534 $success=$this->SASLAuthenticate($mechanisms,$credentials,$authenticated,$mechanism);
535 if(!$success
536 && !strcmp($mechanism,"PLAIN"))
539 * Author: Russell Robinson, 25 May 2003, http://www.tectite.com/
540 * Purpose: Try various AUTH PLAIN authentication methods.
542 $mechanisms=array("PLAIN");
543 $credentials=array(
544 "user"=>$this->user,
545 "password"=>$this->password
547 if(strlen($this->realm))
550 * According to: http://www.sendmail.org/~ca/email/authrealms.html#authpwcheck_method
551 * some sendmails won't accept the realm, so try again without it
553 $success=$this->SASLAuthenticate($mechanisms,$credentials,$authenticated,$mechanism);
555 if(!$success)
558 * It was seen an EXIM configuration like this:
559 * user^password^unused
561 $credentials["mode"]=SASL_PLAIN_EXIM_DOCUMENTATION_MODE;
562 $success=$this->SASLAuthenticate($mechanisms,$credentials,$authenticated,$mechanism);
564 if(!$success)
567 * ... though: http://exim.work.de/exim-html-3.20/doc/html/spec_36.html
568 * specifies: ^user^password
570 $credentials["mode"]=SASL_PLAIN_EXIM_MODE;
571 $success=$this->SASLAuthenticate($mechanisms,$credentials,$authenticated,$mechanism);
574 if($success
575 && strlen($mechanism)==0)
577 $this->error="it is not supported any of the authentication mechanisms required by the server";
578 $success=0;
583 if($success)
585 $this->state="Connected";
586 $this->connected_domain=$domain;
588 else
590 fclose($this->connection);
591 $this->connection=0;
593 return($success);
596 Function MailFrom($sender)
598 if($this->direct_delivery)
600 switch($this->state)
602 case "Disconnected":
603 $this->direct_sender=$sender;
604 return(1);
605 case "Connected":
606 $sender=$this->direct_sender;
607 break;
608 default:
609 $this->error="direct delivery connection is already established and sender is already set";
610 return(0);
613 else
615 if(strcmp($this->state,"Connected"))
617 $this->error="connection is not in the initial state";
618 return(0);
621 $this->error="";
622 if(!$this->PutLine("MAIL FROM:<$sender>"))
623 return(0);
624 if(!IsSet($this->esmtp_extensions["PIPELINING"])
625 && $this->VerifyResultLines("250",$responses)<=0)
626 return(0);
627 $this->state="SenderSet";
628 if(IsSet($this->esmtp_extensions["PIPELINING"]))
629 $this->pending_sender=1;
630 $this->pending_recipients=0;
631 return(1);
634 Function SetRecipient($recipient)
636 if($this->direct_delivery)
638 if(GetType($at=strrpos($recipient,"@"))!="integer")
639 return("it was not specified a valid direct recipient");
640 $domain=substr($recipient,$at+1);
641 switch($this->state)
643 case "Disconnected":
644 if(!$this->Connect($domain))
645 return(0);
646 if(!$this->MailFrom(""))
648 $error=$this->error;
649 $this->Disconnect();
650 $this->error=$error;
651 return(0);
653 break;
654 case "SenderSet":
655 case "RecipientSet":
656 if(strcmp($this->connected_domain,$domain))
658 $this->error="it is not possible to deliver directly to recipients of different domains";
659 return(0);
661 break;
662 default:
663 $this->error="connection is already established and the recipient is already set";
664 return(0);
667 else
669 switch($this->state)
671 case "SenderSet":
672 case "RecipientSet":
673 break;
674 default:
675 $this->error="connection is not in the recipient setting state";
676 return(0);
679 $this->error="";
680 if(!$this->PutLine("RCPT TO:<$recipient>"))
681 return(0);
682 if(IsSet($this->esmtp_extensions["PIPELINING"]))
684 $this->pending_recipients++;
685 if($this->pending_recipients>=$this->maximum_piped_recipients)
687 if(!$this->FlushRecipients())
688 return(0);
691 else
693 if($this->VerifyResultLines(array("250","251"),$responses)<=0)
694 return(0);
696 $this->state="RecipientSet";
697 return(1);
700 Function StartData()
702 if(strcmp($this->state,"RecipientSet"))
704 $this->error="connection is not in the start sending data state";
705 return(0);
707 $this->error="";
708 if(!$this->PutLine("DATA"))
709 return(0);
710 if($this->pending_recipients)
712 if(!$this->FlushRecipients())
713 return(0);
715 if($this->VerifyResultLines("354",$responses)<=0)
716 return(0);
717 $this->state="SendingData";
718 return(1);
721 Function PrepareData(&$data,&$output,$preg=1)
723 if($preg
724 && function_exists("preg_replace"))
725 $output=preg_replace(array("/\n\n|\r\r/","/(^|[^\r])\n/","/\r([^\n]|\$)/D","/(^|\n)\\./"),array("\r\n\r\n","\\1\r\n","\r\n\\1","\\1.."),$data);
726 else
727 $output=ereg_replace("(^|\n)\\.","\\1..",ereg_replace("\r([^\n]|\$)","\r\n\\1",ereg_replace("(^|[^\r])\n","\\1\r\n",ereg_replace("\n\n|\r\r","\r\n\r\n",$data))));
730 Function SendData($data)
732 if(strcmp($this->state,"SendingData"))
734 $this->error="connection is not in the sending data state";
735 return(0);
737 $this->error="";
738 return($this->PutData($data));
741 Function EndSendingData()
743 if(strcmp($this->state,"SendingData"))
745 $this->error="connection is not in the sending data state";
746 return(0);
748 $this->error="";
749 if(!$this->PutLine("\r\n.")
750 || $this->VerifyResultLines("250",$responses)<=0)
751 return(0);
752 $this->state="Connected";
753 return(1);
756 Function ResetConnection()
758 switch($this->state)
760 case "Connected":
761 return(1);
762 case "SendingData":
763 $this->error="can not reset the connection while sending data";
764 return(0);
765 case "Disconnected":
766 $this->error="can not reset the connection before it is established";
767 return(0);
769 $this->error="";
770 if(!$this->PutLine("RSET")
771 || $this->VerifyResultLines("250",$responses)<=0)
772 return(0);
773 $this->state="Connected";
774 return(1);
777 Function Disconnect($quit=1)
779 if(!strcmp($this->state,"Disconnected"))
781 $this->error="it was not previously established a SMTP connection";
782 return(0);
784 $this->error="";
785 if(!strcmp($this->state,"Connected")
786 && $quit
787 && (!$this->PutLine("QUIT")
788 || ($this->VerifyResultLines("221",$responses)<=0
789 && !$this->disconnected_error)))
790 return(0);
791 if($this->disconnected_error)
792 $this->disconnected_error=0;
793 else
794 fclose($this->connection);
795 $this->connection=0;
796 $this->state="Disconnected";
797 if($this->debug)
798 $this->OutputDebug("Disconnected.");
799 return(1);
802 Function SendMessage($sender,$recipients,$headers,$body)
804 if(($success=$this->Connect()))
806 if(($success=$this->MailFrom($sender)))
808 for($recipient=0;$recipient<count($recipients);$recipient++)
810 if(!($success=$this->SetRecipient($recipients[$recipient])))
811 break;
813 if($success
814 && ($success=$this->StartData()))
816 for($header_data="",$header=0;$header<count($headers);$header++)
817 $header_data.=$headers[$header]."\r\n";
818 if(($success=$this->SendData($header_data."\r\n")))
820 $this->PrepareData($body,$body_data);
821 $success=$this->SendData($body_data);
823 if($success)
824 $success=$this->EndSendingData();
827 $error=$this->error;
828 $disconnect_success=$this->Disconnect($success);
829 if($success)
830 $success=$disconnect_success;
831 else
832 $this->error=$error;
834 return($success);