Iterate only unseen messages in IMAP with Laminas Mail - php

We can count unseen messages with:
$unreadMessages = $mail->countMessages([Storage::FLAG_UNSEEN]);
How to iterate only those unseen messages?
When I iterate all messages (as shown in documentation), it's painfully slow.
foreach ($mail as $messageNum => $message) {
if ($message->hasFlag(Storage::FLAG_SEEN) && !$message->hasFlag(Storage::FLAG_UNSEEN) && !$message->hasFlag(Storage::FLAG_RECENT)) {
// echo PHP_EOL . PHP_EOL . "Skipping seen/not-recent e-mail from " . $message->from . PHP_EOL;
continue;
}
}
Thanks.

When you look into how countMessages() function is done, you find out that protocol search function can be used. But protocol is private. So you have to extend the class:
class MyImap extends Laminas\Mail\Storage\Imap
{
public function getProtocol()
{
return $this->protocol;
}
}
$mail = new MyImap(...);
Then you can use this for fast iterating only unseen messages:
$message_nums = $mail->getProtocol()->search(['UNSEEN']);
foreach ($message_nums as $messageNum) {
$message = $mail->getMessage($messageNum);
}

Related

What would cause PHPMailer's $mail->send() to return false?

Looking at the following code, when would we ever end up in the else block?
<?php
try {
if ($mail->send()) {
echo 'success';
} else {
echo 'failed to send';
echo $mail->ErrorInfo;
}
} catch (Exception $e) {
echo 'Exception!';
echo $mail->ErrorInfo;
}
?>
From my testing, any issue with the configuration of the email settings (e.g., bad host or port, trying to send from foo#example.com), results in an exception.
So, I'm wondering if there is any reason to test $mail->send(). Or can we just assume that if no exception occurred, $mail->send() will return true?
From the source:
/**
* Create a message and send it.
* Uses the sending method specified by $Mailer.
*
* #throws Exception
*
* #return bool false on error - See the ErrorInfo property for details of the error
*/
public function send()
{
try {
if (!$this->preSend()) {
return false;
}
return $this->postSend();
} catch (Exception $exc) {
$this->mailHeader = '';
$this->setError($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
}
and the preSend() method:
/**
* Prepare a message for sending.
*
* #throws Exception
*
* #return bool
*/
public function preSend()
{
if ('smtp' == $this->Mailer or
('mail' == $this->Mailer and stripos(PHP_OS, 'WIN') === 0)
) {
//SMTP mandates RFC-compliant line endings
//and it's also used with mail() on Windows
static::setLE("\r\n");
} else {
//Maintain backward compatibility with legacy Linux command line mailers
static::setLE(PHP_EOL);
}
//Check for buggy PHP versions that add a header with an incorrect line break
if (ini_get('mail.add_x_header') == 1
and 'mail' == $this->Mailer
and stripos(PHP_OS, 'WIN') === 0
and ((version_compare(PHP_VERSION, '7.0.0', '>=')
and version_compare(PHP_VERSION, '7.0.17', '<'))
or (version_compare(PHP_VERSION, '7.1.0', '>=')
and version_compare(PHP_VERSION, '7.1.3', '<')))
) {
trigger_error(
'Your version of PHP is affected by a bug that may result in corrupted messages.' .
' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
E_USER_WARNING
);
}
try {
$this->error_count = 0; // Reset errors
$this->mailHeader = '';
// Dequeue recipient and Reply-To addresses with IDN
foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
$params[1] = $this->punyencodeAddress($params[1]);
call_user_func_array([$this, 'addAnAddress'], $params);
}
if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
}
// Validate From, Sender, and ConfirmReadingTo addresses
foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
$this->$address_kind = trim($this->$address_kind);
if (empty($this->$address_kind)) {
continue;
}
$this->$address_kind = $this->punyencodeAddress($this->$address_kind);
if (!static::validateAddress($this->$address_kind)) {
$error_message = sprintf('%s (%s): %s',
$this->lang('invalid_address'),
$address_kind,
$this->$address_kind);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
}
// Set whether the message is multipart/alternative
if ($this->alternativeExists()) {
$this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
}
$this->setMessageType();
// Refuse to send an empty message unless we are specifically allowing it
if (!$this->AllowEmpty and empty($this->Body)) {
throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
}
//Trim subject consistently
$this->Subject = trim($this->Subject);
// Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
$this->MIMEHeader = '';
$this->MIMEBody = $this->createBody();
// createBody may have added some headers, so retain them
$tempheaders = $this->MIMEHeader;
$this->MIMEHeader = $this->createHeader();
$this->MIMEHeader .= $tempheaders;
// To capture the complete message when using mail(), create
// an extra header list which createHeader() doesn't fold in
if ('mail' == $this->Mailer) {
if (count($this->to) > 0) {
$this->mailHeader .= $this->addrAppend('To', $this->to);
} else {
$this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
}
$this->mailHeader .= $this->headerLine(
'Subject',
$this->encodeHeader($this->secureHeader($this->Subject))
);
}
// Sign with DKIM if enabled
if (!empty($this->DKIM_domain)
and !empty($this->DKIM_selector)
and (!empty($this->DKIM_private_string)
or (!empty($this->DKIM_private)
and static::isPermittedPath($this->DKIM_private)
and file_exists($this->DKIM_private)
)
)
) {
$header_dkim = $this->DKIM_Add(
$this->MIMEHeader . $this->mailHeader,
$this->encodeHeader($this->secureHeader($this->Subject)),
$this->MIMEBody
);
$this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . static::$LE .
static::normalizeBreaks($header_dkim) . static::$LE;
}
return true;
} catch (Exception $exc) {
$this->setError($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
}
So in short: It returns false, if an exception occurs.
It is always good practice to test $mail->send().
To answer your questions:
You are correct. If there are no exceptions, then it will always return true.
Some Extra Information:
If you want to test emails before sending it to recipients you can do the following:
You can set sendmail_path in php ini to whatever you want. For example, it can be tee -a mail.log so that everything gets logged to a file.

Trim email address from header of PHP Mailer

I am using PHPMailer to send email to my clients. But I am not satisfied with the output of the
I want to remove <michael#gmail.com> at the end of my name but not sure how to do it.
My current script:
$mail->SetFrom("michael#gmail.com",'Michael Chu');
$mail->XMailer = 'Microsoft Mailer';
$mail->AddAddress($email);
$mail->Subject = "TEST Email";
$mail->Body = "<p>TEST Email<p>";
The relevant parts in the PHPMailer code is in the Pre Send routine, which assembles the mail (and which is obviously called internally always before send):
public function preSend() {
...
try {
$this->error_count = 0; // Reset errors
$this->mailHeader = '';
...
// Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
$this->MIMEHeader = '';
$this->MIMEBody = $this->createBody();
// createBody may have added some headers, so retain them
$tempheaders = $this->MIMEHeader;
$this->MIMEHeader = $this->createHeader();
$this->MIMEHeader .= $tempheaders;
...
return true;
This will be called always. Now: when we look at the createHeader-function we see this:
public function createHeader()
{
$result = '';
...
$result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
...
return $result;
}
So: Create Header always adds the From Address part, but it relies on addrAppend to format it (passing 'From' and an array containing one address-array [email, name])
public function addrAppend($type, $addr)
{
$addresses = [];
foreach ($addr as $address) {
$addresses[] = $this->addrFormat($address);
}
return $type . ': ' . implode(', ', $addresses) . static::$LE;
}
The address-array is passed on:
public function addrFormat($addr)
{
if (empty($addr[1])) { // No name provided
return $this->secureHeader($addr[0]);
}
return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
' <' .
$this->secureHeader($addr[0])
. '>';
}
and formatted with the email... Nothing you can do about it.
So with phpmailer you can't do it. But you can write your own subclass.
Probably something along those lines
<?php
//Import PHPMailer classes into the global namespace
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require '../vendor/autoload.php';
/**
* Use PHPMailer as a base class and extend it
*/
class myPHPMailer extends PHPMailer
{
public function addrFormat($addr)
{
if (empty($addr[1])) { // No name provided
return $this->secureHeader($addr[0]);
}
else {
return $this->secureHeader($addr[1]);
}
}
}

PHP Displaying unread mail count

I am using php imap class.
In my box I have a lot of mail, but with this script I would retrieve only the unreaded mail.
How can I do it?
if ($mbox=imap_open( "{" . $mailserver . ":" . $port . "}INBOX", $user, $pass ))
{
echo "Connected\n";
} else { exit ("Can't connect: " . imap_last_error() ."\n"); echo "FAIL!\n"; };
if ($hdr = imap_check($mbox)) {
$msgCount = $hdr->Nmsgs;
echo "Ci sono ".$msgCount." mail";
} else {
echo "Failed to get mail";
}
If I do
$overview=imap_fetch_overview($mbox,"1:$msgCount",0);
the script load to an infinity time.
The imap_search UNSEEN solution is not good because pop3 don't use this flag.
So how can I do??????
Thanks a lot.
There is two way you can follow:
1. Looping through the messages
$count = imap_num_msg($connection);
for($msgno = 1; $msgno <= $count; $msgno++) {
$headers = imap_headerinfo($connection, $msgno);
if($headers->Unseen == 'U') {
... do something ...
}
}
2. Using imap_search
There's a flag called UNSEEN which you can use to search for the unread emails. You would call the imap_search function with the UNSEEN flag like so:
$result = imap_search($connection, 'UNSEEN');
If you need to combine this with more search flags, for example searching for messages from me#example.com, you could do this:
$result = imap_search($connection, 'UNSEEN FROM "me#example.com"');
For a complete list of the available flags, refer to the criteria section of the imap_search manual page on the PHP website (www.php.net/imap_search)
Source: http://www.electrictoolbox.com/php-imap-unread-messages/
This was a tough one on Google: php imap unread
The first result:
http://www.electrictoolbox.com/php-imap-unread-messages/
There's a flag called UNSEEN which you can use to search for the unread emails. You would call the imap_search function with the UNSEEN flag like so:
view sourceprint?
$result = imap_search($connection, 'UNSEEN');
If you need to combine this with more search flags, for example searching for messages from me#example.com, you could do this:
view sourceprint?
$result = imap_search($connection, 'UNSEEN FROM "me#example.com"');
For a complete list of the available flags, refer to the criteria section of the imap_search manual page on the PHP website (www.php.net/imap_search)
Edit I had read this originally as IMAP. #fail.
Google: php pop3 unread
2nd link:
function CountUnreadMails($host, $login, $passwd) {
$mbox = imap_open("{{$host}/pop3:110}", $login, $passwd);
$count = 0;
if (!$mbox) {
echo "Error";
} else {
$headers = imap_headers($mbox);
foreach ($headers as $mail) {
$flags = substr($mail, 0, 4);
$isunr = (strpos($flags, "U") !== false);
if ($isunr)
$count++;
}
}
imap_close($mbox);
return $count;
}

How to remove an email message in Maildir from PHP?

I'm going crazy with a little problem with Maildir and PHP.
I need to check the APACHE_RUN_USER's Maildir and parse delivery-status messages.
The problem removing message after reading; i noticed that Zend_Mail_Storage_Maildir->removeMessage() is still a stub.
try {
$mailbox = new Zend_Mail_Storage_Maildir( array('dirname' => '/home/' . $_ENV['APACHE_RUN_USER'] . '/Maildir/') );
foreach ($mailbox as $id => $message) {
// seen flag
if ($message->hasFlag(Zend_Mail_Storage::FLAG_SEEN)) { continue; }
//get the unique id
$uniqueid = $mailbox->getUniqueId($id);
//obtain message headers
$headers = $message->getHeaders();
//check if the original message was sent from this app and is a delivery-status
$result = strpos($message, $id_header);
if($result === false) { echo '1 mail skipped: ' . $uniqueid . '. <br />'; continue; }
$result = strpos($headers['content-type'], 'delivery-status');
//if no skip to the next mail
if($result === false) { echo '1 mail skipped: ' . $uniqueid . '. <br />'; continue; }
// if everything it's ok process it.
// clear results
$data = array();
// foreach line of message
foreach( preg_split('/(\r?\n)/', $message) as $line ){
//clear results
$matches = array();
//perform matches on textlines
if( preg_match('/^(.+)\:\s{0,1}(.+)$/', $line, $matches) ) {
//grab intrested headers
foreach( array('Action', 'Status', 'Remote-MTA', 'Diagnostic-Code', $id_header) as $header) {
if($matches[1] == $header) $data[$header] = $matches[2];
}
}
}
// *** I NEED TO DROP THE MESSAGE HERE ***
// not working code ***
$currentmessageid = $mailbox->getNumberByUniqueId($uniqueid);
$mailbox->removeMessage($currentmessageid);
// *** I NEED TO DROP THE MESSAGE HERE ***
// print out results
echo '<pre class="email">';
print_r( $data );
echo '</pre>';
}
} catch (Exception $e) {
echo $e;
}
How can I remove it by hand? Some workarounds?
Thanks.
Sorry , its not implemented yet !
check out issue tracker http://framework.zend.com/issues/browse/ZF-9574
its open issue till today but some comment might be helpful :
In order to delete an email from a
maildir or mbox storage one must use:
Zend_Mail_Storage_Writable_Maildir or
Zend_Mail_Storage_Writable_Mbox
There are historical reasons for this
and they should be addressed and
standardised. For now the above
classes must be used or an exception
will be thrown with a message that is
a bit misleading.
Please refer to:
http://framework.zend.com/issues/browse/ZF-9574
for more details.
In order of tawfekov answer I solved as follow:
Opening mailbox:
$mailbox = new Zend_Mail_Storage_Writable_Maildir( array('dirname' => '/home/' . $_ENV['APACHE_RUN_USER'] . '/Maildir/') );
Processing mail code:
foreach ($mailbox as $id => $message) {
$uniqueid = $mailbox->getUniqueId($id);
/* ... mail processing code ... */
// mark as read
$currentmessageid = $mailbox->getNumberByUniqueId($uniqueid);
$mailbox->setFlags($currentmessageid, array(Zend_Mail_Storage::FLAG_SEEN));
// or uncomment to delete it
//$mailbox->removeMessage($currentmessageid);
}

Fetching mail from a POP3 server using php

I am trying to fetch a mail from POP3 (I am using POP3 mail server and I am trying to fetch the mail content and store into a database table for my project.), but I can't find any PHP script for that, all are only for IMAP.
Do you know how to fetch mail from a POP3 server?
Thanks.
Somewhat surprisingly, PHP's imap library can be also used for working with POP3 mailboxes. Most of the advanced IMAP features won't work, of course (e.g. folders or fetching message parts), but the basic POP3 functionality is implemented.
The main difference is the option string that you're passing to imap_open - to quote that page:
// To connect to a POP3 server on port 110 on the local server, use:
$mbox = imap_open ("{localhost:110/pop3}INBOX", "user_id", "password");
Other than that, it's fair sailing - you won't need more than imap_open, imap_num_msg, imap_body, imap_delete and imap_close for basic POP3 access.
PHP's IMAP functions can deal with both IMAP and POP3 boxes.
These functions enable you to operate with the IMAP protocol, as well as the NNTP, POP3 and local mailbox access methods.
Be warned, however, that some IMAP functions will not work correctly with the POP protocol.
there is a User Contributed Note that provides an interesting snippet. You may want to take a look at it. I can't say anything about its quality but from the surface, it looks okay.
Below, the Contributed Note:
For all the people coming here praying for:
1) a dead-easy way to read MIME attachments, or
2) a dead-easy way to access POP3 folders
Look no further.
function pop3_login($host,$port,$user,$pass,$folder="INBOX",$ssl=false)
{
$ssl=($ssl==false)?"/novalidate-cert":"";
return (imap_open("{"."$host:$port/pop3$ssl"."}$folder",$user,$pass));
}
function pop3_stat($connection)
{
$check = imap_mailboxmsginfo($connection);
return ((array)$check);
}
function pop3_list($connection,$message="")
{
if ($message)
{
$range=$message;
} else {
$MC = imap_check($connection);
$range = "1:".$MC->Nmsgs;
}
$response = imap_fetch_overview($connection,$range);
foreach ($response as $msg) $result[$msg->msgno]=(array)$msg;
return $result;
}
function pop3_retr($connection,$message)
{
return(imap_fetchheader($connection,$message,FT_PREFETCHTEXT));
}
function pop3_dele($connection,$message)
{
return(imap_delete($connection,$message));
}
function mail_parse_headers($headers)
{
$headers=preg_replace('/\r\n\s+/m', '',$headers);
preg_match_all('/([^: ]+): (.+?(?:\r\n\s(?:.+?))*)?\r\n/m', $headers, $matches);
foreach ($matches[1] as $key =>$value) $result[$value]=$matches[2][$key];
return($result);
}
function mail_mime_to_array($imap,$mid,$parse_headers=false)
{
$mail = imap_fetchstructure($imap,$mid);
$mail = mail_get_parts($imap,$mid,$mail,0);
if ($parse_headers) $mail[0]["parsed"]=mail_parse_headers($mail[0]["data"]);
return($mail);
}
function mail_get_parts($imap,$mid,$part,$prefix)
{
$attachments=array();
$attachments[$prefix]=mail_decode_part($imap,$mid,$part,$prefix);
if (isset($part->parts)) // multipart
{
$prefix = ($prefix == "0")?"":"$prefix.";
foreach ($part->parts as $number=>$subpart)
$attachments=array_merge($attachments, mail_get_parts($imap,$mid,$subpart,$prefix.($number+1)));
}
return $attachments;
}
function mail_decode_part($connection,$message_number,$part,$prefix)
{
$attachment = array();
if($part->ifdparameters) {
foreach($part->dparameters as $object) {
$attachment[strtolower($object->attribute)]=$object->value;
if(strtolower($object->attribute) == 'filename') {
$attachment['is_attachment'] = true;
$attachment['filename'] = $object->value;
}
}
}
if($part->ifparameters) {
foreach($part->parameters as $object) {
$attachment[strtolower($object->attribute)]=$object->value;
if(strtolower($object->attribute) == 'name') {
$attachment['is_attachment'] = true;
$attachment['name'] = $object->value;
}
}
}
$attachment['data'] = imap_fetchbody($connection, $message_number, $prefix);
if($part->encoding == 3) { // 3 = BASE64
$attachment['data'] = base64_decode($attachment['data']);
}
elseif($part->encoding == 4) { // 4 = QUOTED-PRINTABLE
$attachment['data'] = quoted_printable_decode($attachment['data']);
}
return($attachment);
}
you can use pop3 e-mail client class which can Access to e-mail mailboxes using the POP3 protocol.
You will get each e-mail body part and can store it in database, even you can retrieve attached files without deleting the original mail in the inbox.
For more go to http://www.phpclasses.org/package/2-PHP-Access-to-e-mail-mailboxes-using-the-POP3-protocol.html
IF you have PHP build with IMAP support, it would be easy, see IMAP documentation (especially comments at this page) at http://php.net/manual/en/book.imap.php
UPDATE: to clarify my answer - as you see in the comments and function reference, PHP imap_* functions can be used also for pop3.
You can open a socket connection and send POP3 commands directly to your server to retrieve emails.
The code below opens a connection to the server, authenticates, requests a count of available messages, downloads them one by one, then deletes them from the server. During the interaction with the server, if an unexpected response is received, the connection is closed.
You should be able to alter the code below to get what you need.
Create a config.ini file and populate it like this:
pop3_host=your.pop3.host
pop3_user=youremailusername
pop3_pass=youremailpassword
Obviously substitute the values with your actual host, username and password. Then change the parse_ini_file parameter to point to your config.ini file.
<?php
$config = parse_ini_file('/path/to/config.ini');
$stream = fsockopen('ssl://' . $config['pop3_host'], 995, $error_code, $error_message);
if (!$stream) {
die('fsockopen ' . $error_code . ' ' . $error_message);
}
$s = fgets($stream);
if (substr($s, 0, 3) !== '+OK') {
quit('OPEN');
}
chat('USER ' . $config['pop3_user']);
chat('PASS ' . $config['pop3_pass']);
$s = chat('STAT');
$stat = explode(' ', $s);
if (count($stat) !== 3) {
quit('STAT+');
}
$n = (integer)$stat[1];
for ($i = 1; $i <= $n; $i++) {
chat('RETR ' . $i);
$file = fopen($i . '.msg', 'wb');
if (!$file) {
quit('FILE ' . $i);
}
while (true) {
$s = fgets($stream);
if ($s === '.' . "\r\n") {
fclose($file);
break;
}
fputs($file, $s);
}
chat('DELE ' . $i);
}
chat('QUIT');
fclose($stream);
function chat(string $command): string {
global $stream;
fputs($stream, $command . "\r\n");
$s = fgets($stream);
if (substr($s, 0, 3) !== '+OK') {
quit(explode(' ', $command)[0]);
}
return $s;
}
function quit(string $message): void {
global $stream;
fputs($stream, 'QUIT' . "\r\n");
$s = fgets($stream);
fclose($stream);
die($message);
}

Categories