Annotation of portolis/portolis.cgi, revision 1.5

1.1       snw         1: #!/usr/pkg/bin/perl
                      2: 
                      3: #
                      4: # Portolis Mail Management Portal
                      5: #  Copyright (C) 2025 Coherent Logic Development LLC
                      6: #
1.5     ! snw         7: # $Id: portolis.cgi,v 1.4 2025/02/17 15:55:49 snw Exp $
1.1       snw         8: #  Author: Serena Willis <snw@coherent-logic.com>
                      9: #
                     10: # Licensed AGPL-3.0
                     11: #
1.2       snw        12: # $Log: portolis.cgi,v $
1.5     ! snw        13: # Revision 1.4  2025/02/17 15:55:49  snw
        !            14: # Fix password change form action URL
        !            15: #
1.4       snw        16: # Revision 1.3  2025/02/17 15:54:19  snw
                     17: # Add password change capability
                     18: #
1.3       snw        19: # Revision 1.2  2025/02/16 03:14:50  snw
                     20: # Add background image
                     21: #
1.2       snw        22: # Revision 1.1.1.1  2025/02/15 23:09:22  snw
                     23: # Initial commit
                     24: #
1.1       snw        25: #
                     26: 
                     27: use CGI qw(:standard);
                     28: use CGI::Session;
                     29: 
                     30: $session = CGI::Session->new();
                     31: 
                     32: sub init
                     33: {
                     34:     $cookie = cookie(CGISESSID => $session->id );
                     35:     print header(-cookie=>$cookie);
                     36: }
                     37: 
                     38: sub find_alias {
                     39:     my ($target, $alias) = @_;
                     40: 
                     41:     open(FH, '<', '/etc/postfix/virtual_aliases');
                     42: 
                     43:     while(<FH>) {
                     44:        my @entry = split(' ', $_);
                     45:        my $falias = $entry[0];
                     46:        my $ftarget = $entry[1];
                     47: 
                     48:        if(($ftarget eq $target) && ($falias eq $alias)) {
                     49:            close FH;
                     50:            return 1;
                     51:        }
                     52:     }
                     53: 
                     54:     close FH;
                     55:     return 0;
                     56: }
                     57: 
                     58: sub add_alias {
                     59:     my ($target, $alias) = @_;
                     60: 
                     61: }
                     62: 
                     63: sub vmail_auth {
                     64:     my ($auser, $apass) = @_;
                     65: 
                     66:     my $line = "";
                     67:     my @pwent = ();
                     68:     
                     69:     open(FH, "<", "/usr/pkg/etc/dovecot/users");
                     70: 
                     71:     while(<FH>){       
                     72:        $line = $_;
                     73:        @pwent = split(':', $line);
                     74:        if($pwent[0] eq $auser) {
                     75: 
                     76:            my $result = `/usr/pkg/bin/doveadm pw -t \'$pwent[1]\' -p \'$apass\'`;
                     77:            my @res = split(' ', $result);
                     78:            
                     79:            if($res[1] eq "(verified)") {
                     80:                close FH;
                     81:                return 1;
                     82:            }
                     83:        }
                     84:     }
                     85: 
                     86:     close FH;
                     87:     return 0;        
                     88: }
                     89: 
                     90: sub render_header {
                     91:     my ($title) = @_;
                     92: 
                     93:     my $navbar = '';
                     94:     my $html = <<"END_HDR";
                     95: <HTML>
                     96: <HEAD>
                     97: <TITLE>$title</TITLE>
                     98: </HEAD>
1.2       snw        99: <BODY BGCOLOR=PaleGoldenrod BACKGROUND=/images/linen2d.jpg>
1.1       snw       100: END_HDR
                    101: 
                    102:     print $html;
                    103:     
                    104:     if($session->param("~logged-in")) {
                    105:        my $email = $session->param("~email");
                    106:        $navbar = <<"END_NAVL";
1.3       snw       107: <A HREF=/cgi-bin/portolis.cgi?exec=home>$email</A> | <A HREF=https://webmail.coherent-logic.com>WebMail</A> | <A HREF=/cgi-bin/portolis.cgi?exec=pw>Change Password</A> | <A HREF=/cgi-bin/portolis.cgi?exec=logout>Log out</A>
1.1       snw       108: <HR>
                    109: END_NAVL
                    110:     }
                    111:     else {
                    112:        $navbar = <<"END_NAVO";
                    113: <A HREF=https://webmail.coherent-logic.com>WebMail</A> | <A HREF=/cgi-bin/portolis.cgi?exec=login>Log In</A>
                    114: <HR>
                    115: END_NAVO
                    116:     }
                    117: 
                    118:     print $navbar;
                    119:     
                    120: }
                    121: 
                    122: sub render_footer {
                    123: 
                    124:     my $html = <<'END_FTR';
                    125: <HR>
1.5     ! snw       126: <EM>$Id: portolis.cgi,v 1.4 2025/02/17 15:55:49 snw Exp $<BR>
1.1       snw       127: Copyright &copy; 2025 Coherent Logic Development LLC</EM>
                    128: </BODY>
                    129: </HTML>
                    130: END_FTR
                    131: 
                    132:     print $html;
                    133: }
                    134: 
                    135: sub list_aliases {
                    136:     my $email = $session->param("~email");
                    137:     open(FH, '<', '/etc/postfix/virtual_aliases');
                    138: 
                    139:     print "<TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>";
                    140:     print "<TR><TH>Alias</TH><TH>Actions</TH></TR>";
                    141:     while(<FH>) {
                    142:        my $ln = $_;
                    143:        my @line = split(' ', $ln);
                    144:        
                    145:        if($line[1] eq $email) {
                    146:            print "<TR><TD>$line[0]</TD><TD><A HREF=/cgi-bin/portolis.cgi?exec=delalias&id=$line[0]>Delete</A></TD></TR>";
                    147:        }
                    148:     }
                    149:     print "</TABLE>";
                    150:     print "<A HREF=/cgi-bin/portolis.cgi?exec=newalias>New Alias</A>";
                    151:     close FH;
                    152: }
                    153: 
                    154: sub exec_home {
                    155:     render_header "Home";
                    156:     my $email = $session->param("~email");
                    157:     my $html = <<"END_HOME";
                    158:     <CENTER>
                    159:     <H1>Account Overview</H1>
                    160:     <P>Your e-mail address is <B>$email</B></P>
                    161: END_HOME
                    162: 
                    163:     print $html;
                    164: 
                    165:     list_aliases();
                    166: 
                    167:     print "</CENTER>";
                    168: }
                    169: 
                    170: sub exec_newalias {
                    171:     my $html = '';
                    172: 
                    173:     if($session->param("~logged-in") == 0) {
                    174:        render_header "Access Denied";
                    175:        $html = <<"END_BADNAF";
                    176:        <H1>Access Denied</H1>
                    177:        <P>You are not logged in.</P>
                    178: END_BADNAF
                    179: 
                    180:        print $html;
                    181:        return;
                    182:     }    
                    183: 
                    184:     my $email = $session->param("~email");
                    185:     render_header "New Alias";
                    186:     
                    187:     if(request_method() eq 'GET') {
                    188:        $html = <<"END_NAF";
                    189:        <CENTER>
                    190:        <H1>Create New Alias</H1>
                    191:        <FORM METHOD=POST ACTION=/cgi-bin/portolis.cgi?exec=newalias>
                    192:        <TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>
                    193:        <TR>
                    194:        <TD><B>Alias:</B></TD>
                    195:        <TD><INPUT TYPE=TEXT NAME=alias></TD>
                    196:        </TR>
                    197:        <TR>
                    198:        <TD><B>Target:</B></TD>
                    199:        <TD>$email</TD>
                    200:        </TR>
                    201:        <TR>
                    202:        <TD COLSPAN=2 ALIGN=RIGHT>
                    203:        <INPUT TYPE=SUBMIT NAME=SUBMIT VALUE=Submit>
                    204:        </TD>
                    205:        </TR>
                    206:        </TABLE>
                    207:        </FORM>
                    208:        </CENTER>
                    209: END_NAF
                    210:     }
                    211:     else {
                    212:        my $proposed = param("alias");
                    213:        my $result = find_alias($email, $proposed);
                    214: 
                    215:        if($result == 0) {
                    216:            open(FH, '>>', '/etc/postfix/virtual_aliases');
                    217:            flock(FH, 2);
                    218:            print FH "$proposed\t$email\n";
                    219:            flock(FH, 8);
                    220:            close(FH);
                    221:            chdir '/etc/postfix';
                    222:            system '/usr/sbin/postmap virtual_aliases';
                    223:            $html = <<"END_AAOK";
                    224:            <CENTER>
                    225:            <H1>Alias Added</H1>
                    226:            <A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
                    227:            </CENTER>
                    228: END_AAOK
                    229:        }
                    230:        else {
                    231:            $html = <<"END_AADUP";
                    232:            <CENTER>
                    233:            <H1>Duplicate Alias</H1>
                    234:            <P>The alias <B>$proposed</B> already exists</P>
                    235:            <A HREF=/cgi-bin/portolis.cgi?exec=newalias>Try Again</a>
                    236:            </CENTER>
                    237: END_AADUP
                    238:        }
                    239:     }
                    240: 
                    241:     print $html;
                    242: 
                    243: }
                    244: 
                    245: sub exec_delalias {
                    246:     my $alias = url_param('id');
                    247:     my $html = '';
                    248:     
                    249:     if($session->param("~logged-in") == 0) {
                    250:        render_header "Access Denied";
                    251:        $html = <<"END_BADDAF";
                    252:        <H1>Access Denied</H1>
                    253:        <P>You are not logged in.</P>
                    254: END_BADDAF
                    255: 
                    256:        print $html;
                    257:        return;
                    258:     }
                    259:     
                    260:     my $email = $session->param("~email");
                    261:     render_header "Delete Alias";
                    262:     
                    263:     if(request_method() eq 'GET') {
                    264:        if(find_alias($email, $alias) == 1) {
                    265:            my @lines = ();
                    266: 
                    267:            open(FH, '<', '/etc/postfix/virtual_aliases');
                    268:            flock(FH, 2);
                    269:            while(<FH>) {
                    270:                my $line = $_;
                    271:                my @tmp = split(' ', $line);
                    272:                if($tmp[0] ne $alias) {
                    273:                    push @lines, $line;
                    274:                }
                    275:                if(($tmp[0] eq $alias) && ($tmp[1] ne $email)) {
                    276:                    push @lines, $line;
                    277:                }
                    278:            }
                    279:            flock(FH, 8);
                    280:            close(FH);
                    281:            open(FH, '>', '/etc/postfix/virtual_aliases');
                    282:            flock(FH, 2);
                    283:            seek(FH, 0, 0);
                    284:            truncate(FH, 0);
                    285:            print FH @lines;
                    286:            flock(FH, 8);
                    287:            close FH;
                    288:            chdir '/etc/postfix';
                    289:            system '/usr/sbin/postmap virtual_aliases';
                    290:            $html = <<"END_DAGOOD";
                    291:            <CENTER>
                    292:            <H1>Alias Deleted</H1>
                    293:            <P><A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
                    294:            </CENTER>
                    295: END_DAGOOD
                    296:        }
                    297:        else {
                    298:            $html = <<"END_DANOF";
                    299:            <CENTER>
                    300:            <H1>Invalid Alias</H1>
                    301:            <P><A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
                    302:            </CENTER>
                    303: END_DANOF
                    304:        }
                    305:     }
                    306:     
                    307:     print $html;
                    308:     
                    309: }
                    310: 
1.3       snw       311: sub exec_pw {
                    312: 
                    313:     if($session->param("~logged-in") == 0) {
                    314:        render_header "Access Denied";
                    315:        $html = <<"END_BADDAF";
                    316:        <H1>Access Denied</H1>
                    317:        <P>You are not logged in.</P>
                    318: END_BADDAF
                    319: 
                    320:        print $html;
                    321:        return;
                    322:     } 
                    323:     
                    324:     my $html = '';
                    325:     render_header "Change Password";    
                    326:     
                    327:     if(request_method() eq 'GET') {
                    328:        $html = <<"END_EPW";
                    329:        <CENTER>
                    330:        <H1>Change Password</H1>
1.4       snw       331:        <FORM METHOD=POST ACTION=/cgi-bin/portolis.cgi?exec=pw>
1.3       snw       332:         <TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>
                    333:        <TR>
                    334:        <TD><B>Password:</B></TD>
                    335:        <TD><INPUT TYPE=PASSWORD NAME=password></TD>
                    336:        </TR>
                    337:        <TR>
                    338:        <TD><B>Enter again to confirm:</B></TD>
                    339:        <TD><INPUT TYPE=PASSWORD NAME=password_confirm></TD>
                    340:        </TR>
                    341:        <TR>
                    342:        <TD COLSPAN=2 ALIGN=RIGHT>
                    343:        <INPUT TYPE=SUBMIT NAME=submit VALUE=Submit>
                    344:        </TD>
                    345:        </TR>
                    346:         </TABLE>
                    347:         </FORM>
                    348: END_EPW
                    349:     }
                    350:     elsif(request_method() eq 'POST') {
                    351:        my $password = param('password');
                    352:        my $password_confirm = param('password_confirm');
                    353:        my $email = $session->param("~email");
                    354:        my @parts = split('@', $email);
                    355:        my $localpart = $parts[0];
                    356:        my $domainpart = $parts[1];
                    357:        if($password eq $password_confirm) {
                    358:            my $hash = `/usr/pkg/bin/doveadm pw -p \"$password\"`;
                    359:            $hash =~s/\R//g;
                    360:            my $str = "$email:$hash:1007:1007:/home/maildeliverer/$domainpart/$localpart\n";
                    361: 
                    362:            open(FH, '<', '/usr/pkg/etc/dovecot/users');            
                    363:            flock FH, 2;
                    364: 
                    365:            my @lines = ();
                    366:            
                    367:            while(<FH>) {
                    368:                my $line = $_;
                    369:                my @pwent = split(':', $line);
                    370:                if($pwent[0] ne $email) {
                    371:                    push @lines, $line;
                    372:                }
                    373:            }
                    374:            push @lines, $str;
                    375: 
                    376:            flock FH, 8;
                    377:            close FH;
                    378: 
1.5     ! snw       379:            open(FH, '>', '/usr/pkg/etc/dovecot/users');
1.3       snw       380:            flock(FH, 2);
                    381:            seek(FH, 0, 0);
                    382:            truncate(FH, 0);
                    383:            print FH @lines;
                    384:            flock FH, 8;
                    385:            close(FH);      
                    386:            $html = <<"END_PWCPOST";
                    387:            <CENTER>
                    388:            <H1>Password Changed</H1>
                    389:            <A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
                    390:            </CENTER>
                    391: END_PWCPOST
                    392:            
                    393:        }
                    394:        else {
                    395:            $html = <<"END_PWCNM";
                    396:            <CENTER>
                    397:            <H1>Submission Error</H1>
                    398:            <P>Passwords did not match. <A HREF=/cgi-bin/portolis.cgi?exec=pw>Try again</A>.</P>
                    399:            </CENTER>
                    400: END_PWCNM
                    401:        }
                    402:     }
                    403: 
                    404:     print $html;
                    405:     
                    406: }
                    407: 
1.1       snw       408: sub exec_login {
                    409: 
                    410:     render_header "Login";
                    411:     
                    412:     my $html = '';
                    413:     
                    414:     if(request_method() eq 'GET') {
                    415:        $html = <<"END_LOGIN";
                    416: <CENTER>
                    417: <H1>Log In</H1>
                    418: <FORM METHOD=POST ACTION=/cgi-bin/portolis.cgi?exec=login>
                    419: <TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>
                    420: <tr>
                    421:   <td><b>E-Mail Address:</b></td>
                    422:   <td><input type="text" name="email"></td>
                    423: </tr>
                    424: <tr>
                    425:   <td><b>Password:</b></td>
                    426:   <td><input type="password" name="password"></td>
                    427: </tr>
                    428: <tr>
                    429:   <td colspan=2 align=right><input type=submit name=submit value=Submit></td>
                    430: </tr>
                    431: </TABLE>
                    432: </FORM>
                    433: </CENTER>
                    434: END_LOGIN
                    435:     }
                    436:     elsif(request_method() eq 'POST') {
                    437:        my $email = param("email");
                    438:        my $password = param("password");
                    439: 
                    440:        if(vmail_auth($email, $password) == 1) {
                    441:            $session->param("~logged-in", 1);
                    442:            $session->param("~email", $email);
                    443:            $html = <<'END_LOGINDONE';
                    444:                <script>location.replace('/cgi-bin/portolis.cgi?exec=home');</script>
                    445: 
                    446: END_LOGINDONE
                    447:        }
                    448:        else {
                    449:            $html = <<'END_ACD';
                    450: <H1>Access Denied</H1>
                    451:            <P>Invalid e-mail address or password</P>
                    452: END_ACD
                    453:        }
                    454:     }
                    455: 
                    456:     print $html;
                    457:        
                    458:        
                    459: }
                    460: 
                    461: sub exec_logout {
                    462:     $session->clear(['~logged-in', 'email']);
                    463:     my $html = <<'END_LO';
                    464:     <script>location.replace('/cgi-bin/portolis.cgi?exec=login');</script>
                    465: END_LO
                    466:     print $html;
                    467: }
                    468: 
                    469: sub exec_unknown {
                    470: 
                    471:     render_header "Error";
                    472:     
                    473:     my $html = <<'END_UNK';
                    474: <CENTER>
                    475:    <H1>Invalid Request</H1>
                    476: <P>The action you're attempting is invalid or has not yet been implemented. Please try something else.</P>
                    477: </CENTER>
                    478: END_UNK
                    479: 
                    480:     print $html;
                    481:     
                    482: }
                    483: 
                    484: sub main {    
                    485:     init;
                    486: 
                    487:     my $exec = url_param('exec');
                    488:     
                    489:     if($exec ne '') {  
                    490:        if($exec eq 'login') {
                    491:            $funcref = \&exec_login;
                    492:        }
                    493:        elsif($exec eq 'logout') {
                    494:            $funcref = \&exec_logout;
                    495:        }
                    496:        elsif($exec eq 'home') {
                    497:            $funcref = \&exec_home;
                    498:        }
                    499:        elsif($exec eq 'newalias') {
                    500:            $funcref = \&exec_newalias;
                    501:        }
                    502:        elsif($exec eq 'delalias') {
                    503:            $funcref = \&exec_delalias;
                    504:        }
1.3       snw       505:        elsif($exec eq 'pw') {
                    506:            $funcref = \&exec_pw;
                    507:        }
1.1       snw       508:        else {
                    509:            $funcref = \&exec_unknown;
                    510:        }
                    511:     }
                    512:     else {
                    513:        $funcref = \&exec_login;
                    514:     }
                    515: 
                    516:     &$funcref;
                    517:     
                    518:     render_footer;
                    519: }
                    520: 
                    521: main();
                    522: 
                    523: 

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>