Annotation of portolis/portolis.cgi, revision 1.3

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

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