CORE-POS/IS4C

View on GitHub
fannie/classlib2.0/data/pipes/NewMemberEmailPipe.php

Summary

Maintainability
B
4 hrs
Test Coverage
F
0%
<?php
/*******************************************************************************

    Copyright 2013 Whole Foods Co-op

    This file is part of CORE-POS.

    IT CORE is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    IT CORE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    in the file license.txt along with IT CORE; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*********************************************************************************/

namespace COREPOS\Fannie\API\data\pipes {

/**
  @class NewMemberEmailPipe

  This is a PHP script designed to receive emails.
  Postfix (and perhaps other MTAs) can deliver a message
  to a program rathan than a mailbox. This script is
  meant to receive those kind of piped messages.

  The main idea is to provide a pathway for a host on
  the general internet to send a message to Fannie
  without allowing access to HTTP/HTTPS/MySQL. There
  are validation/authentication concerns no matter 
  what the mechanism, but I think this approach 
  has some advantages in terms of keeping responses
  to external inputs tailored really, really narrowly.
  It does add "mail server admin" to the list of hats
  the IT staff has to wear though.

  The initial usage I have in mind is signing up
  members via the co-op's website. When someone signs
  up on the site, the website would send an email to
  the store with their information encoded somehow
  (JSON, probably) and then this script could use
  that information to update the database. Using a
  mysql driver that supports prepared statements 
  properly would be highly recommended since this
  increases the danger of malicious input a fair amount.

  For validation, I think I'll prepopuldate a set
  of member accounts on the POS side with some kind
  of random hexadecimal key on each account (likely
  via custdata.blueLine) and push them out to the 
  website. The incoming member signup messages could
  then be required to provide the correct hex key
  for the account they are creating. Headers could
  be validated, too (From, originating server, etc)
  but since it's all spoofable I don't know if it's
  even worth bothering.

  Prepopulating accounts on the website does open
  the possibility that new memberships could 
  become "out of stock" which is rather silly, but
  that should be fairly simple to avoid with a 
  generous initial allocation and then fine-tuning
  based on results. It would eliminate most of the
  risk of accidentally giving out the same number
  twice if one customer is signing up in the store
  while another is signing up online.
*/
class NewMemberEmailPipe
{
    public function processMail($msg)
    {
        $info = $this->parseEmail($msg);
        
        if ($this->validateHeaders($info['headers'])) {
            $data = json_decode($info['body']);
        }
    }

    protected function validateHeaders($headers)
    {
        // maybe impose sender or originating SMTP server restrictions
        // maybe require key-value pairs to help authenticate
        // the message
        return true;
    }

    /**
    Parse email message
    @param $content [string] raw email
    @return keyed array
      - header => key:value [array] of headers
      - body => [string] raw body
      - invalid => [array] of malformed lines

    I highly doubt this is 100% RFC2822 compliant.
    Shouldn't matter for use cases I'm considering,
    but take "invalid" with a grain of salt. It just
    means my validation says it isn't a correctly 
    formatted header line.
    */
    protected function parseEmail($content)
    {
        $headers = array();
        $last_header = '';
        $body = '';
        $reached_body = false;
        $garbage = array();
        $lines = preg_split('/\r?\n|\n/', $content);
        foreach($lines as $line) {

            $test_end = str_replace("\r", "", $line);
            $test_end = str_replace("\n", "", $test_end);
            if (!$reached_body && $test_end === '') {
                $reached_body = true;
                continue;
            }

            if ($reached_body) {
                $body .= $line."\n";
            } else {
                if ($this->rfcCompliant($line[0]) && preg_match('/^([^:]+):(.+)$/', $line, $matches) === 1) {
                    $field_name = rtrim($matches[1]);
                    $field_body = ltrim($matches[2]);
                    if ($this->rfcCompliant($field_name)) {
                        $headers[$field_name] = $field_body;
                        $last_header = $field_name;
                    } else {
                        $garbage[] = $line;
                        $last_header = '';
                    }
                } else if ($last_header !== '') {
                    $headers[$last_header] .= $line;
                } else {
                    $garbage[] = $line;
                }
            }
        }

        return array('headers'=>$headers, 'body'=>$body, 'invalid'=>$garbage);
    }

    /**
      Check if string is an RFC2822 field name
      Field names should contain characters between
      ASCII 33 and 126 except for ":" (ASCII 58)
      @param $str [string] to check
      @return [boolean]
    */
    protected function rfcCompliant($str)
    {
        for($i=0; $i<strlen($str);$i++) {
            if (ord($str[$i]) < 33 || ord($str[$i]) > 126 || ord($str[$i]) == 58) {
                return false;
            }
        }

        return true;
    }
}

}

namespace {
class NewMemberEmailPipe extends \COREPOS\Fannie\API\data\pipes\NewMemberEmailPipe {}

if (php_sapi_name() === 'cli' && basename($_SERVER['PHP_SELF']) == basename(__FILE__)) {
    $obj = new \COREPOS\Fannie\API\data\pipes\NewMemberEmailPipe();
    $message = file_get_contents("php://stdin");
    if (!empty($message)) {
        $fp = fopen('/tmp/savemailtest', 'w');
        fwrite($fp, $message);
        fclose($fp);
        $obj->processMail($message);
    }
} 

}