Wednesday, July 23, 2014

Using cPanel's JSON API with PHP & cURL to Manage POP Email Accounts

While working on the CRM's email relay functionality I became quite hardpressed to find a working PHP wrapper for the latest versions of cPanel's APIs. I tried several different classes people had published around the .net, most of them implementing some ground-breaking file_get_contents() / strpos() technique with the cPanel username and password in the URL. Nothing worked. I was getting desperate.

Not but by the grace of God did I stumble upon the fact that the username and password have to be POSTed along with every HTTP request along with the GET params. After about 12 solid hours of trial and error I figured out the secret recipe. And since I'm just an all-around good guy, I'll share it with you here today. But first there are a few items of business to take care of.

In order for this script to work on your server you should:
  • Have a recent version of cPanel. One that supports the JSON APIs.
  • Have cURL enabled in PHP.
  • Save the cPanelMailManager class to your server as class.cpmm.php, or whatever.
  • Create a directory at the same level as the class file called /cpmm and CHMOD it to 766. This is important. This is where the cookie files and log files are stored. 

class.cpmm.php

/**
 * cPanelMailManager
 * PHP Library for the email module of cPanel's JSON API
 * @author Rob Parham 
 * @copyright Copyright (c) 2014, Rob Parham
 * @license http://www.wtfpl.net/ WTF Public License
 */
class cPanelMailManager {
    public $cookiefile;
    private $cpanelHost;
    private $cpanelPort;
    private $username;
    private $password;
    private $logcurl;
    private $curlfile;
    private $emailArray;
    private $cpsess;

    /**
     * Constructor
     * @param string $user cPanel username
     * @param string $pass cPanel password
     * @param string $host cPanel domain
     * @param int $port cPanel domain
    */
    public function __construct($user,$pass,$host,$port=2083){
        $this->cpanelHost = $host;
        $this->cpanelPort = $port;
        $this->username = $user;
        $this->password = $pass;
        $this->logcurl = false;
        $this->cookiefile = "cpmm/cpmm_cookie_".rand(99999, 9999999).".txt";
        $this->LogIn();
    }

    /**
     * Checks if an email address exists
     * @param string $Needle Email address to check
     * @param bool $FullEmailOnly If false, will return true with or without the domain attached
     * @return bool
    */
    public function emailExists($Needle, $FullEmailOnly = false){
        $Haystack = empty($this->emailArray) ? $this->getEmails() : $this->emailArray;
        foreach($Haystack as $H){
            if($FullEmailOnly === true && $H['email'] == $Needle){
                return true;
            }else if($FullEmailOnly !== true && ($H['user'] == $Needle || $H['email'] == $Needle)){
                return true;
            }
        }
        return false;
    }

    /**
     * Creates a new email address
     * @param string $email Complete mail address to create, ie. myemail@mydomain.com
     * @param string $password Password for new email
     * @param string $quota Disk Space Quota, 0 for unlimited
     * @return bool
    */
    public function createEmail($email,$password,$quota = 0){
        if($this->emailExists($email,true)){
            return "Email address ".$email." already exist";
        }
        $e = explode("@",$email);
        $params = 'user='.$this->username.'&pass='.$this->password;;
        $url = "https://".$this->cpanelHost.":".$this->cpanelPort.$this->cpsess."/json-api/cpanel".
        "?cpanel_jsonapi_version=2".
        "&cpanel_jsonapi_func=addpop".
        "&cpanel_jsonapi_module=Email&".
        "email=".$e[0]."&".
        "domain=".$e[1]."&".
        "password=".urlencode($password)."&".
        "quota=".$quota;
        $answer = json_decode($this->Request($url,$params), true);
        $this->getEmails(true);
        return ($answer["cpanelresult"]["data"][0]['result'] === 1) ? true : false;
    }

    /**
     * Deletes an email address
     * @param string $email Complete mail address to delete, ie. myemail@mydomain.com
     * @return bool
    */
    public function deleteEmail($email){
        if(!$this->emailExists($email,true)){
            return "Email address ".$email." does not exist";
        }
        $e = explode("@",$email);
        $params = 'user='.$this->username.'&pass='.$this->password;;
        $url = "https://".$this->cpanelHost.":".$this->cpanelPort.$this->cpsess."/json-api/cpanel".
        "?cpanel_jsonapi_version=2".
        "&cpanel_jsonapi_func=delpop".
        "&cpanel_jsonapi_module=Email&".
        "email=".$e[0]."&".
        "domain=".$e[1];
        $answer = json_decode($this->Request($url,$params), true);
        $this->getEmails(true);
        return ($answer["cpanelresult"]["data"][0]['result'] === 1) ? true : false;
    }

    /**
     * Changes a password
     * @param string $email Complete email of account, ie. myemail@mydomain.com
     * @param string $newPW New password
     * @return bool
    */
    public function changePW($email, $newPW){
        if(!$this->emailExists($email,true)){
            return "Email address ".$email." does not exist";
        }
        $e = explode("@",$email);
        $params = 'user='.$this->username.'&pass='.$this->password;;
        $url = "https://".$this->cpanelHost.":".$this->cpanelPort.$this->cpsess."/json-api/cpanel".
        "?cpanel_jsonapi_version=2".
        "&cpanel_jsonapi_func=passwdpop".
        "&cpanel_jsonapi_module=Email&".
        "email=".$e[0]."&".
        "domain=".$e[1]."&".
        "password=".urlencode($newPW);
        $answer = json_decode($this->Request($url,$params), true);
        $this->getEmails(true);
        return ($answer["cpanelresult"]["data"][0]['result'] === 1) ? true : false;
    }

    /**
     * Lists all email accounts and their properties
     * @param int $pageSize Number of results per page
     * @param int $currentPage Page number to start from
     * @param bool $paginate Return in pages
     * @param bool $sort Sort the results
     * @param bstring $sortby Column to sort by, ie. "email", "_diskused", "mtime", or "domain"
     * @return array
    */
    public function listEmails($pageSize = 10, $currentPage = 1, $paginate = true, $sort = true, $sortby = "user"){
        $params = 'user='.$this->username.'&pass='.$this->password;;
        $url = "https://".$this->cpanelHost.":".$this->cpanelPort.$this->cpsess."/json-api/cpanel".
        "?cpanel_jsonapi_version=2".
        "&cpanel_jsonapi_func=listpopswithdisk".
        "&cpanel_jsonapi_module=Email".
        "&api2_paginate=".($paginate === false ? 0 : 1).
        "&api2_paginate_size=".$pageSize.
        "&api2_paginate_start=".$currentPage.
        "&api2_sort=".($sort === false ? 0 : 1).
        "&api2_sort_column=".$sortby.
        "&api2_sort_method=alphabet".
        "&api2_sort_reverse=0";
        $answer = $this->Request($url,$params);
        $emails = json_decode($answer, true);
        $this->emailArray = $emails["cpanelresult"]["data"];
        return $this->emailArray;
    }

    /**
     * Turns cURL logging on
     * @param int $curlfile Path to curl log file
     * @return array
    */
    public function logCurl($curlfile = "cpmm/cpmm_curl_log.txt"){
        if(!file_exists($curlfile)){
            try{
                fopen($curlfile, "w");
            }catch(Exception $ex){
                if(!file_exists($curlfile)){
                    return $ex.'Cookie file missing.'; exit;
                }
                return true;
            }
        }else if(!is_writable($curlfile)){
            return 'Cookie file not writable.'; exit;
        }
        $this->logcurl = true;
        return true;
    }
    
    /**
     * Returns a complete list of emails and their properties
     * @access private
    */
    private function getEmails($refresh = false){
        if(!empty($this->emailArray) && !$refresh){
            return $this->emailArray;
        }
        $params = 'user='.$this->username.'&pass='.$this->password;;
        $url = "https://".$this->cpanelHost.":".$this->cpanelPort.$this->cpsess."/json-api/cpanel".
        "?cpanel_jsonapi_version=2".
        "&cpanel_jsonapi_func=listpopswithdisk".
        "&cpanel_jsonapi_module=Email";
        $answer = $this->Request($url,$params);
        $emails = json_decode($answer, true);
        $this->emailArray = $emails["cpanelresult"]["data"];
        return $this->emailArray;
    }

    /**
     * Starts a session on the cPanel server
     * @access private
    */
    private function LogIn(){
        $url = 'https://'.$this->cpanelHost.":".$this->cpanelPort."/login/?login_only=1";
        $url .= "&user=".$this->username."&pass=".urlencode($this->password);
        $answer = $this->Request($url);
        $answer = json_decode($answer, true);
        if(isset($answer['status']) && $answer['status'] == 1){
            $this->cpsess = $answer['security_token'];
            $this->homepage = 'https://'.$this->cpanelHost.":".$this->cpanelPort.$answer['redirect'];
        }
    }

    /**
     * Makes an HTTP request
     * @access private
    */
    private function Request($url,$params=array()){
        if($this->logcurl){
            $curl_log = fopen($this->curlfile, 'a+');
        }
        if(!file_exists($this->cookiefile)){
            @fopen($this->cookiefile, "w");
            if(!file_exists($this->cookiefile)){
                echo 'Cookie file missing. '.$this->cookiefile; exit;
            }
        }else if(!is_writable($this->cookiefile)){
            echo 'Cookie file not writable. '.$this->cookiefile; exit;
        }
        $ch = curl_init();
        $curlOpts = array(
            CURLOPT_URL             => $url,
            CURLOPT_USERAGENT       => 'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0',
            CURLOPT_SSL_VERIFYPEER  => false,
            CURLOPT_RETURNTRANSFER  => true,
            CURLOPT_COOKIEJAR       => realpath($this->cookiefile),
            CURLOPT_COOKIEFILE      => realpath($this->cookiefile),
            CURLOPT_FOLLOWLOCATION  => true,
            CURLOPT_HTTPHEADER      => array(
                "Host: ".$this->cpanelHost,
                "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                "Accept-Language: en-US,en;q=0.5",
                "Accept-Encoding: gzip, deflate",
                "Connection: keep-alive",
                "Content-Type: application/x-www-form-urlencoded")
        );
        if(!empty($params)){
            $curlOpts[CURLOPT_POST] = true;
            $curlOpts[CURLOPT_POSTFIELDS] = $params;
        }
        if($this->logcurl){
            $curlOpts[CURLOPT_STDERR] = $curl_log;
            $curlOpts[CURLOPT_FAILONERROR] = false;
            $curlOpts[CURLOPT_VERBOSE] = true;
        }
        curl_setopt_array($ch,$curlOpts);
        $answer = curl_exec($ch);
        if (curl_error($ch)) {
            echo curl_error($ch); exit;
        }
        curl_close($ch);
        if($this->logcurl){
            fclose($curl_log);
        }
        return (@gzdecode($answer)) ? gzdecode($answer) : $answer;
    }
}

Usage
First, start off by passing your cPanel credentials to the class. This script runs at a cPanel level and probably will not work if you use the root/WHM username and password.


    // include the library
    include("class.cpmm.php");

    // cPanel domain or IP
    $host = "mywebsite.com";

    // cPanel Username
    $user = "cPanel_Username";

    // cPanel Password
    $pass = "cPanel_Password";

    // initialize the class
    $cpmm = new cPanelMailManager($user, $pass, $host);


Once you've initialized the class, basic email functionality is a breeze. You can list, create, delete, validate and even change passwords in a single line each.


    // Create a new email address
    $email = "newemail@mywebsite.com";
    $password = "mybadpassword";
    $result = $cpmm->createEmail($email,$password);
    echo "Email ($email) ".($result ? "successfully" : "not")." created.
";

    // Check if an email exists
    $email = "newemail@mywebsite.com";
    $result = $cpmm->emailExists($email);
    echo "Email ($email) ".($result ? "does" : "does not")." exist.
";

    // Change an email password
    $email = "newemail@mywebsite.com";
    $newPassword = "mybetterpassword";
    $result = $cpmm->changePW($email,$newPassword);
    echo ($result ? "Changed" : "Could not change")." password for email $email.
";

    // Delete an email account
    $email = "newemail@mywebsite.com";
    $result = $cpmm->deleteEmail($email);
    echo ($result ? "Deleted" : "Could not delete")." email account $email.
";

    // List email accounts
    $pageSize = 15;
    $pageNo = 1;
    $result = $cpmm->listEmails($pageSize, $pageNo);
    echo "
";
    var_dump($result);

5 comments:

  1. I'm trying to use your class but tokens that are generated aren't working, I'm getting this:

    {"cpanelresult":{"apiversion":"2","error":"Access denied","data":{"reason":"Access denied","result":"0"},"type":"text"}}

    ReplyDelete
    Replies
    1. You must create a directory at the same level as the class file called /cpmm and CHMOD it to 766. This is important. This is where the cookie files and log files are stored. cPanel requires these cookies.

      Delete
  2. Thank you so much for this GREAT page. I was getting Access Denied messages for a whole day until I used your Class. I did have to modify it by replacing port=2082 and https->http for my site.

    ReplyDelete
  3. Thank you very much for your code, Robert.

    The login part was really useful, and I reused it to be able to install SSL certificates to cPanel (https://github.com/juliogonzalez/cp-installssl).

    Specifically it can be used for renewing and installing Let's Encrypt certificates at cPanel installs when the Let's Encrypt module is not available.

    Needless to say, you are credited at both the source code and the README

    Regards!

    ReplyDelete
  4. Thanks so much for your code
    just make dir cpmm
    *I tested on 3 difference cpanel hosting and all work!*
    funny things ?I been test on many github/cpanel guide but not work ,you are the real one-Thanks

    ReplyDelete