{
    use esmith::config;
    use esmith::db;

    # Quote for Sieve string literals (escape " and \ for Sieve)
    sub sieve_quote {
        my ($s) = @_;
        $s //= '';
        $s =~ s/\\/\\\\/g;   # backslash -> double backslash
        $s =~ s/"/\\"/g;     # quote -> escaped quote
        return $s;
    }

    # Prepare a regex pattern for embedding in a Sieve string (legacy fallback)
    sub sieve_regex_quote_basic {
        my ($s) = @_;
        $s //= '';
        $s =~ s/([\[\]\.])/\\$1/g;  # make [ ] . literal in regex
        $s =~ s/\\/\\\\/g;          # escape backslashes for Sieve string
        $s =~ s/"/\\"/g;            # escape quotes for Sieve string
        return $s;
    }

    # Pass through a true regex pattern but escape for Sieve string literal.
    sub sieve_regex_passthrough {
        my ($s) = @_;
        $s //= '';
        $s =~ s/\\/\\\\/g;   # escape backslashes for Sieve string
        $s =~ s/"/\\"/g;     # escape quotes for Sieve string
        return $s;
    }

    # Normalize DB input for non-regex (contains) tests:
    # - remove user-added escapes for many common punctuation
    # - strip a leading "Subject.*" (case-insensitive) if present
    sub normalize_db_pattern {
        my ($s) = @_;
        $s //= '';
        $s =~ s/\\([\[\]\.\-\(\)\{\}\+\?\^\$\|])/$1/g; # unescape common punctuation
        $s =~ s/^\s*subject\s*\.\*\s*//i;              # drop leading Subject.*
        $s =~ s/^\s+|\s+$//g;                          # trim
        return $s;
    }

    # Simplify an email-like pattern to a plain substring for address tests.
    # Example: ".*user@domain\.tld" -> "user@domain.tld".
    sub simplify_email_value {
        my ($s) = @_;
        $s //= '';
        $s =~ s/^\s+|\s+$//g;
        return '' if $s eq '';
        $s =~ s/\.\*//g;   # remove wildcard segments
        # unescape common punctuation including @
        $s =~ s/\\([@\[\]\.\-\(\)\{\}\+\?\^\$\|])/$1/g;
        $s =~ s/^\s+|\s+$//g;
        return $s;
    }

    # Detect a full email address (simple, robust)
    sub extract_full_email_or_empty {
        my ($s) = @_;
        $s //= '';
        $s =~ s/^\s+|\s+$//g;
        return '' if $s eq '';
    
        # Extract embedded address from: "John Doe <alice@example.com>"
        if ($s =~ /<\s*([^@\s<>"',;]+@[^@\s<>"',;]+)\s*>/) {
            $s = $1;
        }
    
        # Validate as a bare email address
        return ($s =~ /^[^@\s<>"',;]+@[^@\s<>"',;]+$/) ? $s : '';
    }
    
    sub is_full_email {
        my ($s) = @_;
        $s //= '';
        $s =~ s/^\s+|\s+$//g;
        return '' if $s eq '';
    
        if ($s =~ /<\s*([^@\s<>"',;]+@[^@\s<>"',;]+)\s*>/) {
            return $1;
        }
    
        if ($s =~ /^[^@\s<>"',;]+@[^@\s<>"',;]+$/) {
            return $s;
        }
    
        return '';
    }
    
    # Pull out an email address maybe embedded in < >
    sub extract_email_address {
    my ($s) = @_;
    $s //= '';
    $s =~ s/^\s+|\s+$//g;
    return '' if $s eq '';

    # common angle-bracket / display-name format
    if ($s =~ /<\s*([^@\s<>"',;]+@[^@\s<>"',;]+)\s*>/) {
        return $1;
    }

    # already plain email
    if ($s =~ /^([^@\s<>"',;]+@[^@\s<>"',;]+)$/) {
        return $1;
    }

    return '';
}

    # Extract a domain from a simplified email-like string.
    # Returns '' if no clear domain found.
    sub extract_domain {
        my ($s) = @_;
        $s //= '';
        $s =~ s/^\s+|\s+$//g;
        return '' if $s eq '';
        if ($s =~ /@([^@\s<>"',;]+)/) {
            my $d = $1;
            $d =~ s/^[<"]+|[>"]+$//g;
            $d =~ s/[,"';].*$//;
            return lc $d;
        }
        # bare domain case (no @)
        if ($s =~ /^[A-Za-z0-9](?:[A-Za-z0-9\.\-]*[A-Za-z0-9])?\.[A-Za-z0-9\-]{2,}$/) {
            return lc $s;
        }
        return '';
    }

    # Build a robust Subject contains test:
    # - raw header :contains "Subject" original
    # - MIME-decoded header :mime :contains "Subject" underscore->space variant
    sub build_subject_contains_anyof {
        my ($p) = @_;
        my $p_us2sp = $p; $p_us2sp =~ s/_/ /g;
        return "anyof (header :contains \"Subject\" \"$p\", header :mime :contains \"Subject\" \"$p_us2sp\")";
    }

    # Build Subject+addresses fallback anyof clause
    sub build_subject_addr_contains_anyof {
        my ($p) = @_;
        my $p_us2sp = $p; $p_us2sp =~ s/_/ /g;
        return "anyof (header :contains \"Subject\" \"$p\", header :mime :contains \"Subject\" \"$p_us2sp\", address :all :contains [\"from\",\"to\",\"cc\"] \"$p\")";
    }

    my %processmail;
    tie %processmail, 'esmith::config', '/home/e-smith/db/processmail';

    # get users rules
    my @pmRules = ();
    foreach (sort keys %processmail)
    {
        push (@pmRules, $_)
            if (db_get_type(\%processmail, $_) eq $USERNAME);
    }

    # if they have rules add them to the template
    my $pmRules = @pmRules || '0';
    if ($pmRules > 0)
    {
      $OUT .= "\n";
      $OUT .= "# ----  user Sieve rules (".$pmRules.")------------------\n";

      my $pmRule;
      foreach $pmRule (sort @pmRules)
      {
        my $basis      = db_get_prop(\%processmail, $pmRule, "basis")      || '';
        my $criterion  = db_get_prop(\%processmail, $pmRule, "criterion")  || '';
        my $basis2     = db_get_prop(\%processmail, $pmRule, "basis2")     || '';
        my $secondtest = db_get_prop(\%processmail, $pmRule, "basis2")     || '';
        my $criterion2 = db_get_prop(\%processmail, $pmRule, "criterion2") || '';
        my $deliver    = db_get_prop(\%processmail, $pmRule, "deliver")    || '';
        my $deliver2   = db_get_prop(\%processmail, $pmRule, "deliver2")   || '';
        my $copy       = db_get_prop(\%processmail, $pmRule, "copy")       || '';
        my $action     = db_get_prop(\%processmail, $pmRule, "action")     || '';
        my $action2    = db_get_prop(\%processmail, $pmRule, "action2")    || '';

        # Normalize DB criteria (for contains/fallback paths)
        my $norm1 = normalize_db_pattern($criterion);
        my $norm2 = normalize_db_pattern($criterion2);

        # Prepare strings for contains tests
        my $crit1 = sieve_quote($norm1);
        my $crit2 = sieve_quote($norm2);

        # build condition 1
        my $cond1 = '';
        if ($basis eq '<' || $basis eq '>')
        {
          my $num = $criterion; $num =~ s/\s+//g;
          $cond1 = ($basis eq '<') ? "size :under $num" : "size :over $num";
        }
        elsif ($basis eq 'TO_')
        {
          $cond1 = "anyof (address :all :contains [\"to\",\"cc\",\"bcc\"] \"$crit1\")";
        }
        elsif ($basis eq 'headers')
        {
          # Use raw DB value to preserve regex meta (.*, [], \., etc.) for parsing
          my $raw = $criterion // '';
          my $h = $raw; $h =~ s/^\s+|\s+$//g;

          # Parse "HeaderName(s): value" (accept (From|To):... or \(From\|To\):...)
          if ($h =~ /^\s*\^?\s*(.*?)\s*:\s*(.*)$/s)
          {
            my ($names, $hv) = ($1, $2);
            $names =~ s/^\s*(?:\\?\()\s*//;  # optional leading ( or \(
            $names =~ s/\s*(?:\\?\))\s*$//;  # optional trailing ) or \)
            $names =~ s/\\\|/|/g;            # treat \| as |
            my @hn = split /\|/, $names;
            @hn = grep { defined $_ && $_ ne '' } @hn;

            if (@hn) {
              my %addr = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
              my $all_addr = 1; for my $n (@hn) { $all_addr &&= exists $addr{$n}; }
              my $hv_simple = simplify_email_value($hv);
              my $hv_domain = extract_domain($hv_simple);

              if ($all_addr) {
                my @lb = map { lc $_ } @hn;
                my $hn_list = join '","', @lb;
                if ($hv_simple = is_full_email($hv_simple)) {
                  my $addr_q = sieve_quote($hv_simple);
                  $cond1 = "address :is [\"$hn_list\"] \"$addr_q\"";
                } elsif ($hv_domain ne '') {
                  my $dom_q = sieve_quote($hv_domain);
                  $cond1 = "address :domain :is [\"$hn_list\"] \"$dom_q\"";
                } else {
                  # Fall back to true regex against listed headers
                  my @hn_q = map { sieve_quote($_) } @hn;
                  my $hn_list_q = join '","', @hn_q;
                  my $hv_re = sieve_regex_passthrough($hv);
                  $cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
                }
              } else {
                # Mixed/non-address headers: regex against the listed headers
                my @hn_q = map { sieve_quote($_) } @hn;
                my $hn_list_q = join '","', @hn_q;
                my $hv_re = sieve_regex_passthrough($hv);
                $cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
              }
            }
            else {
              # No valid header names extracted: prefer address match only if email/domain-like
              my $hv_simple = simplify_email_value($h);
              my $hv_domain = extract_domain($hv_simple);
              if ($hv_simple = is_full_email($hv_simple)) {
                my $aq = sieve_quote($hv_simple);
                $cond1 = "address :is [\"from\",\"to\",\"cc\"] \"$aq\"";
              } elsif ($hv_domain ne '') {
                my $dq = sieve_quote($hv_domain);
                $cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
              } else {
                my $p = sieve_quote($norm1);
                $cond1 = build_subject_addr_contains_anyof($p);
              }
            }
          }
          else
          {
            # No "Header: value" structure: prefer address match only if email/domain-like
            my $hv_simple = simplify_email_value($h);
            my $hv_domain = extract_domain($hv_simple);
            if ($hv_simple = is_full_email($hv_simple)) {
              my $aq = sieve_quote($hv_simple);
              $cond1 = "address :is [\"from\",\"to\",\"cc\"] \"$aq\"";
            } elsif ($hv_domain ne '') {
              my $dq = sieve_quote($hv_domain);
              $cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
            } else {
              my $p = sieve_quote($norm1);
              $cond1 = build_subject_addr_contains_anyof($p);
            }
          }
        }
        else
        {
          # Non-"headers" basis (explicit header names)
          if (lc($basis) eq 'subject') {
            my $p = $crit1;
            $cond1 = build_subject_contains_anyof($p);
          } else {
            my %addr = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
            if ($addr{$basis})
            {
              my $lb = lc $basis;           
              my $email = extract_email_address($crit1);
              if ($email ne '')
              {
                # Use the extracted bare email (best for matching)
                my $email_q = sieve_quote($email);
                $cond1 = "address :all :contains \"$lb\" \"$email_q\"";
              }
              else
              {
                # Fall back: crit1 wasn't an email, so treat it as generic text
                $cond1 = "address :all :contains \"$lb\" \"$crit1\"";
              }
            }          
          }
        }
        
        # Fallback for unknown/unsupported basis:
        # Must produce valid Sieve syntax that searches headers only.
        if ($cond1 eq '')
        {
          my $p_re = sieve_regex_quote_basic($crit1);
          $cond1 = 'header :regex ["^.*$"] "' . $p_re . '"';
        }

        # build condition 2 if present
        my $cond2 = '';
        if ($secondtest ne '')
        {
          if ($basis2 eq '<' || $basis2 eq '>')
          {
            my $num2 = $criterion2; $num2 =~ s/\s+//g;
            $cond2 = ($basis2 eq '<') ? "size :under $num2" : "size :over $num2";
          }
          elsif ($basis2 eq 'TO_')
          {
            $cond2 = "anyof (address :all :contains [\"to\",\"cc\",\"bcc\"] \"$crit2\")";
          }
          elsif ($basis2 eq 'headers')
          {
            my $raw2 = $criterion2 // '';
            my $hh = $raw2; $hh =~ s/^\s+|\s+$//g;

            if ($hh =~ /^\s*\^?\s*(.*?)\s*:\s*(.*)$/s)
            {
              my ($names2, $hv2) = ($1, $2);
              $names2 =~ s/^\s*(?:\\?\()\s*//;
              $names2 =~ s/\s*(?:\\?\))\s*$//;
              $names2 =~ s/\\\|/|/g;
              my @hn2 = split /\|/, $names2;
              @hn2 = grep { defined $_ && $_ ne '' } @hn2;

              if (@hn2) {
                my %addr2 = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
                my $all_addr2 = 1; for my $n2 (@hn2) { $all_addr2 &&= exists $addr2{$n2}; }
                my $hv2_simple = simplify_email_value($hv2);
                my $hv2_domain = extract_domain($hv2_simple);

                if ($all_addr2) {
                  my @lb2 = map { lc $_ } @hn2;
                  my $hn2_list = join '","', @lb2;
                  if ($hv2_simple = is_full_email($hv2_simple)) {
                    my $aq2 = sieve_quote($hv2_simple);
                    $cond2 = "address :is [\"$hn2_list\"] \"$aq2\"";
                  } elsif ($hv2_domain ne '') {
                    my $dq2 = sieve_quote($hv2_domain);
                    $cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\"";
                  } else {
                    my @hn2_q = map { sieve_quote($_) } @hn2;
                    my $hn2_list_q = join '","', @hn2_q;
                    my $hv2_re = sieve_regex_passthrough($hv2);
                    $cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
                  }
                } else {
                  my @hn2_q = map { sieve_quote($_) } @hn2;
                  my $hn2_list_q = join '","', @hn2_q;
                  my $hv2_re = sieve_regex_passthrough($hv2);
                  $cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
                }
              }
              else {
                # No valid header names: prefer address match only if email/domain-like
                my $hv2_simple = simplify_email_value($hh);
                my $hv2_domain = extract_domain($hv2_simple);
                if ($hv2_simple = is_full_email($hv2_simple)) {
                  my $aq2 = sieve_quote($hv2_simple);
                  $cond2 = "address :is [\"from\",\"to\",\"cc\"] \"$aq2\"";
                } elsif ($hv2_domain ne '') {
                  my $dq2 = sieve_quote($hv2_domain);
                  $cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
                } else {
                  my $p2 = sieve_quote($norm2);
                  $cond2 = build_subject_addr_contains_anyof($p2);
                }
              }
            }
            else
            {
              # No "Header: value" structure: prefer address match only if email/domain-like
              my $hv2_simple = simplify_email_value($hh);
              my $hv2_domain = extract_domain($hv2_simple);
              if ($hv2_simple = is_full_email($hv2_simple)) {
                my $aq2 = sieve_quote($hv2_simple);
                $cond2 = "address :is [\"from\",\"to\",\"cc\"] \"$aq2\"";
              } elsif ($hv2_domain ne '') {
                my $dq2 = sieve_quote($hv2_domain);
                $cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
              } else {
                my $p2 = sieve_quote($norm2);
                $cond2 = build_subject_addr_contains_anyof($p2);
              }
            }
          }
          else
          {
            if (lc($basis2) eq 'subject') {
              my $p2 = $crit2;
              $cond2 = build_subject_contains_anyof($p2);
            } else {
              my %addr2 = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
              if ($addr2{$basis2})
              {
                my $lb2 = lc $basis2;
                $cond2 = "address :all :contains \"$lb2\" \"$crit2\"";
              }
              else
              {
                $cond2 = "header :contains \"$basis2\" \"$crit2\"";
              }
            }
          }
        
          if ($cond2 eq '')
          {
            my $p2_re = sieve_regex_quote_basic($crit2);   # treat criterion2 as literal text
            $cond2 = 'header :regex ["^.*$"] "' . $p_re . '"';
          }
        }

        # mailbox names for sort/create (sanitize: turn "/" into "." to avoid invalid names)
        my $mb1 = $deliver;  $mb1  =~ s/"/\\"/g; $mb1 =~ s|/|.|g;
        my $mb2 = $deliver2; $mb2  =~ s/"/\\"/g; $mb2 =~ s|/|.|g;

        my $mbox1 = ($mb1 eq 'junkmail') ? "Junk" : "$mb1";
        my $mbox2 = ($mb2 eq 'junkmail') ? "Junk" : "$mb2";

        # begin rule
        $OUT .= "\n";
        $OUT .= "# User rule $pmRule\n";
        if ($cond2 ne '')
        {
          $OUT .= "if allof ($cond1, $cond2) \{\n";
        }
        else
        {
          $OUT .= "if $cond1 \{\n";
        }

        # actions
        if ($copy eq 'no')
        {
          if ($action eq 'sort' || $action eq 'create')
          {
            $OUT .= "  fileinto \"$mbox1\";\n";
            $OUT .= "  stop;\n";
          }
          elsif ($action eq 'forward')
          {
            my $addr = $deliver; $addr =~ s/"/\\"/g;
            $OUT .= "  redirect \"$addr\";\n";
            $OUT .= "  stop;\n";
          }
          elsif ($action eq 'delete')
          {
            $OUT .= "  discard;\n";
            $OUT .= "  stop;\n";
          }
          else
          {
            $OUT .= "  # unsupported action \"$action\"; keeping in INBOX\n";
            $OUT .= "  keep;\n";
            $OUT .= "  stop;\n";
          }
        }
        elsif ($copy eq 'yes' && $action2 eq 'inbox')
        {
          if ($action eq 'sort' || $action eq 'create')
          {
            $OUT .= "  fileinto :copy \"$mbox1\";\n";
            $OUT .= "  keep;\n";
            $OUT .= "  stop;\n";
          }
          elsif ($action eq 'forward')
          {
            my $addr = $deliver; $addr =~ s/"/\\"/g;
            $OUT .= "  redirect :copy \"$addr\";\n";
            $OUT .= "  keep;\n";
            $OUT .= "  stop;\n";
          }
          elsif ($action eq 'delete')
          {
            $OUT .= "  discard;\n";
            $OUT .= "  stop;\n";
          }
          else
          {
            $OUT .= "  # unsupported action \"$action\"; keeping in INBOX\n";
            $OUT .= "  keep;\n";
            $OUT .= "  stop;\n";
          }
        }
        else
        {
          # two deliveries (copy + second action)
          if ($action eq 'sort' || $action eq 'create')
          {
            $OUT .= "  fileinto :copy \"$mbox1\";\n";
          }
          elsif ($action eq 'forward')
          {
            my $addr = $deliver; $addr =~ s/"/\\"/g;
            $OUT .= "  redirect :copy \"$addr\";\n";
          }
          elsif ($action eq 'delete')
          {
            $OUT .= "  discard;\n";
          }
          else
          {
            $OUT .= "  # unsupported primary action \"$action\"\n";
          }

          if ($action2 eq 'sort')
          {
            $OUT .= "  fileinto \"$mbox2\";\n";
          }
          elsif ($action2 eq 'forward')
          {
            my $addr2 = $deliver2; $addr2 =~ s/"/\\"/g;
            $OUT .= "  redirect \"$addr2\";\n";
          }
          elsif ($action2 eq 'inbox')
          {
            $OUT .= "  keep;\n";
          }
          else
          {
            $OUT .= "  # unsupported secondary action \"$action2\"\n";
          }
          $OUT .= "  stop;\n";
        }
        $OUT .= "\}\n";
        $OUT .= "# End of User rule $pmRule\n";
      }#foreach rule
    }#if rules exist
}
