Mailadmin | ![]() |
- Introducere
- Descriere Aplicatie
- Implementare
- Linkuri
Introducere
Proiectul de fata este o interfata WEB pentru administrarea userilor, aliasurilor si domeniilor unui server de mail QMAIL.
Serverul de mail original, a fost modificat folosind patch-urile de la http://qmail-sql.digibel.be/ pentru a face managementul
mult mai usor, folosind o baza de date.
Qmail-sql suporta 2 "baze de date" si anume: Postgresql, Mysql plus ODBC (pentru conectivitate standard cu alte baze de date).
Proiectul meu se ocupa doar de serverele de mail instalate cu suport pentru Postgresql.
Qmail-sql permite virtual mail hosting pe acelasi server de mail (fizic).
Descriere Aplicatie
Partile aplicatiei
Aplicatia este impartita in 3, si anume:
- Interfata ADMIN pentru serverul de mail
- Interfata ADMIN domeniu (user = domeniu.ro)
- Interfata ADMIN ADVANCED mode pentru a avea acces direct la tabelele SQL prin intermediul aplicatiei WEB
Administratorul poate sa adauge/stearga domeniile pe care vrea sa le hosteze (mail), precum si editarea unui domeniu in sine, adica cautare/adaugare/stergere/editare useri, cautare/adaugare/stergere/editare aliasuri.
Daca modul este ADMIN domeniu, atunci interfata permite doar editarea userilor, aliasurilor ce apartin acelui domeniu.
In modul ADVANCED, administratorul poate edita direct (low level) o parte din tabelele SQL folosite, avand posibilitati de cautare, sortare, adaugare, editare, stergere.

Implementare
Librarii folosite
Mailadmin are la baza:
- PEAR::DB - pentru transparenta fata de baza de date
- Smarty Template Engine - penru separarea codului PHP de templateul HTML
- Webform2 - Clasa + Plugin-uri Smarty pentru validarea formularelor HTML
Baza de date
Baza de date a fost facuta folosind Sybase Power Designer, datorita usurintei de folosire si a functiilor complexe pe care acest utilitar de Database Modelling il ofera. Serverul folosit este Postgresql.
Structura:

Drept urmare avem 7 tabele si un view.
Structura si Initializare
Structura fisierelor
MVC - este modelul folosit in Mailadmin.
M = Model, V = View, C = Controller
Partea de Model o reprezinta Baza de date si DATEle propriuzise din SQL.
Partea de View o reprezinta, partea vizuala, adica templateurile Smarty.
Partea de Controller o reprezinta codul PHP care se ocupa de View si de Model.
In directorul de baza se afla fisierele .php care (Controller), in WEB-INF/templates template-urile, in WEB-INF/lib librariile (clase, fisiere cu functii php), in WEB-INF/tmpdir fisierele si directoarele temporare (session, smarty, logs).
Partea de ADMIN ADVANCED este pusa in directorul advanced/ si foloseste templateuri din WEB-INF/templates/admin.
Initializarea si configurarea aplicatiei
app.cfg.php, mailadmin.conf sunt cele 2 fisiere in care este configurata aplicatia.
app.cfg.php - este fisierul de baza pe care toate celelalte fisiere il includ (require_once), in care se fac urmatoarele chestii de initializare:
- se seteaza constantele:
define("WORK_PATH",dirname(__FILE__)); define("WEBINF_PATH",WORK_PATH."/WEB-INF"); define("TEMP_PATH",WEBINF_PATH."/tmpdir"); define("CONF_FILE",WEBINF_PATH."/conf/mailadmin.conf"); - se includ fisierele php cu clase si functii
- se seteaza logging system
- se seteaza Smarty Template Engine
- se parseaza fisierul de configurare mailadmin.conf
- se porneste sesiunea $session->start()
- se face o verificare la toate datele POST/GET pe care se aplica un stripslashes
- si cel mai important se face conectarea la baza de date:
// DB settings require_once("DB.php"); $sqlDSN=$conf_data["db"]["dbtype"]."://". $conf_data["db"]["user"].":". $conf_data["db"]["password"]."@". $conf_data["db"]["host"]."/". $conf_data["db"]["database"]; $db = DB::connect($sqlDSN,true); if (DB::isError($db)) { log_error("Could not connect to $sqlDSN. ".$db->getMessage()); $smarty->display("errors/sql.fatal.html"); exit(); } $db->setFetchMode(DB_FETCHMODE_ASSOC);
mailadmin.conf - situat in WEB-INF/conf
; mailadmin configuration file [db] ; supported versions are pgsql or mysql dbtype=pgsql host=localhost user=root password=dotcom database=pbd [default] mailpath=/home/mail uid=1000 gid=1000 minuid=1000 mingid=1000 [language] lang=en_US [browse] page_limit=10
Tipul fisierului este identic cu cel din php.ini, iar parsarea se face folosind $conf_data=parse_ini_file(CONF_FILE,true);.
Conexiunea la baza de date
Conexiunea se face folosind metodat connect din PEAR::DB. PEAR::DB este un package dezvoltat de cei de la php.net care permite programatorului
sa scrie COD SQL portabil (in mare masura) pe mai multe baze de date.
Drept urmare este un API unificat de SQL, la fel cum in java avem java.sql.*;
Metoda $db = DB::connect($sqlDSN,true); va functiona si pe Postgres si pe Mysql si pe MSSQL, si pe Oracle, samd.
connect primeste 2 parametrii:
$sqlDSN care in cazul nostru este: pgsql://root:dotcom@localhost/pbd si reprezinta tipul bazei de date (pgsql), userul si parola,
hostul (localhost) si baza de date folosita (pbd).
true - care face conexiunea sa fie persistenta intre requesturi. Acest lucru este realizat de engine-ul PHP si permite ca aceeasi resursa (conexiunea la baza de date)
sa fie folosita de fara ca ea sa fie inchisa si deschisa la fiecare incarcare de pagina. Un fel de Data Base Connection Polling din Java/Tomcat.
Programare
Autentificare
Autentificarea este facuta in do.login.php. In acest fisier se consulta tabelul mailadmin_auth. In cazul in care avem match pe user/password, se stocheaza in session auth_data si se face redirect catre prima pagina care poate fi: lista de domenii (admin mode), sau lista de useri (domain mode).
// check if the user is admin
$cmd="select * from mailadmin_auth where login=? and passwd=?";
$data=$db->getRow($cmd,array(w2_get("username"),w2_get("password")));
if (DB::isError($data))
{
w2_add_error("sql",$data->getMessage());
return false;
}
if (is_array($data))
{
$session->register("auth_data",$data);
if ($data["mode"]==1)
{
set_auth("admin");
} else set_auth("domain");
return true;
}
w2_add_error("username","No such username / password.");
$cmd="select * from mailadmin_auth where login=? and passwd=?"; - reprezinta comanda SQL. ? si ! sunt place holders.
? tine locul unui string iar ! tine locul unui integer.
La executia comenzii $data=$db->getRow($cmd,array(w2_get("username"),
w2_get("password")));, PEAR::DB face inlocuirea place
holderilor cu valorile trimise: array(w2_get("username"),w2_get("password")).
Acest lucru este foarte util si ofera o securitate sporita pentru ca automat in PEAR::DB se efectueaza string quote lucru ce previne
atacurile gen SQL INJECTION.
$db->getRow(...) - este folosit cand sunt sigur ca rezultatul query-ului este doar pe o linie (1 entry)
$db->getAll(...) - cand am mai multe linii
$db->getOne(...) - cand ma intereseaza doar o valoare. eg: $user_id=$db->getOne("select user_id from user where username=?", array("catalin"));
Verificarea daca comanda SQL a fost executata OK se face cu DB::isError($resource).
Listare domenii
// fetch din baza de date
$domains=$db->getAll("select * from locals order by virtual_host ASC");
...
// assign pentru vizualizare
$smarty->assign("domains",$domains);
// afisare template
$smarty->display("domains.html");
Partea de HTML este generata folosind smarty tag section.
<select name="domain" size="10" id="domain" style="width: 250px;">
{section name="dl" loop="$domains"}
<option value="{$domains[dl].virtual_host}">{$domains[dl].virtual_host}
</option>
{/section}
</select>
Adaugare domeniu
// we add the domain also the required ALIAS user for this domain
$fv=array("virtual_host" => $new_domain);
$ret=$db->autoExecute("locals",$fv,DB_AUTOQUERY_INSERT);
if (DB::isError($ret))
{
log_error("SQL Error: ".$ret->getMessage()." ".$ret->getUserInfo());
}
$fv=array(
"login" => "alias",
"uid" => $conf_data["default"]["uid"],
"gid" => $conf_data["default"]["gid"],
"home" => $conf_data["default"]["mailpath"]."/".$new_domain,
"virtual_host" => $new_domain,
"hardquota" => 0,
"startdate" => "NOW()",
"enabled" => true,
"use_dotqmail" => true);
$ret=$db->autoExecute("passwd",$fv, DB_AUTOQUERY_INSERT);
if (DB::isError($ret))
{
log_error("SQL Error: ".$ret->getMessage()." ".$ret->getUserInfo());
}
$fv=array(
"login" => $new_domain,
"passwd" => "",
"mode" => 2);
$ret=$db->autoExecute("mailadmin_auth",$fv, DB_AUTOQUERY_INSERT);
if (DB::isError($ret))
{
log_error("SQL Error: ".$ret->getMessage()." ".$ret->getUserInfo());
}
La adaugarea unui domeniu se adauga intrarea in locals, userul alias si datele de autentificare pentru acest domeniu in mailadmin_auth.
Cautare/Listare Useri
if (strlen($filter)==1) $filter=$filter."%";
else $filter="%".$filter."%";
if ($conf_data["db"]["dbtype"]=="pgsql")
{
// comanda de numarare
$count_cmd="select count(*) as cate from passwd where login ~~ ?
and virtual_host=?";
// comanda de listare (extragere date), tinand cont de paginare
$list_cmd="select * from passwd where login ~~ ? and virtual_host=?
order by login $order limit ! offset !";
}
$cate=$db->getOne($count_cmd,array($filter,$domain));
// paginare (cate X pe pagina)
$pepagina=$conf_data["browse"]["page_limit"];
$totalpagini=$cate/$pepagina;
if ($totalpagini==0) $paginacurenta=0;
if (($cate%$pepagina)!=0) $totalpagini+=1;
$totalpagini=floor($totalpagini);
$page+=0;
if ($page>$totalpagini) $page=1;
if ($page<1) $page=1;
if ($totalpagini>0)
{
// datele paginii curente
$users=$db->getAll($list_cmd,array($filter,
$domain,
$pepagina,
(($page-1)*$pepagina)));
Mai sus este portiunea de cod in care se face listare/cautarea userilor, folosind filtru (daca e filtru => cautare, else listare, codul e la fel in ambele cazuri).
Initial am pornit la dezvoltarea aplicatiei in ideea ca ea va fi portata si pe Mysql (macar), de aici partea de cod cu if ($conf_data["db"]["dbtype"]=="pgsql").
Adaugare User
$cmd="insert into passwd (
login,
uid,
gid,
home,
virtual_host,
password,
hardquota,
startdate,
stopdate,
enabled,
use_dotqmail)
values(?,!,!,?,?,?,!,NOW(),?,'Yes',?)";
if ($unlimited=="true")
{
// live for ever
$stopdate=($exp_date_Year+500)."-".
$exp_date_Month."-".
$exp_date_Day;
} else
{
$stopdate=$exp_date_Year."-".$exp_date_Month."-".$exp_date_Day;
}
$usedotqmail=strlen($dotqmail)>0?"Yes":"No";
$ret=$db->query($cmd, array($login,
$uid,$gid,$home,
$virtual_host,$password,
$hardquota,$stopdate,
$usedotqmail)
);
Dupa validarea formularului (via webform2) se face introducerea in SQL(insert).
Editare(update) User
$login=w2_get("username");
$uid=w2_get("uid");
$gid=w2_get("gid");
$home=w2_get("home");
$virtual_host=$session->get("domain");
$salt=chr(rand(0,25)+97).chr(rand(0,25)+97);
if (strlen(w2_get("password"))>0)
$password=crypt(w2_get("password"),$salt);
else $password="";
$hadrquota=w2_get("hardquota")+0;
$dotqmail=trim(w2_get("dotqmail"));
$unlimited=w2_get("unlimited");
if ($auth_mode!="admin")
{
// presets
$uid=$conf_data["default"]["uid"];
$gid=$conf_data["default"]["gid"];
$data=$db->getRow("select * from passwd where
login='alias' and virtual_host=?",array($virtual_host));
if (DB::isError($data))
$home="/tmp/".$virtual_host."/users/".$login;
else $home=$data["home"]."/users/".$login;
}
if ($password=="")
{
$cmd="update passwd set uid=!,
gid=!,
home=?,
stopdate=?,
use_dotqmail=? where
login=? and virtual_host=?";
}
else
{
$cmd="update passwd set password=?,
uid=!,
gid=!,
home=?,
stopdate=?,
use_dotqmail=?
where login=? and virtual_host=?";
}
if ($unlimited=="true")
{
// live for ever
$stopdate=($exp_date_Year+500)."-".
$exp_date_Month."-".
$exp_date_Day;
} else
{
$stopdate=$exp_date_Year."-".
$exp_date_Month."-".
$exp_date_Day;
}
$usedotqmail=strlen($dotqmail)>0?"Yes":"No";
if ($password=="")
{
$ret=$db->query($cmd,array(
$uid,
$gid,
$home,
$stopdate,
$usedotqmail,
$login,
$virtual_host));
} else
{
$ret=$db->query($cmd,array(
$password,
$uid,
$gid,
$home,
$stopdate,
$usedotqmail,
$login,
$virtual_host));
}
if (DB::isError($ret))
{
log_error($ret->getMessage()." ".$ret->getUserInfo());
$smarty->assign("exp_date",
$exp_date_Year."-".
$exp_date_Month."-".
$exp_date_Day);
w2_add_error("msg","Could not update user.");
require_once("useredit.php");
exit();
}
if ($usedotqmail=="Yes")
{
$userdata=$session->get("userdata");
if (strlen($userdata["dotqmail"])<1)
{
// no forward => do insert
$ret=$db->query("insert into dotqmails (
login,
virtual_host,
dotqmail,
extension)
values(?,?,?,?)",
array($login,
$virtual_host,
$dotqmail,""));
} else
{
// just update here
$ret=$db->query("update dotqmails set dotqmail=?
where login=? and virtual_host=?",
array($dotqmail, $login, $virtual_host));
}
if (DB::isError($ret))
{
log_error($ret->getMessage()." ".$ret->getUserInfo());
header("Location: useredit.php?login=$login&msg=".
urlencode("Could not update dotqmail
content for this user."));
exit();
}
}
Dupa validarea formularului (via webform2) se face update in SQL(update).
In functie de ce a editat userul se face update in unul sau mai multe tabele.
Cautare/Listare Aliasuri
$count_cmd=" select count(*) as cate from dotqmails where ( (((login ~~ ?) OR (extension ~~ ?)) AND (login<>'alias')) OR ((extension ~~ ?) AND login='alias') OR ((login ~~ ?) AND login<>'alias') ) AND extension<>'' and virtual_host=?"; $list_cmd=" select * from dotqmails where ( (((login ~~ ?) OR (extension ~~ ?)) AND (login<>'alias')) OR ((extension ~~ ?) AND login='alias') OR ((login ~~ ?) AND login<>'alias') ) AND extension<>'' and virtual_host=? order by (login||extension) $order limit ! offset !";
Codul de paginare este identic cu cel de la Useri, singurul lucru care se schimba fiind query-urile (de count si de cauta/listare).
Clauza WHERE este destul de complexa, cauza fiind modul in care un alias este definit.
alias-username@domeniu.ro este de fapt alias pentru adresa username@domeniu.ro
username-private@domeniu.ro este un alias al userului username care isi face EXTRA adresa username-private@domeniu.ro, acest lucru fiind permis de serverul de mail Qmail.
Adaugare Alias
$virtual_host=$session->get("domain");
$alias=w2_get("alias");
$dotqmail=trim(w2_get("dotqmail"));
$cmd="insert into dotqmails (
login,
virtual_host,
extension,
dotqmail)
values(?,?,?,?)";
$ret=$db->query($cmd, array("alias",$virtual_host,$alias,$dotqmail));
Dupa validarea formularului (via webform2) se face introducerea in SQL(insert).
Editare(update) Alias
$virtual_host=$session->get("domain");
$alias=w2_get("alias");
$dotqmail=trim(w2_get("dotqmail"));
$cmd="update dotqmails set dotqmail=?
where login=? and
extension=? and
virtual_host=?";
$ret=$db->query($cmd, array($dotqmail,
"alias", $alias, $virtual_host));
Dupa validarea formularului (via webform2) se face update in SQL(update).
Programare Advanced Mode
Modul Advanced permite DOAR administratorului sa acceseze direct tabelele SQL. (un fel de phpPgAdmin/phpMyAdmin mult mai simplu).
Limitarea este ca doar tabele care au o coloana UNICA pot fi editate, deoarece la edit, delete clausa where foloseste aceasta coloana.
Permite: cautare dupa anumite campuri din tabela editata, sortarea ASCendenta, DESCendenta dupa un anumit camp din tabela editata, precum si adaugare, editare, stergere inregistrari.
Oricum este bine ca asa putem testa error_log entries.

// cautare, listare
function getTableData()
{
global $_GET;
global $table_info;
global $db;
global $table;
global $order, $orderby;
// qpart = query part
if (($orderby) &&
($order=="ASC" || $order=="DESC"))
$order_qpart=" order by
$orderby $order ";
else $order_qpart="";
$where="where ";
foreach($table_info as $field)
{
$gvalue=$_GET["field_".$field["name"]];
if (strlen($gvalue)>0)
{
if (strstr($field["type"],"int"))
{
// avem un INT
$where.=$field["name"]."=".$gvalue;
} else
{
// avem un text sau o data
$where.="lower(".$field["name"].") ~~ ".
$db->quoteSmart("%".strtolower($gvalue)."%");
}
$where.=" AND ";
}
}
$where.=" true ";
$query="select * from $table $where $order_qpart";
$table_data=$db->getAll($query);
return $table_data;
}
// adaugare
function doAdauga()
{
global $_GET;
global $table;
global $db;
$sql_data=simpleVars($_GET);
$ret=$db->autoExecute($table, $sql_data, DB_AUTOQUERY_INSERT);
return $ret;
}
...
$table_info=$db->tableInfo($table);
$smarty->assign("table_info", $table_info);
Extra
Login Log
La fiecare autentificare se introduce in baza de date userul, data, timp si hostul de la care s-a facut autentificarea, din motive de securitate.
// adauga entry in login_stats
// daca nu exista face sequence
$l_id=$db->nextId("login_stats");
$sql_data=array (
"l_id" => $l_id,
"login" => $auth_data["login"],
"host" => getenv("REMOTE_ADDR"));
$db->autoExecute("login_stats", $sql_data,
DB_AUTOQUERY_INSERT);
$db->query("update login_stats set data=now(),
timp=now() where l_id=!", array($l_id));
Password Log
La fiecare modificare de parola se introduce in baza de date userul, data, timp, old_pass si new_pass, tot din motive de securitate.
// adauga entry in password_stats
// daca nu exista face sequence
$p_id=$db->nextId("password_stats");
$old_pass=$session->get("cpass");
$sql_data=array (
"p_id" => $p_id,
"login" => $auth_data["login"],"old_pass" => $old_pass,
"new_pass" => w2_get("password")
);
$db->autoExecute("password_stats",
$sql_data, DB_AUTOQUERY_INSERT);
$db->query("update password_stats set data=now(),
timp=now() where p_id=!", array($p_id));
Linkuri
PEAR - http://pear.php.net/
Smarty - http://smarty.php.net/
PHP - http://www.php.net/
Fam. Popeanga - http://popeanga.go.ro/

