За последние 24 часа нас посетили 24306 программистов и 1711 роботов. Сейчас ищет 1791 программист ...

Отправка почты

Тема в разделе "Вопросы от блондинок", создана пользователем Hight, 26 апр 2011.

  1. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    Товарищи, господа, пацаны и т. д. Есть вопрос.

    Если мы отправляем почту функцией mail(), то всё происходит быстро (как и надо), но проконтролировать отправку письма мы не можем. Или можем?! Можем, но геморройно. Так же функция mail() завязана на локальный sendmail и многое зависит от его настройки.

    Если мы отправляем почту с помощью сокетов через какой-нибудь SMTP-сервер, то всё просто великолепно, мы читаем ответы сервера, отвечаем ему и можем контролировать отправку писем. Но, если у нас начинаются проблемы со связью, или удалённый SMTP-сервер не отвечает по каким-либо причинам, то скрипт начинает подвисать, правильно?! Я ещё не проверял, но подвисать должен, по идеи. А подвисать он будет на попытке соединения с SMTP-сервером. И подвисать он должен круто.

    А в чём проблема?!

    А есть скрипт, который должен быстро отрабатывать и ещё оправлять почту. Если в скрипте мы отправляем почту функцией mail(), то проблем нет. Письмо улетает в очередь локального сендмейла и уже он разруливает его отправку. Но если мы юзаем сокеты, то появляется проблема. Мы не можем даже гарантировать, что скрипт отработает. Может просто отвалиться по таймауту. Устанет ждать ответа от SMTP-сервера и отвалится. И будет жопа.

    Вот и вопрос. Что делать. Я хочу сокеты.

    Думаю надо всю корреспонденцию класть в базу - в очередь на отправку. А рассылать каким-нибудь скриптом-демоном, или по крону каждую минуту дёргать скрипт.

    Но есть ещё одна идея. Пишем скриптик mail.php и кладём его куда-нибудь, да в тот же корень сайта. Во время работы скрипта, когда надо отправить почту, мы кладём письмо в базу и дёргаем наш скрипт.

    file_get_contents('./mail.php?pid=PostID');

    Скрипт получает ИД письма, идёт за ним в базу и потихонечку отправляет. Но, как быть если письмо не получится отправить, да, придётся делать так, чтобы скрипт ещё и не отправленные пытался отправить снова.

    Блин, устал думать... Кто знает решение?! Колитесь. =)
     
  2. Апельсин

    Апельсин Активный пользователь

    С нами с:
    20 мар 2010
    Сообщения:
    3.645
    Симпатии:
    2
    шозабред? так ты не дергнешь файл. Тупо его код получишь.
    апд. Да и то не получишь изза ?pid=PostID
     
  3. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    Да блин, вот так:
    PHP:
    1. file_get_contents('http://site/mail.php?pid=PostID');
    По делу давай.
     
  4. Апельсин

    Апельсин Активный пользователь

    С нами с:
    20 мар 2010
    Сообщения:
    3.645
    Симпатии:
    2
    Hight
    Да не дергнет file_get_contents файл. Где ты такое вычитал? Это же не wget в линухе. file_get_contents тупо прочитает файл без его запуска.
     
  5. Padaboo

    Padaboo Старожил
    Команда форума Модератор

    С нами с:
    26 окт 2009
    Сообщения:
    5.242
    Симпатии:
    1
    письма сразу должны отправляться или это какая то рассылка? вешай на сокет таймаут, выкидывай эксепшен и отлавливай не отправленные письма в базу, потом проходись чем нибудь по базе на предмет таких писем
    если без базы то можно написать демона что бы он свой лист с такими письмами обрабатывал сам сразу из памяти
     
  6. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    =)
     
  7. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    Я тоже думаю о демоне. Только в БД письма класть, для истории.
     
  8. Psih

    Psih Активный пользователь
    Команда форума Модератор

    С нами с:
    28 дек 2006
    Сообщения:
    2.678
    Симпатии:
    6
    Адрес:
    Рига, Латвия
    Hight
    Если хочется отсылать не задерживая скрипт, то делай очередь в базе и сделай демон, который будет делать рассылку - благо на PHP это плёвое дело, а с PHP 5.3 с менеджментом памяти стало ажурно - главное не забывать делать unset почаще. К тому же у сокетов есть настройки таймаутов - юзай :)

    В последствии можно сделать многозадачность и прочие плюшки. Я вот вообще для линяги с pnctl_fork замутил демона, который делает целую кучу обработок - работает как часы - главное логгинг и аккуратная обработка возможных проблемных мест. Исключения рулят безбожно.

    Апельсин
    file_get_contents, запрашивающий скрипт через http тработает так же, как бы отработало с браузером - для веб сервера и PHP никакой разницы.
     
  9. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    Psih
    Ты прав. Без демона тут не обойтись. Есть примерчик посмотреть? А то я не в теме новых веяний.
     
  10. Апельсин

    Апельсин Активный пользователь

    С нами с:
    20 мар 2010
    Сообщения:
    3.645
    Симпатии:
    2
    Psih
    Эт понял. Иначе сначала подумал - если открывать с локали файл тупо для получения содержимого.
     
  11. MiksIr

    MiksIr Активный пользователь

    С нами с:
    29 ноя 2006
    Сообщения:
    2.339
    Симпатии:
    44
    Посмотри что-то в сторону Mail_Queue из pear
     
  12. Psih

    Psih Активный пользователь
    Команда форума Модератор

    С нами с:
    28 дек 2006
    Сообщения:
    2.678
    Симпатии:
    6
    Адрес:
    Рига, Латвия
    Hight
    Ну так как у меня Yii, то я использую компоненту CConsoleCommand и на базе неё просто довернул демонизацию - но по сути тоже самое можно сделать и без Yii - в данном случае тут удобство запуска и функционал AR и прочие плюшки - костяк же сам по себе вполне жить может:
    PHP:
    1. <?php
    2. /**
    3.  * Created by PhpStorm.
    4.  * User: psihius
    5.  * Date: 2010.5.10
    6.  * Time: 15:59:29
    7.  * To change this template use File | Settings | File Templates.
    8.  */
    9.  
    10. /**
    11.  * @class LaunchCommand
    12.  * @extends CConsoleCommand
    13.  */
    14. class XDaemonCommand extends CConsoleCommand {
    15.  
    16.     private $args;
    17.     private $_pid_file;
    18.     private $_pid;
    19.     private $_run;
    20.     private $_daemon_name;
    21.     private $_logs;
    22.     private $processors;
    23.  
    24.     /**
    25.      * Initialize the daemon
    26.      * @param  $args
    27.      * @return void
    28.      */
    29.     public function run($args)
    30.     {
    31.         $this->args = $args;
    32.         $this->_daemon_name = str_replace('Command', '', __CLASS__);
    33.         $this->initDirs();
    34.         $this->log('Starting daemon...', true);
    35.         $this->_run = true;
    36.  
    37.         $this->processors = array();
    38.         if (!$this->isRunning()) {
    39.             // Start the daemon
    40.             $this->demonize();
    41.         } else {
    42.             $this->log('Daemon is running, exiting...', true);
    43.         }
    44.     }
    45.  
    46.     final private function demonize()
    47.     {
    48.         $this->log('Forking...', true);
    49.         $this->_pid = pcntl_fork();
    50.         if ($this->_pid === -1) {
    51.             $this->log('Can\'t fork!', true);
    52.             exit(0);
    53.         } elseif ($this->_pid > 0) {
    54.             $this->log('Sucessfully spawned with ID '.$this->_pid, true);
    55.             Yii::app()->end();
    56.             exit(0);
    57.         }
    58.         if (posix_setsid() < 0) {
    59.             $this->log('Failed to make child as master process...');
    60.             Yii::app()->end();
    61.             exit(-1);
    62.         }
    63.  
    64.         Yii::getLogger()->autoFlush = 1;
    65.         if (function_exists('setproctitle')) {
    66.             setproctitle($this->_daemon_name);
    67.         }
    68.  
    69.         // Save PID
    70.         file_put_contents($this->_pid_file, getmypid());
    71.         // Register signal handler
    72.         pcntl_signal(SIGTERM, array($this, 'signalHandler'));
    73.         $last_shedulue = time() - (24 * 3600);
    74.         // Go into daemon loop
    75.         while ($this->_run) {
    76.             try {
    77.                 // Make criteria to fetch daemon process queue
    78.                 $criteria = new CDbCriteria(array(
    79.                     'condition' => 'start_on <= NOW()',
    80.                     'order' => 'start_on ASC',
    81.                 ));
    82.                 /** @var $queue DaemonQueue[]
    83.                  * Fetch transactions
    84.                  */
    85.                 $queue = DaemonQueue::model()->with('transaction')->findAll($criteria);
    86.                 if (is_array($queue)) {
    87.                     /** @var $item DaemonQueue */
    88.                     foreach ($queue as $item) {
    89.                         $class = ucfirst($item->keyword).'Processor';
    90.                         try {
    91.                             /** @var $processor Processor */
    92.                             $processor = new $class();
    93.                             $processor->run($item->transaction, $item, $this);
    94.                             $processor->cleanup();
    95.                             unset($processor);
    96.                         } catch (ProcessorException $e) {
    97.                             // Catch and log non-critical, application-logic specific errors
    98.                             if ($item->transaction instanceof Transactions) {
    99.                                 Yii::log('Transaction #'.$item->transaction->id.PHP_EOL.$e->getMessage().$e->getDetails(), 'warning');
    100.                                 $item->transaction->status = Transactions::SUPPORT;
    101.                                 $item->transaction->save();
    102.                                 Operations::addEntry($item->transaction, $e->getMessage());
    103.                                 if (!$item->delayed()) {
    104.                                     $item->delete();
    105.                                 }
    106.                             } else {
    107.                                 Yii::log($e->getMessage().$e->getDetails(), 'warning');
    108.                             }
    109.                         }   catch (CException $e) {
    110.                             // This is some serious stuff, include any info we can get
    111.                             if ($item->transaction instanceof Transactions) {
    112.                                 Yii::log('Transaction #'.$item->transaction->id.PHP_EOL.$e->getMessage().PHP_EOL.$e->getTraceAsString(), 'error');
    113.                                 $item->transaction->status = Transactions::SUPPORT;
    114.                                 $item->transaction->save();
    115.                                 Operations::addEntry($item->transaction, $e->getMessage());
    116.                                 if (!$item->delayed()) {
    117.                                     $item->delete();
    118.                                 }
    119.                             } else {
    120.                                 Yii::log($e->getMessage().PHP_EOL.$e->getTraceAsString(), 'error');
    121.                             }
    122.                         }
    123.                         unset($item);
    124.                         if (!$this->_run) {
    125.                             break;
    126.                         }
    127.                     }
    128.                 }
    129.                 $time = time();
    130.                 if ($time - $last_shedulue > 300) {
    131.                     $last_shedulue = $time;
    132.                     $processor = new StaleTransactionProcessor();
    133.                     $processor->run();
    134.                     unset($processor);
    135.                 }
    136.                 unset($criteria, $queue);
    137.                 // Sleep for 0.5 second, so CPU doesn't get loaded 100%
    138.                 if ($this->_run) {
    139.                     usleep(500000);
    140.                 }
    141.             } catch (Exception $e) {
    142.                 $this->_run = false;
    143.                 $this->log($e->getMessage().PHP_EOL.$e->getTraceAsString());
    144.             }
    145.         }
    146.         $this->log('Shutdown complete, cleaning up...');
    147.         foreach ($this->processors as $k => $v) {
    148.             unset($v);
    149.             $this->processors[$k] = null;
    150.         }
    151.         if (file_exists($this->_pid_file)) {
    152.             unlink($this->_pid_file);
    153.         }
    154.     }
    155.  
    156.     final public function signalHandler($signo) {
    157.         switch($signo) {
    158.             case SIGTERM:
    159.                 $this->log('SIGTERM received, starting shutdown...');
    160.                 $this->_run = false;
    161.                 break;
    162.  
    163.             default:
    164.                 break;
    165.         }
    166.     }
    167.  
    168.     /**
    169.      * @return void
    170.      */
    171.     final private function initDirs()
    172.     {
    173.         // Init PID file path
    174.         $pid_dir = Yii::app()->getRuntimePath().DIRECTORY_SEPARATOR.'pids'.DIRECTORY_SEPARATOR.$this->_daemon_name.DIRECTORY_SEPARATOR;
    175.         if (!file_exists($pid_dir)) {
    176.             mkdir($pid_dir, 0644, true);
    177.         }
    178.         $this->_pid_file = $pid_dir.DIRECTORY_SEPARATOR.md5(__CLASS__.implode('', $this->args)).'.pid';
    179.  
    180.         // Init LOG dir
    181.         $this->_logs = Yii::app()->getRuntimePath().DIRECTORY_SEPARATOR.'logs'.DIRECTORY_SEPARATOR;
    182.         if (!file_exists($this->_logs)) {
    183.             mkdir($this->_logs, 0644, true);
    184.         }
    185.     }
    186.  
    187.     /**
    188.      * @return bool
    189.      */
    190.     final private function isRunning()
    191.     {
    192.         $running = false;
    193.         if (file_exists($this->_pid_file)) {
    194.             // Get the PID
    195.             $this->_pid = file_get_contents($this->_pid_file);
    196.             if (file_exists('/proc/'.$this->_pid) || posix_kill($this->_pid, 0)) {
    197.                 $running =  true;
    198.             } else {
    199.                 $this->pidRemove();
    200.             }
    201.         }
    202.         return $running;
    203.     }
    204.  
    205.     /**
    206.      * @param  $message
    207.      * @param bool $echo
    208.      * @return void
    209.      */
    210.     final private function log($message, $echo = false)
    211.     {
    212.         $error = posix_get_last_error();
    213.         if ($error !== 0) {
    214.             $message .= PHP_EOL.'Posix error description: '.posix_strerror($error);
    215.         }
    216.         $message .= PHP_EOL;
    217.         error_log('['.date('Y-m-d H:i:s').'] '.$message, 3, $this->_logs.'daemon.log');
    218.         if ($echo) {
    219.             echo $message;
    220.         }
    221.     }
    222.  
    223.     final private function pidRemove()
    224.     {
    225.         if (!unlink($this->_pid_file)) {
    226.             $this->log('Error while deleting PID file!', true);
    227.         }
    228.     }
    229. }
    230.  
    В нутри while ($this->_run) 98% кода application specific. Общая идея в PHP реализуется буквально 10-15 строками - остальное это красивости и удобство для логгирования. Вообще собираюсь свою наработку допилить и предложить свою компоненту CConsoleDaemon :)
     
  13. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    Psih
    Угу, спасибо. Всё понятно.

    Насколько я понимаю у тебя можно прямо в админке на кнопочку нажать и консольная команда запустит скрипт. Там же можно и посмотреть работает ли демон? Занятно.

    А я как-то всё скриптами в init.d запускаю, демоны ведь. Привычка. Ещё раз спасибо за код, появились новые идеи.
     
  14. Psih

    Psih Активный пользователь
    Команда форума Модератор

    С нами с:
    28 дек 2006
    Сообщения:
    2.678
    Симпатии:
    6
    Адрес:
    Рига, Латвия
    Hight
    Ну не совсем так, в админке я не делал ничего такого. Но по сути стопать/запускать можно и по кнопке - были бы у WEB права делать "system('php '.$path_to_daemon.'/console.php daemonname')" и "system('kill -9 '.get_file_contents($path_to_pid_file))" - всё дорабатывается по мере надобности.
    Вообще запуск через init.d штука хорошая - я собираюсь доработать это дело в ближайшую неделю-две - всё же удобнее администрировать и перезапускать.
    В общем мысль в том, что делается всё крайне легко если есть желание разобраться :)
     
  15. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    У меня PHP-FPM от юзверя работает. Если им cmd дёрну, то и скрипт от этого юзверя заведётся, всё должно быть хорошо. Да, по идее всё должно быть круто, попробую сделать в админке интерфейс для запуска и остановки демонов, удобно ведь.
    Всё само заводит при старте сервера, можно легко остановить демон, перезапустить. Класс.
     
  16. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.408
    Симпатии:
    1.768
    скрипт отправки должен метить письма которые не смог отправить.
     
  17. MiksIr

    MiksIr Активный пользователь

    С нами с:
    29 ноя 2006
    Сообщения:
    2.339
    Симпатии:
    44
    Я вот не понимаю, ты посты набиваешь? Или просто собой каждый топик метишь?
     
  18. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    MiksIr он мозговой слизень, пожирает мозги. Не будем ему мешать =)
     
  19. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.408
    Симпатии:
    1.768
    тогда я вобще проблемы не вижу...
     
  20. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    igordata
    Расслабься, я с разбега не смог с решением определиться. В бошке куча мусора, понимаешь, трудно что-то одно выбрать.
     
  21. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.408
    Симпатии:
    1.768
    Hight
    я в таких случаях забиваю на пару дней - благо неотложных дел хватает. а потом... Семен Семеныч! :D

    я честно говоря не понимаю вобще что тебя смущает.

    Было спрошено (как я понял): скрипт, который отправляет через сокеты и авторизацию может недождаться ответа и отпасть.

    Было предложено и мной и падабу метить неотправленные и пытаться снова. Можно сделать счетчик попыток.

    Я так понял что ты хочешь гарантировать себе отправку почты - каждого письма без исключения. Чем тебе не нравится поставить их в очередь к почтовику - я не понял. Ну видимо есть на то причины. Но даже если ты отправишь письмо точно, то не факт что оно дойдет.

    По сути гарантировать доставку ты не можешь в принципе. Если только не сам себе их шлешь. Т.е. ты можешь гарантировать себе отправку, договорившись с сервером лично. Но какая нафик разница от отправки через mail() - нипанятна. По сути сам факт правильной настройки сервера уже должен тебя достаточно успокоить. Почта не протухнет, если даже инет пропадет. Отправится когда восстановится. Случаи, когда почта не отправилась с настроенного сервера редки и ими можно пренебречь, т.к. неработающий сервер - это и без того великая проблема.

    В любом случае, т.к. письма которые не удалось отправить ты можешь отметить при сокетном способе отправки, то ты можешь после устранения неполадок возобновить и таки отправить почту.

    в чем беда? или я дебил, и ктонить мог бы мне сказать в чем косяк.
     
  22. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    igordata
    Ты немного не понял. Я знаю как работает почта, хорошо знаю. Я знаю, что не могу точно проверить дошло письмо или нет. Но это меня не волнует.

    Я хочу сделать велосипед, свой "сендмейл". С блэкджеком и шлюхами. Чтобы не запариваться с настройкой постфикса на серваке. Мне нужно независимое решение, гибкое, своё. Вопрос в том как лучше это сделать.

    Лично я сейчас думаю так. Есть скрипт, он отрабатывает, чота там делает и кладёт в БД письма. Есть демон, он работает всегда, каждую секунду-2-3 он ломится в БД и ищет там не отправленные письма, выбирает, отправляет, помечает как отправленные. Шикарно правда.

    Почему я задумался. У меня есть класс для работы с почтой. Он умеет отправлять письма, но только с помощью функции mail(). Я его хочу научить отправляет письма с помощью сокетов. Если бы я в метод отправки письма встроил бы функциональность с сокетами, это бы привело к пиздецам. Как я уже говорил, функция mail() отрабатывает мгновенно, она просто вызывает локальный сендмейл и отдаёт ему письмо, а он уже разруливает его отправку. Но это не прокатывает с сокетами, там возможны задержки, крутые тормоза... Значит эту задачку надо выносить куда-то, чтобы общение с SMTP-сервером происходило не в методе отправки почты, а где-то в другом месте, где пофигу тормоза.

    Вот и получается, что я делаю в админке флаг - юзать SMTP-сервер, там же скармливаю в конфиг параметры сервера (адрес, логин, пароль)... В методе отправки почты смотрю как отправлять, если флага нет - юзать функцию mail(), флаг есть - класть письмо в БД, пусть демон отправки разруливает отправку.

    А?!
     
  23. Psih

    Psih Активный пользователь
    Команда форума Модератор

    С нами с:
    28 дек 2006
    Сообщения:
    2.678
    Симпатии:
    6
    Адрес:
    Рига, Латвия
    Hight
    кстати, возми Zend_Mail - там все есть: атачи, html и.т.д. и реюз смтп соединения для батч отправки
     
  24. Hight

    Hight Старожил
    Команда форума Модератор

    С нами с:
    5 мар 2006
    Сообщения:
    7.153
    Симпатии:
    0
    Адрес:
    из злой параллельной вселенной
    Psih
    У меня тоже есть аттач и хтмл. У меня всё есть, мне извращений не хватает. Что-то вот меня на них потянуло =)
     
  25. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.408
    Симпатии:
    1.768
    Hight
    тогда вот тебе такое извращение:

    не демон а либо плодящийся демон, клонящийся. либо крон - экземпляр скрипта каждую секунду.

    новорожденный скрипт лезет в бд, включает транзакцию, берет сотню писем, помечает их флагом "забрал на отсыл", коммитит транзакцию и рассылает себе, вылетая с таймаутами даже - метит что "не удалось" и мол надо еще разок. если отослал - помечает что "ушло" или удаляет.

    если он ковыряется долго - не страшно. Следующий скрипт лезет в бд и выбирает письма, которые еще не "забрали на отсыл" или не "ушло"... ну ты понел.

    если случился форс-мажор и сервак внезапно ребутнулся - надо предусмотреть таймаут, при котором "забранные на отсыл" письма требуется слать заново.

    при таком раскладе не страшно, если демоны будут друг друга перекрывать. Не страшно, если кто-то из них подвисает на отсылке. Не страшно даже если умрет.

    это как муровьи - они будут приходить, брать, срать. Т.е. слать.