Annotation of portolis/portolis.cgi, revision 1.4

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

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