Archive for the ‘PHP’ Category

Dovecot IMAP SquirrelMail Cannot Append Error

Saturday, May 31st, 2008

If you’re a user of SquirrelMail and Dovecot 1.x, you may run across this error “ERROR: Bad or malformed request. Server responded: Error in IMAP command APPEND:” after sending a message. The composed email message still sends, but you get that annoying error. I don’t know exactly what caused it other than a possible Dovecot upgrade, but nevertheless, this is how to alter SquirrelMail to resolve the issue.

In the functions directory of your SquirrelMail install, open the imap_general.php file.

Alter the sqimap_append PHP function by commenting out the line that starts with the fputs function and type in the replacement so it reads as:

function sqimap_append ($imap_stream, $sent_folder, $length) {
//    fputs ($imap_stream, sqimap_session_id() . " APPEND \"$sent_folder\" (\\Seen) \{$length}\r\n");
    fputs ($imap_stream, sqimap_session_id() . " APPEND \"$sent_folder\" (\\Seen) {" . $length . "}\r\n");
    $tmp = fgets ($imap_stream, 1024);
    sqimap_append_checkresponse($tmp, $sent_folder);
}

Save the changes and the problem should be solved. FYI: Upgrading to the latest version of SquirrelMail will resolve this issue as well, but applying this code change is less of a hassle.

Fix Source: XMail Forum -> dovecot imap cannot append

Problems upgrading old PEAR versions

Wednesday, April 30th, 2008

On some old servers I’ve had problems upgrading PEAR. I don’t remember the exact error message but I used it for a quick Google search to find one solution. If you have problems upgrading old versions of PEAR, you can try these steps to force an upgrade from PEAR 1.3.2 and up.

You can use these commands to force the upgrade.

pear upgrade --force http://pear.php.net/get/Archive_Tar http://pear.php.net/get/XML_Parser http://pear.php.net/get/Console_Getopt-1.2.2
pear upgrade --force http://pear.php.net/get/PEAR-1.3.3 (use only if your PEAR is older than v1.3.3)
pear upgrade --force http://pear.php.net/get/PEAR-1.4.3
pear upgrade PEAR

Here you can see the upgrade in action (fun fun fun).

[root@linux ~]# pear upgrade --force http://pear.php.net/get/Archive_Tar http://pear.php.net/get/XML_Parser http://pear.php.net/get/Console_Getopt-1.2.2
downloading Archive_Tar-1.3.2.tgz ...
Starting to download Archive_Tar-1.3.2.tgz (17,150 bytes)
......done: 17,150 bytes
WARNING: channel "pear.php.net" has updated its protocols, use "channel-update pear.php.net" to update
downloading XML_Parser-1.2.8.tgz ...
Starting to download XML_Parser-1.2.8.tgz (13,476 bytes)
...done: 13,476 bytes
downloading Console_Getopt-1.2.2.tgz ...
Starting to download Console_Getopt-1.2.2.tgz (4,252 bytes)
...done: 4,252 bytes
Did not download optional dependencies: pear/XML_RPC, use --alldeps to download automatically
warning: pear/PEAR dependency package "pear/Console_Getopt" downloaded version 1.2.2 is not the recommended version 1.2.3
downloading PEAR-1.7.1.tgz ...
Starting to download PEAR-1.7.1.tgz (302,377 bytes)
...done: 302,377 bytes
upgrade ok: channel://pear.php.net/PEAR-1.7.1
PEAR: Optional feature webinstaller available (PEAR's web-based installer)
PEAR: Optional feature gtkinstaller available (PEAR's PHP-GTK-based installer)
PEAR: Optional feature gtk2installer available (PEAR's PHP-GTK2-based installer)
upgrade ok: channel://pear.php.net/Console_Getopt-1.2.2
upgrade ok: channel://pear.php.net/XML_Parser-1.2.8
upgrade ok: channel://pear.php.net/Archive_Tar-1.3.2
To install use "pear install pear/PEAR#featurename"
[root@linux ~]# pear upgrade --force http://pear.php.net/get/PEAR-1.4.3
downloading PEAR-1.4.3.tgz ...
Starting to download PEAR-1.4.3.tgz (276,859 bytes)
.........................................................done: 276,859 bytes
WARNING: channel "pear.php.net" has updated its protocols, use "channel-update pear.php.net" to update
Did not download optional dependencies: pear/XML_RPC, use --alldeps to download automatically
downloading Console_Getopt-1.2.3.tgz ...
Starting to download Console_Getopt-1.2.3.tgz (4,011 bytes)
...done: 4,011 bytes
upgrade ok: channel://pear.php.net/Console_Getopt-1.2.3
upgrade ok: channel://pear.php.net/PEAR-1.4.3
PEAR: Optional feature webinstaller available (PEAR's web-based installer)
PEAR: Optional feature gtkinstaller available (PEAR's PHP-GTK-based installer)
PEAR: To install optional features use "pear install pear/PEAR#featurename"
[root@linux ~]# pear upgrade PEAR
WARNING: channel "pear.php.net" has updated its protocols, use "channel-update pear.php.net" to update
Did not download optional dependencies: pear/XML_RPC, use --alldeps to download automatically
downloading PEAR-1.7.1.tgz ...
Starting to download PEAR-1.7.1.tgz (302,377 bytes)
..............................................................done: 302,377 bytes
upgrade ok: channel://pear.php.net/PEAR-1.7.1
PEAR: Optional feature webinstaller available (PEAR's web-based installer)
PEAR: Optional feature gtkinstaller available (PEAR's PHP-GTK-based installer)
PEAR: Optional feature gtk2installer available (PEAR's PHP-GTK2-based installer)
To install use "pear install PEAR#featurename"

This article is based entirely from this PEAR Bug posting:
PEAR :: Bug #12990 :: Issues with PEAR Upgrade News Item from 1/3/08

Creating Captcha Images with PHP and the GD Library

Friday, February 1st, 2008

A long time ago I wrote some code to create captcha images using PHP. The goal was to create similar or exact representations of the captcha’s used on Yahoo’s Overture. I really don’t have an answer why I chose their style except that it’s possible I liked the way they looked. I am sharing this code for creating the images but it’s up to you to create some logic in applying their use with HTML forms (I’ll give some hints at the end of this post).

This is the code to create the image. It returns an image resource identifier. Note the use of a custom TrueType font — you’ll need to change that line to the path of a font on your system.

<?php
// generate captcha image - returns image handle
function captcha_image($code_string, $img_width=150, $img_height=40) {
  // seed srand
  srand((double)microtime()*1000000);

  // create image
  $im = @imagecreate($img_width, $img_height) or die("Cannot Initialize new GD image stream");

  // security code
  $security_code = $code_string;

  // define font
  $font = "/usr/fonts/ttf/Georgia.ttf";

  // create some colors
  $black = imagecolorallocate($im, 0, 0, 0);
  $white = imagecolorallocate($im, 255, 255, 255);
  $grey = imagecolorallocate($im, 128, 128, 128);

  // randomness, we need lots of randomness :)
  // background color -> 1=black, 2=white, 3=grey (more colors can be added)
  // lines -> black bg (1=white or 2=grey), white bg (1=black or 2=grey), grey bg (black only)
  $randval = rand(1, 3);
  if ($randval == 1) {
    $bgcolor = $black;
    $fontcolor = $white;
    $linecolor = ((rand(0, 1) == 0) ? $black : $white);
  } elseif ($randval == 2) {
    $bgcolor = $white;
    $fontcolor = $black;
    $linecolor = ((rand(0, 1) == 0) ? $black : $white);
  } else {
    $bgcolor = $grey;
    $fontcolor = $black;
    $linecolor = ((rand(0, 1) == 0) ? $black : $grey);
  }

  // line positioning and increment
  $x_start = rand(0, 10);
  $x_size = rand(5, 10);
  $y_start = rand(0, 10);
  $y_size = rand(5, 10);

  // fill with background color
  imagefill($im, 0, 0, $bgcolor);

  // initial x position
  $font_x = 10;

  // write text
  for ($i = 0; $i < strlen($security_code); $i++) {
    // font size -> 20 to 35
    $font_size = rand(25, 35);
    // font angle -> -20 to +20
    $font_angle = rand(0, 20);
    if ($font_angle != 0) { if (rand(0, 1) == 0) { $font_angle = -$fone_angle; } }
    // font y position -> if font_size <= 27 then 30 to 35, if font_size > 27 then 30 to 35
    if ($font_size <= 27) { $font_y = rand(25, 30); } else { $font_y = rand(30, 35); }
    // write the text
    imagettftext($im, $font_size, $font_angle, $font_x, $font_y, $fontcolor, $font, $security_code{$i});
    // one more time to make it bolder
    imagettftext($im, $font_size, $font_angle, $font_x+1, $font_y+1, $fontcolor, $font, $security_code{$i});
    // next font x position
    $font_x += ($font_size + 5);
  }

  // draw horizontal lines
  for ($y = $y_start; $y < $img_height; $y += $y_size) {
    imageline($im, 0, $y, $img_width, $y, $linecolor);
  }
  // draw vertical lines
  for ($x = $x_start; $x < $img_width; $x += $x_size) {
    imageline($im, $x, 0, $x, $img_height, $linecolor);
  }

  // return captcha image handle
  return $im;
}
?>

We need a method for generating random four character strings when the captcha is created and displayed to the user. This function will do the trick.

<?php
function secret_key($length=4) {
  $salt = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
  srand((double)microtime()*1000000);
  $i = 0;
  $skey = "";
  while ($i < $length) {
    $num = rand() % strlen($salt);
    $tmp = substr($salt, $num, 1);
    $skey .= $tmp;
    $i++;
  }
  return $skey;
}
?>

With everything in place, we can generate the secret key, create the captcha, and finally display it to the user using this code.

<?php
// set headers
header("Content-type: image/png");
header("Cache-Control: no-cache");
header("Pragma: no-cache");

// generate secret
$skey = secret_key();

// create captcha and output to browser as PNG image
$im = captcha_image($skey);
@imagepng($im);
@imagedestroy($im);
?>

Here is an example of the code in action. The style looks very similar to the images used on Yahoo’s Overture.

Captcha Demo ยป Captcha

Now you know how to create a captcha, so what about verifying the input against the captcha value? This can be accomplished a variety of ways and everyone tends to have their preference.

  • One method is to save the secret key in a session variable. As the image is created, store the key in a session variable and once the form is submitted, check the user’s value against the one stored in the session. If they match, proceed but if they fail, return an error and don’t process the form data.
  • If you don’t want to use sessions, you could try using temp files. Store the key in a temp file and pass some value as a query string identifying to the script that the key is in that file. Read in the key and MD5 or SHA1 crypt the key and save it in a hidden form field. When the form is submitted, compare the hashed key against the user input (which you will also hash). Process the form data if the keys match.

You can download the provided source file. Use the PHP file as an image source in your HTML IMG tag.

<img src="http://www.yourdomain.com/captcha.php">

Don’t forget to edit the path to the TrueType font you want to use in the captcha_image function. Failure to do so will lead to missing image characters.

Source Files: captcha.zip

Searching for fields in unknown MySQL tables

Friday, November 2nd, 2007

A friend emailed me weeks ago asking if there is a SQL SELECT statement to search for fields in a database if the table names are unknown. As far as I knew there wasn’t and suggested creating a script to do the searching for her.

1. Here’s a code snippet to achieve a search for a single field across all tables in a specified database.

<?php
// variables
$dbname = 'my_db';
$dbfield = 'my_field';
// connect to database
$dblink = mysql_connect('localhost', 'user', 'password');
if ($dblink) {
  mysql_select_db($dbname, $dblink);
} else {
  die('Failed to connect to DB');
}
// get table list
$query = "show tables";
$result = mysql_query($query, $dblink);
// loop through table names
while (list($table) =  mysql_fetch_row($result)) {
  // get table description
  $query = "describe $table";
  $result2 = mysql_query($query, $dblink);
  // loop through table description fields
  while ($row = mysql_fetch_array($result2, MYSQL_ASSOC)) {
    // does this field match what we're looking for?
    if ($row['Field'] == $dbfield) {
      echo "Found '".$row['Field']."' in table => $table\n";
    }
  }
  mysql_free_result($result2);
}
mysql_free_result($result);
?>

2. This is a variant to achieve a search for an array of fields across all tables in a specified database.

<?php
// variables
$dbname = 'my_db';
$dbfields = array('my_field1','my_field2','my_field3');
// connect to database
$dblink = mysql_connect('localhost', 'user', 'password');
if ($dblink) {
  mysql_select_db($dbname, $dblink);
} else {
  die('Failed to connect to DB');
}
// get table list
$query = "show tables";
$result = mysql_query($query, $dblink);
// loop through table names
while (list($table) =  mysql_fetch_row($result)) {
  // get table description
  $query = "describe $table";
  $result2 = mysql_query($query, $dblink);
  // loop through table description fields
  while ($row = mysql_fetch_array($result2, MYSQL_ASSOC)) {
    // does this field match any in the array?
    if (in_array($row['Field'], $dbfields)) {
      echo "Found '".$row['Field']."' in table => $table\n";
    }
  }
  mysql_free_result($result2);
}
mysql_free_result($result);
?>

3. And finally, this version searches for an array of fields across all tables and databases in MySQL.

<?php
// variables
$dbfields = array('my_field1','my_field2','my_field3');
// connect to database
$dblink = mysql_connect('localhost', 'user', 'password');
if (!$dblink) {
  die('Failed to connect to DB');
}
// get databases list
$query = "show databases";
$result = mysql_query($query, $dblink);
// loop through table names
while (list($dbname) =  mysql_fetch_row($result)) {
  mysql_select_db($dbname);
  // get table list
  $query = "show tables";
  $result2 = mysql_query($query, $dblink);
  // loop through table names
  while (list($table) =  mysql_fetch_row($result2)) {
    // get table description
    $query = "describe $table";
    $result3 = mysql_query($query, $dblink);
    // loop through table description fields
    while ($row = mysql_fetch_array($result3, MYSQL_ASSOC)) {
      // does this field match what we're looking for?
      if (in_array($row['Field'], $dbfields)) {
        echo "Found '".$row['Field']."' in table => $table of database => $dbname\n";
      }
    }
    mysql_free_result($result3);
  }
  mysql_free_result($result2);
}
mysql_free_result($result);
?>

Be sure to change the variables $dbname, $dbfields, and the mysql_connect options before using these scripts. You can run them by typing

php -f /path/filename

on the command-line or from your web server. If run from the web, change the new line (\n) to a HTML line break (<br>).

Source Files: db-search-fields.zip

Custom SquirrelMail Login Pages for Virtual Hosts mini-HOWTO

Saturday, October 13th, 2007

Originally posted on August 8, 2006 9:57 PM

This little tutorial will show you how to create custom SquirrelMail login pages for many virtual hosts, or in plain English, you have one copy of SquirrelMail and host many web sites on a single server. The purpose is to provide a unique login page for each web site domain without installing multiple copies of SquirrelMail. This is very useful if you are running a webhosting business or you host your friend’s web sites (like I do) and you don’t want SquirrelMail branded with your name but theirs. If that isn’t a good enough reason then how about the fact that the default SquirrelMail login page is boring and you should jazz it up with a custom web site theme.

The following screenshots are examples of some webmail login pages on my server. They all use the same copy of SquirrelMail (version 1.4.6) but with my tweaks each domain has a custom login page.

XenoCafe WebMail Login Page PineappleNOW! WebMail Login Page Salina's WebMail Login Page

This walkthrough makes the assumption that you have HTML, PHP, and some graphic design skills. To edit the SquirrelMail files you should know how to copy and paste and understand PHP, but to create your own custom templates you’ll need to know HTML, PHP, and graphics. You should also have root access to your server or have the ability to upload the modified SquirrelMail files to your server. The other obvious assumption is that you have SquirrelMail 1.4.6 installed and configured on your server (using another version of SquirrelMail may work, but I don’t know how much their code changed between revisions). If you don’t have SquirrelMail installed then you can check out my SquirrelMail 1.4.6 tutorial on XenoCafe. With that said, let’s get started.

1. You should already know where SquirrelMail is installed on your server and have it configured for each domain that accesses it. For this howto I’ll use the example directory location of /www/webmail/html/ and the Apache virtual host file for webmail has a ServerAlias directive set for each domain (webmail.domain1.com, webmail.domain2.com, etc…). See my Apache Web Server tutorial for more information on setting up virtual hosts.

2. The way I accomplished custom webmail login pages was to create a directory in /www/webmail/html/ called hosts. This directory is where all your templates are stored for each custom webmail login page and their associated images. Start by creating a host directory in your webmail root directory (for clarification, this is the same top level directory where the SquirrelMail "src" directory exists).

cd /www/webmail/html/
mkdir hosts

3. There are three SquirrelMail PHP files that we’ll need to edit. The primary one is called global.php that resides in the functions directory. We’ll insert our custom function called show_login_page that makes custom logins possible. Let’s take a look at this login page function we’re creating.

function show_login_page( $errTitle="", $errString="" ) {

  $httphost = strtolower( $_SERVER["HTTP_HOST"] );

  switch ($httphost) {
    case "webmail.domain1.com":
      require( SM_PATH . "hosts/domain1/domain1.php" );
      break;

    case "domain2.com":
    case "www.domain2.com":
    case "webmail.domain2.com":
      include( SM_PATH . "hosts/domain2/domain2.php" );
      break;

    default:
      require( SM_PATH . "hosts/default/default.php" );
      break;
  }

}

This function accepts two arguments ($errTitle and $errString). You’ll learn about these later when we modify one of the other SquirrelMail files. $httphost contains the server name of which web site the user is visiting. This is acquired from the global $_SERVER["HTTP_HOST"] PHP variable. We use a switch/case structure to evaluate the HTTP HOST name. Depending on which host is visited the custom template for that host is shown for the login page. The host names are set up through Apache virtual hosts and DNS, in the case of the webmail subdomain. For www.domain.com and domain.com, it is assumed access to the site is coming from a /webmail directory. That can be established via symbolic links like so.

ln -s /www/webmail/html/ /www/domain/html/webmail

So webmail can be accessed from http://webmail.domain.com or from http://www.domain.com/webmail since both paths map to the same physical space. You’ll see the PHP include function being used once a host is matched. The include function inserts your custom login page’s code as part of the HTML output to the web browser. The path to the PHP page is within the hosts directory we created earlier. For each host you want to serve custom logins you’ll create a subdirectory within hosts (I use the domain name without the ending .com, .net, etc). You’ll store your custom login page and image files within that domain directory. The last part of the switch/case is the default login page. This is shown when there are no HTTP HOST matches and thus displays the default SquirrelMail login page.

Open up the functions/global.php file in a text editor and add the show_login_page function to the end of the file right above the ending ?> PHP tag. Customize the hosts for your server and save the file when you’re done.

4. Next we’ll edit the src/login.php page. We’re basically gonna strip out parts of the exsiting SquirrelMail code and replace it with a call to our show_login_page function. Make a backup copy of login.php and open the original in a text editor. Delete everything in the file and insert this code.

<?php

/**
 * login.php -- simple login screen
 *
 * Copyright (c) 1999-2006 The SquirrelMail Project Team
 * Licensed under the GNU GPL. For full terms see the file COPYING.
 *
 * This a simple login screen. Some housekeeping is done to clean
 * cookies and find language.
 *
 * @version $Id: login.php,v 1.98.2.11 2006/02/03 22:27:55 jervfors Exp $
 * @package squirrelmail
 */

/**
 * Path for SquirrelMail required files.
 * @ignore
 */
define("SM_PATH","../");

/* SquirrelMail required files. */
require_once(SM_PATH . "functions/strings.php");
require_once(SM_PATH . "config/config.php");
require_once(SM_PATH . "functions/i18n.php");
require_once(SM_PATH . "functions/plugin.php");
require_once(SM_PATH . "functions/constants.php");
require_once(SM_PATH . "functions/page_header.php");
require_once(SM_PATH . "functions/html.php");
require_once(SM_PATH . "functions/global.php");
require_once(SM_PATH . "functions/forms.php");

/**
 * $squirrelmail_language is set by a cookie when the user selects
 * language and logs out
 */
set_up_language($squirrelmail_language, TRUE, TRUE);

/**
 * Find out the base URI to set cookies.
 */
if (!function_exists("sqm_baseuri")){
    require_once(SM_PATH . "functions/display_messages.php");
}
$base_uri = sqm_baseuri();

/*
 * In case the last session was not terminated properly, make sure
 * we get a new one.
 */

sqsession_destroy();

header("Pragma: no-cache");

do_hook("login_cookie");

$loginname_value = (sqGetGlobalVar("loginname", $loginname) ? htmlspecialchars($loginname) : "");

if(sqgetGlobalVar("mailto", $mailto)) {
    $rcptaddress = "<input type=\"hidden\" name=\"mailto\" value=\"$mailto\" />\n";
} else {
    $rcptaddress = "";
}

show_login_page();

?>

Save your changes when you’re done and then we’ll move on to the last file.

5. The last file we edit is functions/display_messages.php. This file contains functions to display any messages (error or otherwise) on the screen, such as login failures, IMAP failures, etc. We need to change the logout_error function so when a login failure occurs that our custom page is shown instead of the default SquirrelMail version. Replace the logout_error function with this code.

function logout_error( $errString, $errTitle = "" ) {

    list($junk, $errString, $errTitle) = do_hook("logout_error", $errString, $errTitle);

    if ( $errTitle == "" ) {
        $errTitle = $errString;
    }

   show_login_page($errTitle, $errString);

}

If you were wondering about those arguments to our show_login_page function, now you can see what they’re for. SquirrelMail uses a hook to capture error messages on events. All we are doing is passing those error values to our login page function. When you intially go to the page, no errors are shown. If you login in and provide invalid information, our same login page will be shown again, but this time it’ll have the error messages within it. It’s similar to a postback but the URL is different. When we defined our login page function, we initialized those arguments with default null string values so no errors would show. When logout_error is called we pass the errors so they will be shown.

6. SquirrelMail’s PHP files are done so what’s left is to go over the steps of creating a custom login theme. I’ll now go over the parts you need to include when creating a template. Here is an example of the default template.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" type="text/css" href="../themes/css/verdana-10.css" />
<title>Domain - Login<?php echo (($errTitle != "" ) ? " - ".$errTitle : ""); ?></title><script language="JavaScript" type="text/javascript">
<!--
  function squirrelmail_loginpage_onload() {
    document.forms[0].js_autodetect_results.value = "<?php echo SMPREF_JS_ON; ?>";
    var textElements = 0;
    for (i = 0; i < document.forms[0].elements.length; i++) {
      if (document.forms[0].elements[i].type == "text" || document.forms[0].elements[i].type == "password" ) {
        textElements++;
        if (textElements == <?php echo (isset($loginname) ? 2 : 1); ?>) {
          document.forms[0].elements[i].focus();
          break;
        }
      }
    }
  }
// -->
</script>

<style type="text/css">
<!--
  /* avoid stupid IE6 bug with frames and scrollbars */
  body {
      voice-family: "\"}\"";
      voice-family: inherit;
      width: expression(document.documentElement.clientWidth - 30);
  }
-->
</style>

</head>

<body text="#000000" bgcolor="#ffffff" link="#0000cc" vlink="#0000cc" alink="#0000cc" onLoad="squirrelmail_loginpage_onload();">
<form action="redirect.php" method="post">
<table bgcolor="#ffffff" border="0" cellspacing="0" cellpadding="0" width="100%"><tr><td align="center"><center><img src="http://webmail.domain.com/images/domain_logo.gif" alt="Domain Logo" width="180" height="50" /><br />
<small>SquirrelMail version 1.4.6<br />
  By the SquirrelMail Project Team<br /></small>
<table bgcolor="#ffffff" border="0" width="350"><tr><td bgcolor="#dcdcdc" align="center"><b>Domain Login</b>
</td>
</tr>
<tr><td bgcolor="#ffffff" align="left">
<table bgcolor="#ffffff" align="center" border="0" width="100%"><tr><td align="right" width="30%">Name:</td>
<td align="left" width="*"><input type="text" name="login_username" value="<?php echo $loginname_value; ?>" />
</td>
</tr>

<tr><td align="right" width="30%">Password:</td>
<td align="left" width="*"><input type="password" name="secretkey" />
<input type="hidden" name="js_autodetect_results" value="<?php echo SMPREF_JS_OFF; ?>" />
<?php echo $rcptaddress; ?>
<input type="hidden" name="just_logged_in" value="1" />
</td>
</tr>
</table>
</td>
</tr>
<tr><td align="left"><center><input type="submit" value="Login" />
</center></td>
</tr>
</table>
</center>
<br><?php if ($errString != "" ) { echo "<center><font size=\"2\" face=\"Arial, Helvetica, sans-serif\" color=\"#ff0000\"><strong>$errString</strong></font></center>"; } ?>
</td>
</tr>
</table>
</form>
</body></html>

The code pieces in bold should be included in your template. I won’t go in depth over the code, that’s why I said you should know HTML and PHP.

I’ve included a zip file containing the altered PHP files and a couple templates to give you examples to follow when creating your own custom login pages. Be sure to read the README file included in the zip.

Source Files: sqmail-custom-logins.zip