Plugin Directory

Changeset 3333095


Ignore:
Timestamp:
07/23/2025 05:32:08 PM (7 months ago)
Author:
nexlifycreator
Message:

Feat(v1.0.3): Major update with AWS fixes, diagnostics, UI improvements, and enhanced security

  • Fix AWS connectivity issues for WorkMail and SES integration
  • Add comprehensive system diagnostic tools for troubleshooting
  • Improve mobile UI responsiveness and frontend design
  • Introduce advanced duplicate detection with semantic text analysis
  • Provide comprehensive uninstall data removal options for better security
  • Ensure complete cleanup of email credentials, IMAP settings, and OAuth data
  • Improve user control over data retention and removal during uninstall

This major release focuses on production readiness, enhanced security, improved user experience, and comprehensive data management capabilities.

Location:
nexlifydesk
Files:
354 added
26 edited

Legend:

Unmodified
Added
Removed
  • nexlifydesk/trunk/assets/css/nexlifydesk-admin.css

    r3330741 r3333095  
    1212    padding: 20px;
    1313    max-width: 100%;
     14    overflow-x: auto;
     15    min-width: 0;
    1416}
    1517
     
    6870    flex-direction: column;
    6971    gap: 24px;
     72    min-width: 0;
     73    overflow-wrap: break-word;
     74    word-wrap: break-word;
     75    word-break: break-word;
    7076}
    7177
     
    163169    padding: 12px;
    164170    width: 100%;
     171    min-width: 0;
     172    overflow-wrap: break-word;
     173    word-wrap: break-word;
     174    word-break: break-word;
    165175}
    166176.nexlifydesk-admin-single-ticket-ui .agent-message .message-content {
     
    188198    line-height: 1.6;
    189199    color: #374151;
     200    overflow-wrap: break-word;
     201    word-wrap: break-word;
     202    word-break: break-word;
    190203}
    191204
     
    200213    text-decoration: none;
    201214    word-break: break-all;
     215    overflow-wrap: break-word;
     216    word-wrap: break-word;
    202217}
    203218.nexlifydesk-admin-single-ticket-ui .message .attachments {
     
    13611376    font-style: italic;
    13621377}
     1378
  • nexlifydesk/trunk/assets/css/nexlifydesk.css

    r3326104 r3333095  
    790790}
    791791
    792 /* --- Frontend Ticket List Styles --- */
    793 .nexlifydesk-ticket-list-header {
     792/* --- Frontend Ticket List Styles (Table Design) --- */
     793.nexlifydesk-table-container {
     794    width: 100%;
     795    max-width: 1200px;
     796    margin: 20px auto;
     797    padding: 20px;
     798    background: #fff;
     799    border-radius: 8px;
     800    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
     801    font-family: Arial, sans-serif;
     802    color: #333;
     803    line-height: 1.6;
     804}
     805
     806/* Header */
     807.nexlifydesk-table-header {
    794808    display: flex;
    795809    justify-content: space-between;
    796810    align-items: center;
    797     flex-wrap: wrap;
    798     gap: 16px;
    799     margin-bottom: 32px;
    800 }
    801 .nexlifydesk-ticket-list-title {
    802     font-size: 2rem;
    803     font-weight: 700;
    804     color: #2d3748;
     811    margin-bottom: 20px;
     812}
     813
     814.nexlifydesk-table-header h1 {
     815    font-size: 24px;
    805816    margin: 0;
    806 }
    807 .nexlifydesk-ticket-list-desc {
    808     margin: 4px 0 0 0;
    809     color: #6c757d;
    810     font-size: 1rem;
    811 }
    812 .nexlifydesk-ticket-list-header__btn {
    813     padding: 12px 28px;
    814     font-size: 1rem;
    815     border-radius: 8px;
    816     background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
     817    color: #333;
     818}
     819
     820.nexlifydesk-header-actions {
     821    display: flex;
     822    gap: 10px;
     823}
     824
     825/* Buttons */
     826.nexlifydesk-btn-primary {
     827    padding: 8px 12px;
     828    background: #007bff;
    817829    color: #fff;
    818     font-weight: 600;
     830    border: none;
     831    border-radius: 4px;
     832    cursor: pointer;
    819833    text-decoration: none;
    820     box-shadow: 0 4px 16px rgba(44,83,100,0.08);
    821 }
    822 .nexlifydesk-ticket-list-header__btn:hover,
    823 .nexlifydesk-no-tickets__btn:hover {
    824     background: linear-gradient(135deg, #203a43, #2c5364, #0f2027);
     834    font-size: 14px;
     835    font-weight: normal;
     836    display: inline-block;
     837}
     838
     839.nexlifydesk-btn-primary:hover {
     840    background: #0056b3;
    825841    color: #fff;
    826 }
    827 
    828 .nexlifydesk-no-tickets {
     842    text-decoration: none;
     843}
     844
     845.nexlifydesk-view-btn {
     846    padding: 6px 10px;
     847    background: #007bff;
     848    color: #fff;
     849    border: none;
     850    border-radius: 4px;
     851    cursor: pointer;
     852    text-decoration: none;
     853    font-size: 14px;
     854    display: inline-block;
     855}
     856
     857.nexlifydesk-view-btn:hover {
     858    background: #0056b3;
     859    color: #fff;
     860    text-decoration: none;
     861}
     862
     863/* Table styles */
     864.nexlifydesk-ticket-table {
     865    width: 100%;
     866    border-collapse: collapse;
     867    margin-bottom: 20px;
     868}
     869
     870.nexlifydesk-ticket-table th,
     871.nexlifydesk-ticket-table td {
     872    padding: 12px;
     873    text-align: left;
     874    border-bottom: 1px solid #ddd;
     875}
     876
     877.nexlifydesk-ticket-table th {
     878    background: #f8f9fa;
     879    font-weight: bold;
     880    color: #333;
     881}
     882
     883.nexlifydesk-ticket-table tr:hover {
     884    background: #f1f1f1;
     885}
     886
     887.nexlifydesk-subject-cell {
     888    max-width: 300px;
     889}
     890
     891.nexlifydesk-ticket-preview {
     892    font-size: 12px;
     893    color: #666;
     894    margin-top: 4px;
     895    font-style: italic;
     896}
     897
     898/* Status badges */
     899.nexlifydesk-status {
     900    padding: 4px 8px;
     901    border-radius: 4px;
     902    font-size: 12px;
     903    color: #fff;
     904    font-weight: normal;
     905    text-transform: capitalize;
     906}
     907
     908.nexlifydesk-status.open {
     909    background: #28a745;
     910}
     911
     912.nexlifydesk-status.pending {
     913    background: #ffc107;
     914    color: #333;
     915}
     916
     917.nexlifydesk-status.in-progress {
     918    background: #ffc107;
     919    color: #333;
     920}
     921
     922.nexlifydesk-status.resolved {
     923    background: #28a745;
     924}
     925
     926.nexlifydesk-status.closed {
     927    background: #dc3545;
     928}
     929
     930/* Priority badges */
     931.nexlifydesk-priority {
     932    padding: 4px 8px;
     933    border-radius: 4px;
     934    font-size: 12px;
     935    color: #fff;
     936    font-weight: normal;
     937    text-transform: capitalize;
     938}
     939
     940.nexlifydesk-priority.low {
     941    background: #28a745;
     942}
     943
     944.nexlifydesk-priority.medium {
     945    background: #ffc107;
     946    color: #333;
     947}
     948
     949.nexlifydesk-priority.high {
     950    background: #fd7e14;
     951}
     952
     953.nexlifydesk-priority.urgent {
     954    background: #dc3545;
     955}
     956
     957/* No tickets state */
     958.nexlifydesk-no-tickets-table {
    829959    text-align: center;
    830960    padding: 60px 20px;
    831961    background: #f8f9fa;
    832     border-radius: 16px;
    833     box-shadow: 0 4px 24px rgba(44,83,100,0.06);
    834 }
    835 .nexlifydesk-no-tickets__icon {
    836     font-size: 56px;
     962    border-radius: 8px;
     963    border: 1px solid #ddd;
     964}
     965
     966.nexlifydesk-no-tickets-icon {
     967    font-size: 48px;
    837968    margin-bottom: 20px;
    838 }
    839 .nexlifydesk-no-tickets__title {
    840     font-size: 1.5rem;
    841     font-weight: 700;
    842     color: #2d3748;
    843     margin-bottom: 8px;
    844 }
    845 .nexlifydesk-no-tickets__desc {
    846     color: #6c757d;
    847     font-size: 1rem;
    848     margin-bottom: 24px;
    849 }
    850 .nexlifydesk-no-tickets__btn {
    851     padding: 12px 28px;
    852     font-size: 1rem;
    853     border-radius: 8px;
    854     background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
    855     color: #fff;
    856     font-weight: 600;
    857     text-decoration: none;
    858 }
    859 
    860 .nexlifydesk-ticket-list-grid {
    861     display: grid;
    862     grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    863     gap: 24px;
    864 }
    865 .nexlifydesk-ticket-card {
    866     background: #fff;
    867     border-radius: 16px;
    868     box-shadow: 0 4px 24px rgba(44,83,100,0.08);
    869     padding: 24px;
    870     display: flex;
    871     flex-direction: column;
    872     gap: 12px;
    873     border: 1px solid #e2e8f0;
    874     transition: box-shadow 0.2s;
    875 }
    876 .nexlifydesk-ticket-card:hover {
    877     box-shadow: 0 8px 32px rgba(44,83,100,0.14);
    878 }
    879 .nexlifydesk-ticket-card__top {
    880     display: flex;
    881     justify-content: space-between;
    882     align-items: flex-start;
    883     gap: 12px;
    884 }
    885 .nexlifydesk-ticket-card__id {
     969    opacity: 0.5;
     970}
     971
     972.nexlifydesk-no-tickets-table h3 {
     973    font-size: 20px;
     974    margin-bottom: 10px;
     975    color: #333;
     976}
     977
     978.nexlifydesk-no-tickets-table p {
     979    color: #666;
     980    margin-bottom: 20px;
     981}
     982
     983/* Footer */
     984.nexlifydesk-table-footer {
     985    text-align: center;
     986    color: #777;
    886987    font-size: 14px;
    887     color: #6c757d;
    888     font-weight: 500;
    889     background: #e9ecef;
    890     padding: 4px 12px;
    891     border-radius: 12px;
    892 }
    893 .nexlifydesk-ticket-card__status {
    894     padding: 6px 16px;
    895     border-radius: 20px;
    896     font-size: 14px;
    897     font-weight: 600;
    898     text-transform: uppercase;
    899     letter-spacing: 0.5px;
    900     border: 1px solid transparent;
    901 }
    902 .nexlifydesk-ticket-card__status.status-open { background: #e6fffa; color: #00b894; border-color: #00b894; }
    903 .nexlifydesk-ticket-card__status.status-pending { background: #fff3cd; color: #f39c12; border-color: #f39c12; }
    904 .nexlifydesk-ticket-card__status.status-resolved { background: #e8f5e8; color: #27ae60; border-color: #27ae60; }
    905 .nexlifydesk-ticket-card__status.status-closed { background: #f8f9fa; color: #6c757d; border-color: #6c757d; }
    906 
    907 .nexlifydesk-ticket-card__title {
    908     font-size: 1.1rem;
    909     font-weight: 700;
    910     color: #2d3748;
    911     margin: 8px 0 0 0;
    912 }
    913 .nexlifydesk-ticket-card__title a {
    914     color: #2d3748;
    915     text-decoration: none;
    916 }
    917 .nexlifydesk-ticket-card__title a:hover {
    918     text-decoration: underline;
    919 }
    920 .nexlifydesk-ticket-card__meta {
    921     display: flex;
    922     gap: 10px;
    923     align-items: center;
    924     flex-wrap: wrap;
    925 }
    926 .nexlifydesk-ticket-card__priority {
    927     padding: 6px 16px;
    928     border-radius: 20px;
    929     font-size: 14px;
    930     font-weight: 600;
    931     text-transform: uppercase;
    932     letter-spacing: 0.5px;
    933     border: 1px solid transparent;
    934 }
    935 .nexlifydesk-ticket-card__priority.priority-high { background: #ffe6e6; color: #e74c3c; border-color: #e74c3c; }
    936 .nexlifydesk-ticket-card__priority.priority-urgent { background: #e74c3c; color: #fff; border-color: #c0392b; }
    937 .nexlifydesk-ticket-card__priority.priority-medium { background: #fff3cd; color: #f39c12; border-color: #f39c12; }
    938 .nexlifydesk-ticket-card__priority.priority-low { background: #e8f5e8; color: #27ae60; border-color: #27ae60; }
    939 .nexlifydesk-ticket-card__created {
    940     font-size: 13px;
    941     color: #6c757d;
    942 }
    943 .nexlifydesk-ticket-card__actions {
    944     margin-top: 10px;
    945 }
    946 .nexlifydesk-ticket-card__actions .btn {
    947     padding: 10px 20px;
    948     border-radius: 8px;
    949     background: #f8f9fa;
    950     color: #2d3748;
    951     border: 1px solid #e2e8f0;
    952     font-weight: 600;
    953     text-decoration: none;
    954     font-size: 14px;
    955     transition: background 0.2s;
    956 }
    957 .nexlifydesk-ticket-card__actions .btn:hover {
    958     background: #e2e8f0;
    959 }
    960 
     988}
     989
     990.nexlifydesk-table-footer p {
     991    margin: 0;
     992}
     993
     994/* Responsive design */
    961995@media (max-width: 768px) {
    962     .nexlifydesk-ticket-list-header { flex-direction: column; align-items: flex-start; gap: 8px; }
    963     .nexlifydesk-ticket-list-title { font-size: 1.3rem; }
    964     .nexlifydesk-ticket-list-grid { grid-template-columns: 1fr; }
    965     .nexlifydesk-ticket-card { padding: 16px; }
    966 }
     996    .nexlifydesk-table-container {
     997        margin: 10px;
     998        padding: 15px;
     999    }
     1000   
     1001    .nexlifydesk-ticket-table,
     1002    .nexlifydesk-ticket-table thead,
     1003    .nexlifydesk-ticket-table tbody,
     1004    .nexlifydesk-ticket-table th,
     1005    .nexlifydesk-ticket-table td,
     1006    .nexlifydesk-ticket-table tr {
     1007        display: block;
     1008    }
     1009
     1010    .nexlifydesk-ticket-table thead tr {
     1011        display: none;
     1012    }
     1013
     1014    .nexlifydesk-ticket-table tr {
     1015        margin-bottom: 15px;
     1016        border: 1px solid #ddd;
     1017        border-radius: 4px;
     1018        padding: 10px;
     1019        background: #fff;
     1020    }
     1021
     1022    .nexlifydesk-ticket-table td {
     1023        border: none;
     1024        position: relative;
     1025        padding-left: 50%;
     1026        text-align: right;
     1027        padding-bottom: 8px;
     1028        padding-top: 8px;
     1029    }
     1030
     1031    .nexlifydesk-ticket-table td:before {
     1032        content: attr(data-label) ":";
     1033        position: absolute;
     1034        left: 10px;
     1035        width: 45%;
     1036        padding-right: 10px;
     1037        white-space: nowrap;
     1038        text-align: left;
     1039        font-weight: bold;
     1040        color: #333;
     1041    }
     1042
     1043    .nexlifydesk-table-header {
     1044        flex-direction: column;
     1045        align-items: flex-start;
     1046        gap: 15px;
     1047    }
     1048
     1049    .nexlifydesk-table-header h1 {
     1050        font-size: 20px;
     1051    }
     1052
     1053    .nexlifydesk-header-actions {
     1054        width: 100%;
     1055    }
     1056
     1057    .nexlifydesk-subject-cell {
     1058        max-width: none;
     1059    }
     1060
     1061    .nexlifydesk-ticket-preview {
     1062        margin-top: 5px;
     1063        text-align: right;
     1064    }
     1065}
     1066
     1067@media (max-width: 480px) {
     1068    .nexlifydesk-table-container {
     1069        margin: 5px;
     1070        padding: 10px;
     1071    }
     1072   
     1073    .nexlifydesk-table-header h1 {
     1074        font-size: 18px;
     1075    }
     1076
     1077    .nexlifydesk-ticket-table td {
     1078        padding-left: 45%;
     1079        font-size: 14px;
     1080    }
     1081
     1082    .nexlifydesk-ticket-table td:before {
     1083        width: 40%;
     1084        font-size: 12px;
     1085    }
     1086}
  • nexlifydesk/trunk/assets/js/nexlifydesk.js

    r3330741 r3333095  
    1818
    1919    $(document).ready(function() {
     20       
    2021        $('.page-title-action').on('click', function(e) {
    2122            e.preventDefault();
     
    700701    });
    701702
    702     $('#keep_data_on_uninstall').on('change', function() {
    703         if ($(this).is(':checked')) {
    704             $('#data-deletion-warning').slideUp();
    705         } else {
    706             $('#data-deletion-warning').slideDown();
    707         }
    708     });
    709 
    710     if (!$('#keep_data_on_uninstall').is(':checked')) {
    711         $('#data-deletion-warning').show();
    712     }
    713 
    714703})(jQuery);
    715704
     
    14151404    });
    14161405
     1406    $('#aws-diagnostics').on('click', function(){
     1407        var $btn = $(this);
     1408        $btn.data('diagnostics-bound', true);
     1409       
     1410        var $result = $('#aws-diagnostics-result');
     1411       
     1412        if ($result.length === 0) {
     1413            alert('Error: Diagnostics result container not found. Please refresh the page.');
     1414            return;
     1415        }
     1416       
     1417        if (!window.nexlifydesk_aws_test_nonce) {
     1418            $result.html('<p style="color: red;">❌ Security nonce not available. Please refresh the page.</p>');
     1419            return;
     1420        }
     1421       
     1422        $btn.prop('disabled', true).text('Running Diagnostics...');
     1423        $result.html('<p style="color: #666;">Running system diagnostics...</p>');
     1424       
     1425        $.post(ajaxurl, {
     1426            action: 'nexlifydesk_aws_diagnostics',
     1427            nonce: window.nexlifydesk_aws_test_nonce || ''
     1428        }).done(function(response) {
     1429            $btn.prop('disabled', false).text('System Diagnostics');
     1430            if (response.success && response.data.diagnostics) {
     1431                var html = '<div style="background: #f5f5f5; padding: 15px; border-radius: 5px; margin-top: 10px;">';
     1432                html += '<h4 style="margin-top: 0;">🔍 System Diagnostics Report</h4>';
     1433               
     1434                Object.keys(response.data.diagnostics).forEach(function(key) {
     1435                    var diag = response.data.diagnostics[key];
     1436                    var statusIcon = '';
     1437                    var statusColor = '';
     1438                   
     1439                    switch(diag.status) {
     1440                        case 'OK':
     1441                            statusIcon = '✅';
     1442                            statusColor = 'green';
     1443                            break;
     1444                        case 'FAILED':
     1445                            statusIcon = '❌';
     1446                            statusColor = 'red';
     1447                            break;
     1448                        case 'WARNING':
     1449                            statusIcon = '⚠️';
     1450                            statusColor = 'orange';
     1451                            break;
     1452                        default:
     1453                            statusIcon = 'ℹ️';
     1454                            statusColor = 'blue';
     1455                    }
     1456                   
     1457                    html += '<div style="margin: 8px 0; padding: 8px; background: white; border-radius: 3px;">';
     1458                    html += '<strong style="color: ' + statusColor + ';">' + statusIcon + ' ' + key.replace(/_/g, ' ').toUpperCase() + ':</strong> ';
     1459                    html += '<span style="color: #333;">' + diag.message + '</span>';
     1460                    html += '</div>';
     1461                });
     1462               
     1463                html += '<div style="margin-top: 15px; padding: 10px; background: #e3f2fd; border-radius: 3px; font-size: 12px; color: #1976d2;">';
     1464                html += '<strong>💡 Troubleshooting Tips:</strong><br>';
     1465                html += '• If IMAP extension is missing, contact your hosting provider<br>';
     1466                html += '• SSL/HTTPS is required for AWS WorkMail<br>';
     1467                html += '• Ensure your server can connect to AWS (check firewall/network)<br>';
     1468                html += '• Verify your AWS WorkMail credentials are correct';
     1469                html += '</div>';
     1470                html += '</div>';
     1471               
     1472                $result.html(html);
     1473            } else {
     1474                $result.html('<p style="color: red;">❌ Diagnostics failed to run.</p>');
     1475            }
     1476        }).fail(function() {
     1477            $btn.prop('disabled', false).text('System Diagnostics');
     1478            $result.html('<p style="color: red;">❌ Diagnostics request failed.</p>');
     1479        });
     1480    });
     1481
     1482    $(document).on('click', '#aws-diagnostics', function(e) {
     1483        if ($(this).data('diagnostics-bound')) {
     1484            return;
     1485        }
     1486       
     1487        var $btn = $(this);
     1488        var $result = $('#aws-diagnostics-result');
     1489       
     1490        if ($result.length === 0) {
     1491            alert('Error: Diagnostics result container not found. Please refresh the page.');
     1492            return;
     1493        }
     1494       
     1495        if (!window.nexlifydesk_aws_test_nonce) {
     1496            $result.html('<p style="color: red;">❌ Security nonce not available. Please refresh the page.</p>');
     1497            return;
     1498        }
     1499       
     1500        $btn.prop('disabled', true).text('Running Diagnostics...');
     1501        $result.html('<p style="color: #666;">Running system diagnostics...</p>');
     1502       
     1503        $.post(ajaxurl, {
     1504            action: 'nexlifydesk_aws_diagnostics',
     1505            nonce: window.nexlifydesk_aws_test_nonce || ''
     1506        }).done(function(response) {
     1507            $btn.prop('disabled', false).text('System Diagnostics');
     1508            if (response.success && response.data.diagnostics) {
     1509                var html = '<div style="background: #f5f5f5; padding: 15px; border-radius: 5px; margin-top: 10px;">';
     1510                html += '<h4 style="margin-top: 0;">🔍 System Diagnostics Report</h4>';
     1511               
     1512                Object.keys(response.data.diagnostics).forEach(function(key) {
     1513                    var diag = response.data.diagnostics[key];
     1514                    var statusIcon = '';
     1515                    var statusColor = '';
     1516                   
     1517                    switch(diag.status) {
     1518                        case 'OK':
     1519                            statusIcon = '✅';
     1520                            statusColor = 'green';
     1521                            break;
     1522                        case 'FAILED':
     1523                            statusIcon = '❌';
     1524                            statusColor = 'red';
     1525                            break;
     1526                        case 'WARNING':
     1527                            statusIcon = '⚠️';
     1528                            statusColor = 'orange';
     1529                            break;
     1530                        default:
     1531                            statusIcon = 'ℹ️';
     1532                            statusColor = 'blue';
     1533                    }
     1534                   
     1535                    html += '<div style="margin: 8px 0; padding: 8px; background: white; border-radius: 3px;">';
     1536                    html += '<strong style="color: ' + statusColor + ';">' + statusIcon + ' ' + key.replace(/_/g, ' ').toUpperCase() + ':</strong> ';
     1537                    html += '<span style="color: #333;">' + diag.message + '</span>';
     1538                    html += '</div>';
     1539                });
     1540               
     1541                html += '<div style="margin-top: 15px; padding: 10px; background: #e3f2fd; border-radius: 3px; font-size: 12px; color: #1976d2;">';
     1542                html += '<strong>💡 Troubleshooting Tips:</strong><br>';
     1543                html += '• If IMAP extension is missing, contact your hosting provider<br>';
     1544                html += '• SSL/HTTPS is required for AWS WorkMail<br>';
     1545                html += '• Ensure your server can connect to AWS (check firewall/network)<br>';
     1546                html += '• Verify your AWS WorkMail credentials are correct';
     1547                html += '</div>';
     1548                html += '</div>';
     1549               
     1550                $result.html(html);
     1551            } else {
     1552                $result.html('<p style="color: red;">❌ Diagnostics failed to run.</p>');
     1553            }
     1554        }).fail(function() {
     1555            $btn.prop('disabled', false).text('System Diagnostics');
     1556            $result.html('<p style="color: red;">❌ Diagnostics request failed.</p>');
     1557        });
     1558    });
     1559
    14171560    $('#test-google-connection').on('click', function(){
    14181561        var $btn = $(this);
  • nexlifydesk/trunk/email-source/nexlifydesk-email-pipe.php

    r3330879 r3333095  
    1414    $provider = isset($settings['provider']) ? $settings['provider'] : 'custom';
    1515
    16     // Check if IMAP extension is available for providers that need it
    1716    if (($provider === 'custom' || $provider === 'aws') && !extension_loaded('imap')) {
    18         // Add admin notice for users to see
    1917        add_action('admin_notices', function() {
    2018            if (current_user_can('manage_options')) {
     
    5452
    5553/**
    56  * Gets ticket by ticket_id string (e.g., "T1102")
    57  *
     54 * Get ticket by ticket ID
    5855 * @param string $ticket_id The ticket ID string
    5956 * @return object|null The ticket object if found, null otherwise
     
    6461
    6562/**
    66  * Clean email subject for better duplicate detection
     63 *
    6764 * Removes Re:, Fwd:, etc. prefixes and normalizes the subject
    6865 *
     
    9390    $table_name = $wpdb->prefix . 'nexlifydesk_tickets';
    9491
    95     // For non-registered users (user_id = 0)
    9692    if ($user_id == 0 && !empty($email)) {
    9793       
     
    166162
    167163function nexlifydesk_fetch_custom_emails() {
    168     // Check if IMAP extension is available
    169164    if (!extension_loaded('imap')) {
    170         // Add admin notice for users to see
    171165        add_action('admin_notices', function() {
    172166            if (current_user_can('manage_options')) {
     
    206200                $date = isset($overview->date) ? $overview->date : '';
    207201               
    208                 $subject = nexlifydesk_decode_email_content($subject);
     202                if (function_exists('nexlifydesk_decode_email_subject')) {
     203                    $subject = nexlifydesk_decode_email_subject($subject);
     204                } else {
     205                    $subject = nexlifydesk_decode_email_content($subject);
     206                }
    209207                $from = nexlifydesk_decode_email_content($from);
    210208               
     
    380378            $date = isset($overview->date) ? $overview->date : (isset($header->date) ? $header->date : '');
    381379           
    382             $subject = nexlifydesk_decode_email_content($subject);
     380            if (function_exists('nexlifydesk_decode_email_subject')) {
     381                $subject = nexlifydesk_decode_email_subject($subject);
     382            } else {
     383                $subject = nexlifydesk_decode_email_content($subject);
     384            }
    383385            $from = nexlifydesk_decode_email_content($from);
    384386           
     
    566568        return $email_content;
    567569    }
    568    
    569570    if (!empty($from_email) && nexlifydesk_is_admin_or_agent_email($from_email)) {
    570571        return $email_content;
    571572    }
    572573   
    573     $original_content = $email_content;
    574    
    575     if (preg_match('/^[A-Za-z0-9+\/=\s]+$/', $email_content) && base64_decode($email_content, true) !== false) {
    576         $decoded_base64 = base64_decode($email_content);
    577         if (strlen($decoded_base64) > strlen($email_content) * 0.5) {
    578             $email_content = $decoded_base64;
    579         }
    580     }
    581    
    582     if (strpos($email_content, '=') !== false) {
    583         if (preg_match('/=\r?\n/', $email_content) || preg_match('/=[0-9A-F]{2}/', $email_content)) {
    584             $email_content = quoted_printable_decode($email_content);
    585         }
    586     }
    587    
    588     if (strpos($email_content, '=?') !== false) {
    589         $email_content = preg_replace_callback('/=\?([^?]*)\?([BQ])\?([^?]*)\?=/', function($matches) {
     574    $body = str_replace(["\r\n", "\r"], "\n", $email_content);
     575    if (preg_match('/^[A-Za-z0-9+\/=\s]+$/', $body) && base64_decode($body, true) !== false) {
     576        $decoded_base64 = base64_decode($body);
     577        if (strlen($decoded_base64) > strlen($body) * 0.5) {
     578            $body = $decoded_base64;
     579        }
     580    }
     581    if (strpos($body, '=') !== false) {
     582        if (preg_match('/=\n/', $body) || preg_match('/=[0-9A-F]{2}/', $body)) {
     583            $body = quoted_printable_decode($body);
     584        }
     585    }
     586    if (strpos($body, '=?') !== false) {
     587        $body = preg_replace_callback('/=\?([^?]*)\?([BQ])\?([^?]*)\?=/', function($matches) {
    590588            $charset = $matches[1];
    591589            $encoding = strtoupper($matches[2]);
    592590            $text = $matches[3];
    593            
    594591            if ($encoding === 'B') {
    595592                $decoded = base64_decode($text);
     
    599596                return $matches[0];
    600597            }
    601            
    602598            if (strcasecmp($charset, 'UTF-8') !== 0) {
    603599                $decoded = mb_convert_encoding($decoded, 'UTF-8', $charset);
    604600            }
    605            
    606601            return $decoded;
    607         }, $email_content);
    608     }
    609    
    610     if (!mb_check_encoding($email_content, 'UTF-8')) {
    611         $email_content = mb_convert_encoding($email_content, 'UTF-8', 'auto');
    612     }
    613    
    614     $is_html = (stripos($email_content, '<html') !== false || stripos($email_content, '<body') !== false || strpos($email_content, '<') !== false);
    615    
     602        }, $body);
     603    }
     604    if (!mb_check_encoding($body, 'UTF-8')) {
     605        $body = mb_convert_encoding($body, 'UTF-8', 'auto');
     606    }
     607    $is_html = (stripos($body, '<html') !== false || stripos($body, '<body') !== false || strpos($body, '<') !== false);
    616608    if ($is_html) {
    617609        if (function_exists('mb_convert_encoding')) {
    618             $email_content = mb_convert_encoding($email_content, 'HTML-ENTITIES', 'UTF-8');
    619         }
    620        
    621         $email_content = preg_replace('/<br\s*\/?>/i', "\n", $email_content);
    622         $email_content = preg_replace('/<\/p>/i', "\n\n", $email_content);
    623         $email_content = preg_replace('/<\/div>/i', "\n", $email_content);
    624         $email_content = preg_replace('/<\/h[1-6]>/i', "\n\n", $email_content);
    625         $email_content = preg_replace('/<\/li>/i', "\n", $email_content);
    626         $email_content = preg_replace('/<hr\s*\/?>/i', "\n---\n", $email_content);
    627        
    628         $email_content = wp_strip_all_tags($email_content);
    629        
    630         $email_content = html_entity_decode($email_content, ENT_QUOTES, 'UTF-8');
    631     }
    632    
    633     $email_content = preg_replace('/\r\n|\r/', "\n", $email_content);
    634     $email_content = preg_replace('/\n{3,}/', "\n\n", $email_content);
    635     $lines = explode("\n", $email_content);
    636     $clean_lines = [];
    637     $found_reply_separator = false;
    638     $signature_start = -1;
    639    
    640     for ($i = 0; $i < count($lines); $i++) {
    641         $line = trim($lines[$i]);
    642        
    643         $strong_thread_indicators = [
    644             '/^-----Original Message-----/',
    645             '/^From:.*@.*/',
    646             '/^To:.*@.*/',
    647             '/^Sent:.*\d{4}/',
    648             '/^Date:.*\d{4}/',
    649             '/^On .+\d{4}.+at.+wrote:/',
    650             '/^>.+wrote:/',
    651             '/^________________________________/',
    652             '/^______________/',
    653             '/^-{3,}\s*On\s+.+wrote\s*-{3,}/',  // Matches "---- On [date] [name] wrote ---"
    654             '/^-{2,}\s*On\s+.+\d{4}.+wrote\s*-{2,}/',  // More flexible version
    655             '/^-{2,}\s*On\s+\w+,\s*\d{1,2}\s+\w+\s+\d{4}.+wrote\s*-{2,}/',  // "-- On Tue, 15 Jul 2025 ... wrote --"
    656             '/^On\s+\w+,\s*\d{1,2}\s+\w+\s+\d{4}.+wrote\s*:?$/',  // "On Tue, 15 Jul 2025 ... wrote:"
    657             '/^At\s+\d{1,2}:\d{2}.*on\s+\d{1,2}\/\d{1,2}\/\d{4}.*wrote:/',  // "At 08:44 on 15/07/2025 ... wrote:"
    658             '/^Le\s+\d{1,2}\/\d{1,2}\/\d{4}.*a\s+écrit\s*:/',  // French format
    659             '/^Am\s+\d{1,2}\.\d{1,2}\.\d{4}.*schrieb\s*:/',  // German format
    660             '/^Il\s+\d{1,2}\/\d{1,2}\/\d{4}.*ha\s+scritto\s*:/',  // Italian format
    661             '/^El\s+\d{1,2}\/\d{1,2}\/\d{4}.*escribió\s*:/',  // Spanish format
    662             '/^Em\s+\d{1,2}\/\d{1,2}\/\d{4}.*escreveu\s*:/',  // Portuguese format
    663             '/^\d{1,2}\/\d{1,2}\/\d{4}.*wrote\s*:/',  // Simple date format
    664             '/=+$/',  // Long line of equal signs
    665             '/-{10,}/',  // Long line of dashes
    666             '/_{10,}/',  // Long line of underscores
    667             '/^Reply above this line/',
    668             '/^Please reply above this line/',
    669             '/^Do not reply below this line/',
    670             '/^-----\s*Reply\s*above\s*this\s*line\s*-----/',
    671         ];
    672        
    673         foreach ($strong_thread_indicators as $pattern) {
    674             if (preg_match($pattern, $line)) {
    675                 $found_reply_separator = true;
    676                 $signature_start = $i;
    677                 break 2;
    678             }
    679         }
    680        
    681         if ($i > 10 && count($clean_lines) > 5) {
    682             $signature_patterns = [
    683                 '/^--$/',
    684                 '/^---$/',
    685                 '/^Best regards,?$/i',
    686                 '/^Thank you,?$/i',
    687                 '/^Sincerely,?$/i',
    688                 '/^Kind regards,?$/i',
    689             ];
    690            
    691             foreach ($signature_patterns as $pattern) {
    692                 if (preg_match($pattern, $line)) {
    693                     $signature_start = $i;
    694                     break 2;
    695                 }
    696             }
    697         }
    698     }
    699    
    700     for ($i = 0; $i < count($lines); $i++) {
    701         if ($signature_start !== -1 && $i >= $signature_start) {
     610            $body = mb_convert_encoding($body, 'HTML-ENTITIES', 'UTF-8');
     611        }
     612        $body = preg_replace('/<br\s*\/?>(?i)/', "\n", $body);
     613        $body = preg_replace('/<\/p>(?i)/', "\n\n", $body);
     614        $body = preg_replace('/<\/div>(?i)/', "\n", $body);
     615        $body = preg_replace('/<\/h[1-6]>(?i)/', "\n\n", $body);
     616        $body = preg_replace('/<\/li>(?i)/', "\n", $body);
     617        $body = preg_replace('/<hr\s*\/?>(?i)/', "\n---\n", $body);
     618        $body = wp_strip_all_tags($body);
     619        $body = html_entity_decode($body, ENT_QUOTES, 'UTF-8');
     620    }
     621    $body = preg_replace('/\n{3,}/u', "\n\n", $body);
     622    $header_pattern = '/^On[\s\S]+?wrote:\s*$/mus';
     623    $reply_separator_regexes = [
     624        $header_pattern,
     625        '/^Am[\s\S]+?schrieb:\s*$/mus',
     626        '/^Le[\s\S]+?écrit\s*:/mus',
     627        '/^Il[\s\S]+?scritto\s*:/mus',
     628        '/^El[\s\S]+?escribió\s*:/mus',
     629        '/^Em[\s\S]+?escreveu\s*:/mus',
     630        '/^At[\s\S]+?wrote:\s*$/mus',
     631        '/^From:[\s\S]+?$/mus',
     632        '/^Sent:[\s\S]+?$/mus',
     633        '/^Date:[\s\S]+?$/mus',
     634        '/^To:[\s\S]+?$/mus',
     635        '/^Subject:[\s\S]+?$/mus',
     636        '/^Reply above this line$/mus',
     637        '/^Please reply above this line$/mus',
     638        '/^Do not reply below this line$/mus',
     639        '/^-----Original Message-----$/mus',
     640        '/^-{2,}\s*On[\s\S]+wrote\s*-{2,}$/mus',
     641        '/=+$/mus',
     642        '/-{10,}/u',
     643        '/_{10,}/u',
     644    ];
     645    $separator_pos = false;
     646    foreach ($reply_separator_regexes as $regex) {
     647        if (preg_match($regex, $body, $match, PREG_OFFSET_CAPTURE)) {
     648            $separator_pos = $match[0][1];
    702649            break;
    703650        }
    704        
    705         $line = $lines[$i];
    706         $trimmed_line = trim($line);
    707        
    708         $is_thread_line = false;
    709         foreach ($strong_thread_indicators as $pattern) {
    710             if (preg_match($pattern, $trimmed_line)) {
    711                 $is_thread_line = true;
    712                 break;
    713             }
    714         }
    715        
    716         if ($is_thread_line) {
    717             break;
    718         }
    719        
    720         if (preg_match('/^>\s*/', $line)) {
    721             if (stripos($line, 'wrote:') !== false ||
    722                 stripos($line, 'original message') !== false ||
    723                 stripos($line, 'from:') !== false ||
    724                 stripos($line, 'sent:') !== false ||
    725                 stripos($line, 'date:') !== false ||
    726                 stripos($line, 'to:') !== false ||
    727                 preg_match('/\d{4}/', $line)) { 
    728                 break;
    729             }
    730         }
    731        
    732         if (preg_match('/^(From|To|Sent|Date|Subject|CC|BCC):\s*/', $trimmed_line)) {
    733             break;
    734         }
    735        
    736         if (preg_match('/^(Sent from my iPhone|Sent from my iPad|Sent from my Android|Get Outlook for)/', $trimmed_line)) {
    737             break;
    738         }
    739        
    740         $clean_lines[] = $line;
    741     }
    742    
    743     $clean_content = implode("\n", $clean_lines);
    744     $clean_content = preg_replace('/\n\s*\n\s*\n/', "\n\n", $clean_content);
    745     $clean_content = trim($clean_content);
    746    
    747     if (strlen($clean_content) < (strlen($original_content) * 0.3) && strlen($original_content) > 100) {
    748        
    749         $conservative_lines = explode("\n", $email_content);
    750         $preserved_lines = [];
    751        
    752         foreach ($conservative_lines as $line) {
    753             $trimmed_line = trim($line);
    754            
    755             $is_thread_indicator = false;
    756             foreach ($strong_thread_indicators as $pattern) {
    757                 if (preg_match($pattern, $trimmed_line)) {
    758                     $is_thread_indicator = true;
    759                     break;
    760                 }
    761             }
    762            
    763             if ($is_thread_indicator) {
    764                 break;
    765             }
    766            
    767             if (preg_match('/^>\s*/', $line) &&
    768                 (stripos($line, 'wrote:') !== false ||
    769                  stripos($line, 'original message') !== false ||
    770                  stripos($line, 'from:') !== false ||
    771                  stripos($line, 'sent:') !== false ||
    772                  stripos($line, 'date:') !== false)) {
    773                 break;
    774             }
    775            
    776             if (preg_match('/^(From|To|Sent|Date|Subject|CC|BCC):\s*/', $trimmed_line)) {
    777                 break;
    778             }
    779            
    780             $preserved_lines[] = $line;
    781         }
    782        
    783         $clean_content = implode("\n", $preserved_lines);
    784         $clean_content = preg_replace('/\n\s*\n\s*\n/', "\n\n", $clean_content);
    785         $clean_content = trim($clean_content);
    786     }
    787    
     651    }
     652    if ($separator_pos !== false) {
     653        $body = substr($body, 0, $separator_pos);
     654    }
     655    $body = preg_replace('/^\s*(>|\|).*/m', '', $body);
     656    $body = preg_replace('/\n{3,}/u', "\n\n", $body);
     657    $clean_content = trim($body);
    788658    if (empty($clean_content)) {
    789659        $clean_content = '[Customer replied via email]';
    790660    }
    791    
    792661    return $clean_content;
    793662}
    794663
    795664/**
    796  * Safe password retrieval with fallback
    797  * Handles both encrypted and plain text passwords
     665 * Safe password retrieval with strict encryption enforcement
     666 * Only returns decrypted passwords, never plain text
    798667 */
    799668function nexlifydesk_get_safe_password($encrypted_password) {
     
    802671    }
    803672   
     673    // Only proceed if password is properly encrypted
    804674    if (nexlifydesk_is_encrypted($encrypted_password)) {
    805675        $decrypted = nexlifydesk_decrypt($encrypted_password);
     
    808678        }
    809679    }
    810    
    811     return $encrypted_password;
    812 }
    813 
    814 /**
    815  * Extract email body content from IMAP message with proper encoding handling
    816  *
     680    // If not encrypted or decryption fails, return empty string
     681    return '';
     682}
     683
     684/**
    817685 * @param resource $inbox IMAP connection resource
    818686 * @param int $message_number Message number
     
    829697            $part_number = $i + 1;
    830698           
    831             if ($part->type == 0) { // TEXT
     699            if ($part->type == 0) {
    832700                $body = imap_fetchbody($inbox, $message_number, $part_number);
    833701               
     
    899767
    900768/**
    901  * Convert HTML email content to plain text
    902769 *
    903770 * @param string $html_content The HTML content to convert
  • nexlifydesk/trunk/email-source/providers/aws-ses/aws-handler.php

    r3330751 r3333095  
    22
    33if (!defined('ABSPATH')) exit;
     4
     5// Ensure helpers are available for email decoding functions
     6if (!function_exists('nexlifydesk_decode_email_subject')) {
     7    require_once dirname(__FILE__) . '/../../includes/helpers.php';
     8}
    49
    510/**
     
    1217 */
    1318function nexlifydesk_fetch_aws_emails() {
    14     // Check if IMAP extension is available
    15     if (!extension_loaded('imap')) {
     19    // Check if IMAP extension is available with better detection
     20    if (!extension_loaded('imap') || !function_exists('imap_open')) {
    1621       
    1722        add_action('admin_notices', function() {
    1823            if (current_user_can('manage_options')) {
    1924                echo '<div class="notice notice-error"><p><strong>NexlifyDesk:</strong> ' .
    20                      esc_html__('IMAP extension is not installed on this server. AWS WorkMail email piping is not available. Please contact your hosting provider to enable the PHP IMAP extension.', 'nexlifydesk') .
     25                     esc_html__('IMAP extension is not properly installed or configured on this server. AWS WorkMail email piping is not available. Please contact your hosting provider to enable the PHP IMAP extension with all required functions.', 'nexlifydesk') .
    2126                     '</p></div>';
    2227            }
     
    6772    $mailbox = "{{$imap_host}:{$imap_port}/imap/{$encryption}}INBOX";
    6873   
    69     $inbox = @imap_open($mailbox, $email, $password);
     74    // Clear any previous IMAP errors
     75    if (function_exists('imap_errors')) {
     76        imap_errors();
     77    }
     78    if (function_exists('imap_alerts')) {
     79        imap_alerts();
     80    }
     81   
     82    $inbox = @imap_open($mailbox, $email, $password, OP_SILENT);
    7083    if (!$inbox) {
    7184        return;
     85    }
     86   
     87    $mailbox_info = imap_mailboxmsginfo($inbox);
     88    if ($mailbox_info) {
     89    } else {
     90        $total_msgs = imap_num_msg($inbox);
    7291    }
    7392   
     
    85104    $emails = imap_search($inbox, $search_criteria);
    86105   
     106    // If no emails found with date criteria, try just UNSEEN to see if there are any unread emails at all
     107    if (!$emails) {
     108        $emails_unseen_only = imap_search($inbox, "UNSEEN");
     109        if ($emails_unseen_only) {
     110            // For now, let's process all unseen emails to solve the immediate issue
     111            $emails = $emails_unseen_only;
     112        } else {
     113            // IMAP search is failing, let's manually check all messages
     114            $total_msgs = imap_num_msg($inbox);
     115           
     116            $manually_found = [];
     117            for ($i = 1; $i <= $total_msgs; $i++) {
     118                $overview = imap_fetch_overview($inbox, $i, 0);
     119                if (!empty($overview) && isset($overview[0])) {
     120                    $msg = $overview[0];
     121                    // Check if message is unread (not seen)
     122                    if (!isset($msg->seen) || $msg->seen == 0) {
     123                        $manually_found[] = $i;
     124                    }
     125                }
     126            }
     127           
     128            if (!empty($manually_found)) {
     129                $emails = $manually_found;
     130            }
     131        }
     132    }
     133   
    87134    if (!$emails) {
    88135        imap_close($inbox);
     
    107154        $from = $header->fromaddress ?? $overview->from;
    108155        $subject = $overview->subject ?? '(No Subject)';
     156       
     157        // Decode MIME-encoded subject to fix encoding issues like "=?UTF-8?Q?..."
     158        if (function_exists('nexlifydesk_decode_email_subject')) {
     159            $subject = nexlifydesk_decode_email_subject($subject);
     160        }
    109161       
    110162        $parsed_from = function_exists('nexlifydesk_parse_email_from') ?
     
    148200        }
    149201       
    150         $email_date = isset($overview->date) ? strtotime($overview->date) : 0;
    151         if ($email_date < $fetch_start_time) {
    152             imap_setflag_full($inbox, $email_number, "\\Seen");
    153             continue;
    154         }
    155        
    156202        if (function_exists('nexlifydesk_should_block_email') &&
    157203            nexlifydesk_should_block_email($email_address, $subject, $message, $settings)) {
     
    166212        $user_id = $user ? $user->ID : 0;
    167213       
    168         // Check rate limit for this email address
    169214        if (class_exists('NexlifyDesk_Rate_Limiter') && NexlifyDesk_Rate_Limiter::is_rate_limited($user_id, $email_address)) {
    170            
    171             // Mark email as processed to prevent reprocessing
    172215            imap_setflag_full($inbox, $email_number, "\\Seen");
    173216            if ($delete_emails) {
     
    195238            ];
    196239           
    197             // Use the email-specific duplicate detection function
    198240            if (function_exists('nexlifydesk_check_email_duplicate')) {
    199241                $existing_ticket = nexlifydesk_check_email_duplicate($ticket_data);
    200242            } else {
    201                 // Fallback to main duplicate detection
    202243                $existing_ticket = NexlifyDesk_Tickets::check_for_duplicate_ticket($ticket_data);
    203244            }
     
    318359    }
    319360   
    320     /**
    321      * Check if AWS credentials are properly configured
    322      */
     361    // Check if the connection is ready for AWS SES
    323362    public function is_connection_ready() {
    324363        $is_ssl_enabled = $this->check_ssl_enabled();
     
    358397     */
    359398    public function test_imap_connection() {
     399        if (!extension_loaded('imap') || !function_exists('imap_open')) {
     400            return new WP_Error('imap_not_available', 'IMAP extension is not properly installed or configured on this server. Please contact your hosting provider to enable the PHP IMAP extension with all required functions.');
     401        }
     402       
    360403        $is_ssl_enabled = $this->check_ssl_enabled();
    361404       
     
    376419        $mailbox = "{{$imap_host}:993/imap/ssl}INBOX";
    377420       
    378         $inbox = @imap_open($mailbox, $email, $password);
     421        if (function_exists('imap_errors')) {
     422            imap_errors();
     423        }
     424        if (function_exists('imap_alerts')) {
     425            imap_alerts();
     426        }
     427       
     428        $inbox = @imap_open($mailbox, $email, $password, OP_READONLY | OP_SILENT);
    379429       
    380430        if (!$inbox) {
    381             $error = imap_last_error();
     431            $error = 'Unknown connection error';
     432           
     433            if (function_exists('imap_last_error')) {
     434                $last_error = imap_last_error();
     435                if ($last_error) {
     436                    $error = $last_error;
     437                }
     438            }
     439           
    382440            return new WP_Error('connection_failed', "AWS WorkMail IMAP connection failed: {$error}");
    383441        }
  • nexlifydesk/trunk/email-source/providers/google/google-handler.php

    r3330751 r3333095  
    22
    33if (!defined('ABSPATH')) exit;
     4
     5// Ensure helpers are available for email decoding functions
     6if (!function_exists('nexlifydesk_decode_email_subject')) {
     7    require_once dirname(__FILE__) . '/../../includes/helpers.php';
     8}
    49
    510/**
     
    1722    }
    1823
    19     // Verify OAuth state parameter for security
    2024    if (isset($_GET['state'])) {
    2125        $state = sanitize_text_field(wp_unslash($_GET['state']));
     
    118122    }
    119123
    120     // Generate a state parameter for security
    121124    $state = wp_generate_password(32, false);
    122     set_transient('nexlifydesk_google_oauth_state', $state, 600); // 10 minutes
     125    set_transient('nexlifydesk_google_oauth_state', $state, 600);
    123126
    124127    $redirect_uri = admin_url('admin.php?action=nexlifydesk_google_oauth_callback');
     
    214217        $from_header = array_values(array_filter($headers, fn($h) => $h['name'] === 'From'))[0]['value'] ?? '';
    215218        $subject_header = array_values(array_filter($headers, fn($h) => $h['name'] === 'Subject'))[0]['value'] ?? '(No Subject)';
     219
     220        // Decode MIME-encoded subject to fix encoding issues like "=?UTF-8?Q?..."
     221        if (function_exists('nexlifydesk_decode_email_subject')) {
     222            $subject_header = nexlifydesk_decode_email_subject($subject_header);
     223        }
    216224
    217225        $parsed_from = function_exists('nexlifydesk_parse_email_from') ?
     
    437445
    438446/**
    439  * Marks a Gmail message as read without deleting it.
    440447 *
    441448 * @param string $message_id The ID of the message to modify.
     
    452459
    453460/**
    454  * Marks a Gmail message as read and moves it to trash.
    455461 *
    456462 * @param string $message_id The ID of the message to modify.
  • nexlifydesk/trunk/includes/class-nexlifydesk-admin.php

    r3330879 r3333095  
    14961496                'default_priority' => isset($_POST['default_priority']) ? sanitize_text_field(wp_unslash($_POST['default_priority'])) : '',
    14971497                'auto_assign' => isset($_POST['auto_assign']) ? 1 : 0,
     1498                'auto_assign_to_admin' => isset($_POST['auto_assign_to_admin']) ? 1 : 0,
    14981499                'allowed_file_types' => isset($_POST['allowed_file_types']) ? self::validate_file_types(sanitize_text_field(wp_unslash($_POST['allowed_file_types']))) : 'jpg,jpeg,png,pdf',
    14991500                'max_file_size' => isset($_POST['max_file_size']) ? absint($_POST['max_file_size']) : 0,
     
    15051506                'ticket_id_start' => isset($_POST['ticket_id_start']) ? absint($_POST['ticket_id_start']) : 0,
    15061507                'status_change_notification' => isset($_POST['status_change_notification']) ? 1 : 0,
    1507                 'auto_assign_to_admin' => isset($_POST['auto_assign_to_admin']) ? 1 : 0,
     1508                'keep_data_on_uninstall' => isset($_POST['keep_data_on_uninstall']) ? 1 : 0,
     1509                'check_duplicates' => isset($_POST['check_duplicates']) ? 1 : 0,
     1510                'duplicate_threshold' => isset($_POST['duplicate_threshold']) ? absint($_POST['duplicate_threshold']) : 80,
    15081511            );
    15091512
     
    16481651        <div class="wrap">
    16491652            <h1><?php esc_html_e('NexlifyDesk Email Templates', 'nexlifydesk'); ?></h1>
     1653           
     1654            <div class="notice notice-info">
     1655                <p>
     1656                    <strong><?php esc_html_e('Template System Information:', 'nexlifydesk'); ?></strong><br>
     1657                    <?php esc_html_e('When fields below are empty, the system uses default template files from ', 'nexlifydesk'); ?>
     1658                    <code>templates/emails/</code> <?php esc_html_e('directory. Add content here only if you want to customize the default templates.', 'nexlifydesk'); ?>
     1659                </p>
     1660            </div>
     1661           
    16501662            <form method="post">
    16511663                <?php wp_nonce_field('nexlifydesk_save_email_templates'); ?>
    16521664               
    16531665                <h2><?php esc_html_e('New Ticket', 'nexlifydesk'); ?></h2>
     1666                <?php if (empty($templates['new_ticket'])): ?>
     1667                    <p class="description" style="color: #0073aa; margin-bottom: 10px;">
     1668                        <strong><?php esc_html_e('Currently using:', 'nexlifydesk'); ?></strong>
     1669                        <code>templates/emails/new_ticket.php</code>
     1670                        <?php esc_html_e('(Default template with proper styling)', 'nexlifydesk'); ?>
     1671                    </p>
     1672                <?php endif; ?>
    16541673                <?php wp_editor(
    16551674                    $templates['new_ticket'],
     
    16701689               
    16711690                <h2><?php esc_html_e('New Reply', 'nexlifydesk'); ?></h2>
     1691                <?php if (empty($templates['new_reply'])): ?>
     1692                    <p class="description" style="color: #0073aa; margin-bottom: 10px;">
     1693                        <strong><?php esc_html_e('Currently using:', 'nexlifydesk'); ?></strong>
     1694                        <code>templates/emails/new_reply.php</code>
     1695                        <?php esc_html_e('(Default template with proper styling)', 'nexlifydesk'); ?>
     1696                    </p>
     1697                <?php endif; ?>
    16721698                <?php wp_editor(
    16731699                    $templates['new_reply'],
     
    16881714               
    16891715                <h2><?php esc_html_e('Status Changed', 'nexlifydesk'); ?></h2>
     1716                <?php if (empty($templates['status_changed'])): ?>
     1717                    <p class="description" style="color: #0073aa; margin-bottom: 10px;">
     1718                        <strong><?php esc_html_e('Currently using:', 'nexlifydesk'); ?></strong>
     1719                        <code>templates/emails/status_changed.php</code>
     1720                        <?php esc_html_e('(Default template with proper styling)', 'nexlifydesk'); ?>
     1721                    </p>
     1722                <?php endif; ?>
    16901723                <?php wp_editor(
    16911724                    $templates['status_changed'],
     
    17421775               
    17431776                <h2><?php esc_html_e('SLA Breach', 'nexlifydesk'); ?></h2>
     1777                <?php if (empty($templates['sla_breach'])): ?>
     1778                    <p class="description" style="color: #0073aa; margin-bottom: 10px;">
     1779                        <strong><?php esc_html_e('Currently using:', 'nexlifydesk'); ?></strong>
     1780                        <code>templates/emails/sla_breach.php</code>
     1781                        <?php esc_html_e('(Default template with proper styling)', 'nexlifydesk'); ?>
     1782                    </p>
     1783                <?php endif; ?>
    17441784                <?php wp_editor(
    17451785                    $templates['sla_breach'],
  • nexlifydesk/trunk/includes/class-nexlifydesk-ajax.php

    r3330879 r3333095  
    1818        add_action('wp_ajax_nexlifydesk_add_category', array(__CLASS__, 'add_category'));
    1919        add_action('wp_ajax_nexlifydesk_test_aws_connection', array(__CLASS__, 'test_aws_connection'));
     20        add_action('wp_ajax_nexlifydesk_aws_diagnostics', array(__CLASS__, 'aws_diagnostics'));
    2021        add_action('wp_ajax_nexlifydesk_manual_fetch_emails', array(__CLASS__, 'manual_fetch_emails'));
    2122        add_action('wp_ajax_nexlifydesk_bulk_action', array(__CLASS__, 'handle_bulk_action'));
     
    187188        }
    188189
     190        $current_user = wp_get_current_user();
     191        $can_reply = false;
     192       
     193        if (current_user_can('nexlifydesk_manage_tickets') || current_user_can('manage_options')) {
     194            $can_reply = true;
     195        }
     196       
     197        if (!$can_reply && (int)$ticket->user_id === (int)$current_user->ID && $ticket->user_id > 0) {
     198            $can_reply = true;
     199        }
     200       
     201        if (!$can_reply && (int)$ticket->user_id === 0) {
     202            $customer_email = get_post_meta($ticket->id, 'customer_email', true);
     203            if ($customer_email && $customer_email === $current_user->user_email) {
     204                $can_reply = true;
     205            }
     206        }
     207       
     208        if (!$can_reply) {
     209            wp_send_json_error(__('You are not authorized to reply to this ticket.', 'nexlifydesk'));
     210            return;
     211        }
     212
    189213        $data = array(
    190214            'ticket_id' => $ticket->id,
    191215            'message' => wp_kses_post(wp_unslash($_POST['message']))
    192216        );
    193 
    194         $current_user = wp_get_current_user();
    195217       
    196218        if ($ticket && $ticket->status === 'closed') {
     
    770792        }
    771793
    772         // Check if IMAP extension is available
    773         if (!extension_loaded('imap')) {
    774             wp_send_json_error(array('message' => __('IMAP extension is not installed on this server. Please contact your hosting provider to enable the PHP IMAP extension.', 'nexlifydesk')));
     794        $imap_available = extension_loaded('imap') && function_exists('imap_open');
     795        if (!$imap_available) {
     796            wp_send_json_error(array('message' => __('IMAP extension is not properly installed or configured on this server. Please contact your hosting provider to enable the PHP IMAP extension with all required functions.', 'nexlifydesk')));
    775797        }
    776798
     
    823845            $imap_port = 993;
    824846           
     847            if (function_exists('imap_errors')) {
     848                imap_errors();
     849            }
     850            if (function_exists('imap_alerts')) {
     851                imap_alerts();
     852            }
     853           
    825854            $connection = @imap_open(
    826855                "{{$imap_host}:{$imap_port}/imap/ssl}INBOX",
    827856                $email,
    828857                $password,
    829                 OP_READONLY
     858                OP_READONLY | OP_SILENT
    830859            );
    831860
    832861            if (!$connection) {
    833                 $error = imap_last_error();
    834                 /* translators: 1: IMAP error, 2: Organization ID, 3: Email, 4: AWS region */
    835                 wp_send_json_error(array('message' => sprintf(__('AWS WorkMail IMAP connection failed: %1$s. Please verify your Organization ID (%2$s), Email (%3$s), and Password are correct for region %4$s.', 'nexlifydesk'), $error, $organization_id, $email, $region)));
     862                $error = 'Unknown connection error';
     863               
     864                if (function_exists('imap_last_error')) {
     865                    $last_error = imap_last_error();
     866                    if ($last_error) {
     867                        $error = $last_error;
     868                    }
     869                }
     870               
     871                $error_message = sprintf(
     872                    /* translators: %1$s: Error message from IMAP connection */
     873                    __('AWS WorkMail IMAP connection failed: %1$s', 'nexlifydesk'),
     874                    $error
     875                );
     876               
     877                if (strpos($error, 'connect') !== false || strpos($error, 'timeout') !== false) {
     878                    $error_message .= ' ' . __('This usually indicates a network connectivity issue or firewall blocking. Please verify your server can connect to AWS WorkMail servers.', 'nexlifydesk');
     879                } elseif (strpos($error, 'authenticate') !== false || strpos($error, 'login') !== false) {
     880                    /* translators: 1: Organization ID, 2: Email, 3: AWS Region */
     881                    $error_message .= ' ' . sprintf(__('Please verify your Organization ID (%1$s), Email (%2$s), and Password are correct for region %3$s.', 'nexlifydesk'), $organization_id, $email, $region);
     882                } else {
     883                    /* translators: 1: Organization ID, 2: Email, 3: AWS Region */
     884                    $error_message .= ' ' . sprintf(__('Please verify your AWS WorkMail configuration: Organization ID (%1$s), Email (%2$s), Region (%3$s).', 'nexlifydesk'), $organization_id, $email, $region);
     885                }
     886               
     887                wp_send_json_error(array('message' => $error_message));
    836888            }
    837889
    838890            imap_close($connection);
    839             $messages[] = 'AWS WorkMail IMAP connection successful';
     891            /* translators: 1: Email address, 2: AWS region */
     892            $messages[] = sprintf(__('AWS WorkMail IMAP connection successful for %1$s in region %2$s', 'nexlifydesk'), $email, $region);
    840893           
    841894            if (!empty($access_key_id) && !empty($secret_access_key)) {
     
    868921
    869922            $final_message = implode('. ', $messages);
    870             wp_send_json_success(array('message' => $final_message));
     923           
     924            $note = ' ' . __('Note: A successful connection test means IMAP authentication works. If email fetching still has issues, check your email provider settings, firewall, or contact support.', 'nexlifydesk');
     925           
     926            wp_send_json_success(array('message' => $final_message . $note));
    871927
    872928        } catch (Exception $e) {
     
    882938        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
    883939
    884         // Check if IMAP extension is available
    885940        if (!extension_loaded('imap')) {
    886941            wp_send_json_error(array('message' => __('IMAP extension is not installed on this server. Please contact your hosting provider to enable the PHP IMAP extension.', 'nexlifydesk')));
     
    15041559        return $size;
    15051560    }
     1561
     1562    /**
     1563     * AWS IMAP Diagnostics - provides detailed information about IMAP setup, required in production.
     1564     */
     1565    public static function aws_diagnostics() {
     1566       
     1567        if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) {
     1568            wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk')));
     1569        }
     1570
     1571        if (
     1572            !isset($_POST['nonce']) ||
     1573            !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk_aws_test')
     1574        ) {
     1575            wp_send_json_error(array('message' => __('Invalid nonce.', 'nexlifydesk')));
     1576        }
     1577
     1578        $diagnostics = [];
     1579        $settings = get_option('nexlifydesk_imap_settings', []);
     1580        $aws_configured = false;
     1581       
     1582        if (!empty($settings['aws_region']) && !empty($settings['aws_organization_id']) &&
     1583            !empty($settings['aws_email']) && !empty($settings['aws_password'])) {
     1584            $aws_configured = true;
     1585        }
     1586       
     1587        if (!$aws_configured) {
     1588            $diagnostics['aws_configuration'] = [
     1589                'status' => 'WARNING',
     1590                'message' => 'No AWS WorkMail credentials configured. Please configure AWS settings first before running diagnostics.'
     1591            ];
     1592            wp_send_json_success(array('diagnostics' => $diagnostics));
     1593            return;
     1594        }
     1595       
     1596        $diagnostics['imap_extension'] = [
     1597            'status' => extension_loaded('imap') ? 'OK' : 'FAILED',
     1598            'message' => extension_loaded('imap') ? 'IMAP extension is loaded' : 'IMAP extension is NOT loaded'
     1599        ];
     1600       
     1601        $required_functions = ['imap_open', 'imap_search', 'imap_fetchheader', 'imap_body', 'imap_close', 'imap_last_error'];
     1602        $missing_functions = [];
     1603       
     1604        foreach ($required_functions as $function) {
     1605            if (!function_exists($function)) {
     1606                $missing_functions[] = $function;
     1607            }
     1608        }
     1609       
     1610        $diagnostics['imap_functions'] = [
     1611            'status' => empty($missing_functions) ? 'OK' : 'FAILED',
     1612            'message' => empty($missing_functions) ? 'All required IMAP functions are available' : 'Missing functions: ' . implode(', ', $missing_functions)
     1613        ];
     1614       
     1615        $ssl_transports = stream_get_transports();
     1616        $has_ssl = in_array('ssl', $ssl_transports) && in_array('tls', $ssl_transports);
     1617       
     1618        $diagnostics['ssl_support'] = [
     1619            'status' => $has_ssl ? 'OK' : 'FAILED',
     1620            'message' => $has_ssl ? 'SSL/TLS support is available' : 'SSL/TLS support is NOT available'
     1621        ];
     1622       
     1623        $is_ssl_enabled = (
     1624            is_ssl() ||
     1625            (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ||
     1626            (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
     1627            (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') ||
     1628            (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') ||
     1629            (wp_parse_url(home_url(), PHP_URL_SCHEME) === 'https')
     1630        );
     1631       
     1632        if (defined('NEXLIFYDESK_FORCE_SSL_ENABLED') && NEXLIFYDESK_FORCE_SSL_ENABLED) {
     1633            $is_ssl_enabled = true;
     1634        }
     1635       
     1636        $diagnostics['site_ssl'] = [
     1637            'status' => $is_ssl_enabled ? 'OK' : 'WARNING',
     1638            'message' => $is_ssl_enabled ? 'Site is running on HTTPS' : 'Site is NOT running on HTTPS (required for AWS WorkMail)'
     1639        ];
     1640       
     1641        $aws_hosts_to_check = [
     1642            'imap.mail.us-east-1.awsapps.com',
     1643            'imap.mail.us-west-2.awsapps.com',
     1644            'imap.mail.eu-west-1.awsapps.com'
     1645        ];
     1646       
     1647        $connectivity_results = [];
     1648        foreach ($aws_hosts_to_check as $host) {
     1649            $ip = gethostbyname($host);
     1650            $connectivity_results[] = ($ip !== $host) ? "$host: OK" : "$host: DNS resolution failed";
     1651        }
     1652       
     1653        $diagnostics['aws_connectivity'] = [
     1654            'status' => 'INFO',
     1655            'message' => 'DNS Resolution Test: ' . implode(', ', $connectivity_results)
     1656        ];
     1657       
     1658        $current_host = "imap.mail.{$settings['aws_region']}.awsapps.com";
     1659        $diagnostics['current_config'] = [
     1660            'status' => 'INFO',
     1661            'message' => "Current AWS WorkMail host: {$current_host}, Organization: {$settings['aws_organization_id']}, Username: {$settings['aws_email']}"
     1662        ];
     1663       
     1664        $aws_password = nexlifydesk_decrypt($settings['aws_password']);
     1665        if (!empty($aws_password)) {
     1666            try {
     1667                $connection = @imap_open(
     1668                    "{{$current_host}:993/imap/ssl}INBOX",
     1669                    $settings['aws_email'],
     1670                    $aws_password,
     1671                    OP_READONLY | OP_SILENT
     1672                );
     1673               
     1674                if ($connection) {
     1675                    imap_close($connection);
     1676                    $diagnostics['aws_connection_test'] = [
     1677                        'status' => 'OK',
     1678                        'message' => 'AWS WorkMail IMAP connection test successful with saved credentials'
     1679                    ];
     1680                } else {
     1681                    $error = imap_last_error() ?: 'Connection failed';
     1682                    $diagnostics['aws_connection_test'] = [
     1683                        'status' => 'FAILED',
     1684                        'message' => "AWS WorkMail IMAP connection test failed: {$error}"
     1685                    ];
     1686                }
     1687            } catch (Exception $e) {
     1688                $diagnostics['aws_connection_test'] = [
     1689                    'status' => 'FAILED',
     1690                    'message' => "AWS WorkMail connection test error: {$e->getMessage()}"
     1691                ];
     1692            }
     1693        } else {
     1694            $diagnostics['aws_connection_test'] = [
     1695                'status' => 'WARNING',
     1696                'message' => 'Cannot test AWS connection - password decryption failed'
     1697            ];
     1698        }
     1699       
     1700        $php_version = PHP_VERSION;
     1701        $openssl_available = extension_loaded('openssl');
     1702       
     1703        $diagnostics['php_info'] = [
     1704            'status' => $openssl_available ? 'OK' : 'WARNING',
     1705            'message' => "PHP Version: {$php_version}, OpenSSL: " . ($openssl_available ? 'Available' : 'NOT Available')
     1706        ];
     1707       
     1708        wp_send_json_success(array('diagnostics' => $diagnostics));
     1709    }
    15061710}
  • nexlifydesk/trunk/includes/class-nexlifydesk-database.php

    r3330879 r3333095  
    193193   
    194194    public static function check_and_run_migrations() {
    195         $current_version = get_option('nexlifydesk_db_version', '1.0.2');
     195        $current_version = get_option('nexlifydesk_db_version', '1.0.3');
    196196        $plugin_version = NEXLIFYDESK_VERSION;
    197197       
    198         if (version_compare($current_version, '1.0.2', '<')) {
     198        if (version_compare($current_version, '1.0.3', '<')) {
    199199            self::migrate_to_1_0_1();
    200             update_option('nexlifydesk_db_version', '1.0.2');
    201         }
    202        
    203         if (version_compare($current_version, '1.0.2', '<')) {
     200            update_option('nexlifydesk_db_version', '1.0.3');
     201        }
     202       
     203        if (version_compare($current_version, '1.0.3', '<')) {
    204204            self::migrate_to_1_0_2();
    205             update_option('nexlifydesk_db_version', '1.0.2');
    206         }
    207        
    208         // Update to current version
     205            update_option('nexlifydesk_db_version', '1.0.3');
     206        }
     207       
     208        $security_migration_done = get_option('nexlifydesk_password_encryption_migration', false);
     209        if (!$security_migration_done) {
     210            self::migrate_encrypt_passwords();
     211            update_option('nexlifydesk_password_encryption_migration', true);
     212        }
     213       
    209214        if ($current_version !== $plugin_version) {
    210215            update_option('nexlifydesk_db_version', $plugin_version);
     
    215220        global $wpdb;
    216221       
    217         // Ensure tables array is initialized
    218222        if (empty(self::$tables)) {
    219223            self::init();
     
    289293        global $wpdb;
    290294       
    291         $current_version = get_option('nexlifydesk_version', '1.0.2');
    292        
    293         if ($current_version === '1.0.2' && !get_option('nexlifydesk_db_version')) {
     295        $current_version = get_option('nexlifydesk_version', '1.0.3');
     296       
     297        if ($current_version === '1.0.3' && !get_option('nexlifydesk_db_version')) {
    294298            return;
    295299        }
     
    329333        }
    330334    }
     335   
     336    /**
     337     * Security migration: Encrypt any plain text passwords in IMAP settings
     338     */
     339    private static function migrate_encrypt_passwords() {
     340        $imap_settings = get_option('nexlifydesk_imap_settings', array());
     341       
     342        if (empty($imap_settings)) {
     343            return;
     344        }
     345       
     346        $updated = false;
     347       
     348        if (isset($imap_settings['aws']) && is_array($imap_settings['aws'])) {
     349            foreach ($imap_settings['aws'] as $key => $aws_config) {
     350                if (isset($aws_config['password']) && !empty($aws_config['password'])) {
     351                    if (!nexlifydesk_is_encrypted($aws_config['password'])) {
     352                        $imap_settings['aws'][$key]['password'] = nexlifydesk_encrypt($aws_config['password']);
     353                        $updated = true;
     354                    }
     355                }
     356            }
     357        }
     358       
     359        $providers = array('google', 'outlook', 'generic');
     360        foreach ($providers as $provider) {
     361            if (isset($imap_settings[$provider]) && is_array($imap_settings[$provider])) {
     362                foreach ($imap_settings[$provider] as $key => $config) {
     363                    if (isset($config['password']) && !empty($config['password'])) {
     364                        if (!nexlifydesk_is_encrypted($config['password'])) {
     365                            $imap_settings[$provider][$key]['password'] = nexlifydesk_encrypt($config['password']);
     366                            $updated = true;
     367                        }
     368                    }
     369                }
     370            }
     371        }
     372       
     373        // Save updated settings if any passwords were encrypted
     374        if ($updated) {
     375            update_option('nexlifydesk_imap_settings', $imap_settings);
     376        }
     377    }
    331378}
  • nexlifydesk/trunk/includes/class-nexlifydesk-rate-limiter.php

    r3330751 r3333095  
    2020     * Time window in seconds (30 minutes)
    2121     */
    22     const TIME_WINDOW_SECONDS = 1800; // 30 minutes
     22    const TIME_WINDOW_SECONDS = 1800;
    2323   
    2424    /**
     
    5858   
    5959    /**
    60      * Record any email activity (ticket creation or reply) for rate limiting
    61      *
    6260     * @param int $user_id WordPress user ID (0 for non-registered users)
    6361     * @param string $email_address Email address
     
    8280   
    8381    /**
    84      * Get the remaining tickets allowed for a user/email
    85      *
    8682     * @param int $user_id WordPress user ID (0 for non-registered users)
    8783     * @param string $email_address Email address
     
    10298   
    10399    /**
    104      * Get the time remaining until rate limit resets
    105      *
    106100     * @param int $user_id WordPress user ID (0 for non-registered users)
    107101     * @param string $email_address Email address
     
    126120   
    127121    /**
    128      * Clear rate limit for a user/email (admin function)
    129      *
    130122     * @param int $user_id WordPress user ID (0 for non-registered users)
    131123     * @param string $email_address Email address
     
    144136   
    145137    /**
    146      * Get rate limit status for a user/email
    147      *
    148138     * @param int $user_id WordPress user ID (0 for non-registered users)
    149139     * @param string $email_address Email address
     
    172162   
    173163    /**
    174      * Get a unique identifier for rate limiting
    175      * Uses user ID if available, otherwise falls back to email address
    176      *
    177164     * @param int $user_id WordPress user ID (0 for non-registered users)
    178165     * @param string $email_address Email address
     
    188175   
    189176    /**
    190      * Format time remaining in human readable format
    191      *
    192177     * @param int $seconds Seconds remaining
    193178     * @return string Human readable time
     
    226211   
    227212    /**
    228      * Get rate limit error message
    229      *
    230213     * @param int $user_id WordPress user ID (0 for non-registered users)
    231214     * @param string $email_address Email address
  • nexlifydesk/trunk/includes/class-nexlifydesk-shortcodes.php

    r3330879 r3333095  
    1616        ), $atts, 'nexlifydesk_ticket_form');
    1717       
    18         // Return shortcode placeholder ONLY for documentation pages that are specifically about documentation
    19         // NOT for actual support ticket submission pages
    2018        if (self::is_documentation_page() && !self::is_actual_support_page()) {
    2119            return '<div class="nexlifydesk-shortcode-placeholder" style="background: #f0f0f0; padding: 15px; border: 1px dashed #ccc; border-radius: 4px; margin: 10px 0; text-align: center; color: #666;"><strong>Shortcode:</strong> <code>[nexlifydesk_ticket_form]</code><br><small>This shortcode displays the ticket submission form for customers.</small></div>';
     
    4038        ), $atts, 'nexlifydesk_ticket_list');
    4139       
    42         // Return shortcode placeholder ONLY for documentation pages that are specifically about documentation
    43         // NOT for actual support ticket list pages
    4440        if (self::is_documentation_page() && !self::is_actual_support_page()) {
    4541            return '<div class="nexlifydesk-shortcode-placeholder" style="background: #f0f0f0; padding: 15px; border: 1px dashed #ccc; border-radius: 4px; margin: 10px 0; text-align: center; color: #666;"><strong>Shortcode:</strong> <code>[nexlifydesk_ticket_list]</code><br><small>This shortcode displays the user\'s ticket history and allows them to view their support tickets.</small></div>';
     
    181177            'nexlifydesk-help',
    182178            'nexlifydesk-manual',
    183             'knowledge-base',
     179            'knowledge-base',
     180            'nexlifydesk',
    184181            'kb',
    185182            'faq',
     
    228225        $current_page_id = $post->ID;
    229226       
    230         // Check if this is the configured support pages
    231227        if ($current_page_id === $ticket_form_page_id || $current_page_id === $ticket_page_id) {
    232228            return true;
    233229        }
    234230       
    235         // Check if page title or slug indicates it's for actual support (ticket submission/viewing)
    236231        $page_title = strtolower($post->post_title);
    237232        $page_slug = strtolower($post->post_name);
     
    254249        foreach ($support_keywords as $keyword) {
    255250            if (strpos($page_title, $keyword) !== false || strpos($page_slug, $keyword) !== false) {
    256                 // But exclude documentation-specific terms
    257251                if (strpos($page_title, 'documentation') === false &&
    258252                    strpos($page_title, 'guide') === false &&
  • nexlifydesk/trunk/includes/class-nexlifydesk-tickets.php

    r3330879 r3333095  
    11<?php
     2
    23if (!defined('ABSPATH')) {
    34    exit;
     5}
     6
     7if (!function_exists('nexlifydesk_extract_customer_details')) {
     8    require_once __DIR__ . '/helpers.php';
    49}
    510
     
    7176        $data['email'] = $email_address;
    7277
    73         // For non-registered users, add a small delay to avoid race conditions with concurrent email processing
    7478        if (!empty($data['source']) && $data['source'] === 'email' && $user_id == 0) {
    75             // Add a micro-delay to reduce race conditions in email processing
    7679            usleep(100000); // 100ms delay
    7780        }
     
    132135        $new_ticket_id = $wpdb->insert_id;
    133136       
    134         // Store customer email as post meta for both web and email tickets
    135137        if (!empty($data['email'])) {
    136138            update_post_meta($new_ticket_id, 'customer_email', $data['email']);
     
    148150
    149151        register_shutdown_function(function() use ($ticket, $data) {
    150             // Different notification handling for email vs web channels
    151152            if (!empty($data['source']) && $data['source'] === 'email') {
    152                 // Send simple auto-response for email tickets
    153153                NexlifyDesk_Tickets::send_email_channel_notification($ticket, 'new_ticket');
    154154            } else {
    155                 // Send standard notifications for web tickets
    156155                NexlifyDesk_Tickets::send_notification($ticket, 'new_ticket');
    157156            }
     
    442441       
    443442        if (isset($attachments['name']) && is_array($attachments['name'])) {
    444             // Legacy multi-file format from $_FILES
    445443            $file_count = count($attachments['name']);
    446444           
     
    461459            }
    462460        } else {
    463             // Process each attachment individually (new format from AJAX)
    464461            foreach ($attachments as $attachment) {
    465462                if (is_array($attachment) && isset($attachment['name'])) {
     
    609606        global $wpdb;
    610607       
    611         $allowed_statuses = array('open', 'pending', 'in_progress', 'resolved', 'closed'); // <-- Add in_progress here
     608        $allowed_statuses = array('open', 'pending', 'in_progress', 'resolved', 'closed');
    612609        if (!in_array($status, $allowed_statuses)) {
    613610            return false;
     
    677674        $user = get_userdata($ticket->user_id);
    678675        $admin_email = get_option('admin_email');
    679        
    680         // Check if admin notifications are enabled
    681676        $admin_notifications_enabled = !empty($settings['admin_email_notifications']);
    682        
    683677        $customer_details = nexlifydesk_extract_customer_details($ticket->message);
    684678        $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
     
    727721                }
    728722
    729                 // Send to admin only if:
    730                 // 1. Admin notifications are enabled AND
    731                 // 2. Either no agent is assigned OR the ticket is assigned to admin
    732723                if ($admin_notifications_enabled && !in_array($admin_email, $emailed)) {
    733724                    $should_notify_admin = false;
    734725                   
    735726                    if (empty($ticket->assigned_to)) {
    736                         // No agent assigned, notify admin
    737727                        $should_notify_admin = true;
    738728                    } else {
    739                         // Check if assigned to admin
    740729                        $assigned_user = get_userdata($ticket->assigned_to);
    741730                        if ($assigned_user && in_array('administrator', $assigned_user->roles)) {
     
    881870            ob_start();
    882871            include NEXLIFYDESK_PLUGIN_DIR . 'templates/emails/' . $template . '.php';
    883             return ob_get_clean();
     872            $template_content = ob_get_clean();
     873           
     874            // Fallback if template file is empty or doesn't exist
     875            if (empty($template_content)) {
     876                $template_content = self::get_fallback_email_template($template, $ticket, $reply_id);
     877            }
     878           
     879            return $template_content;
    884880        }
    885881
     
    948944
    949945    /**
     946     * Get fallback email template if no custom template is set and file template is empty
     947     */
     948    private static function get_fallback_email_template($template, $ticket, $reply_id = null) {
     949        // Generate a basic fallback template if template files are not available
     950        $user = get_userdata($ticket->user_id);
     951        $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     952        $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
     953       
     954        switch ($template) {
     955            case 'new_ticket':
     956                return '<p>Hello ' . esc_html($customer_name) . ',</p><p>Your support ticket #' . esc_html($ticket->ticket_id) . ' has been created.</p><p>Subject: ' . esc_html($ticket->subject) . '</p><p>We will get back to you soon.</p>';
     957            case 'new_reply':
     958                return '<p>Hello ' . esc_html($customer_name) . ',</p><p>You have a new reply on ticket #' . esc_html($ticket->ticket_id) . '.</p>';
     959            case 'status_changed':
     960                return '<p>Hello ' . esc_html($customer_name) . ',</p><p>The status of your ticket #' . esc_html($ticket->ticket_id) . ' has been updated to: ' . esc_html(ucfirst($ticket->status)) . '.</p>';
     961            case 'sla_breach':
     962                return '<p>Attention: Ticket #' . esc_html($ticket->ticket_id) . ' has breached its SLA.</p>';
     963            default:
     964                return '<p>Email notification for ticket #' . esc_html($ticket->ticket_id) . '</p>';
     965        }
     966    }
     967
     968    /**
    950969     *
    951970     * This function detects common SMTP plugins and avoids adding Message-ID,
     
    11051124        $from_name = $reply_user->display_name ?: get_bloginfo('name');
    11061125        $from_email = $reply_user->user_email ?: get_option('admin_email');
    1107        
    1108         // Create fresh headers to avoid conflicts with SMTP plugins
    11091126        $reply_headers = self::get_email_headers($ticket);
    1110         // Override the From header with the reply user's details
    11111127        $reply_headers[1] = 'From: ' . $from_name . ' <' . $from_email . '>';
    11121128       
     
    11961212        $subject = sprintf(__('[New Email Ticket] [#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
    11971213
    1198         // Notify admin only if admin notifications are enabled and conditions are met
    11991214        if ($admin_notifications_enabled) {
    12001215            $should_notify_admin = false;
     
    16201635   
    16211636    /**
    1622      * Check for duplicate tickets based on simple rules:
    1623      * - For non-registered users: Check if they have any active ticket (any status except closed/resolved)
    1624      * - For registered users: Check for subject similarity only
    1625      *
    16261637     * @param array $data Ticket data to check for duplicates
    16271638     * @return object|false Returns existing ticket if duplicate found, false otherwise
    16281639     */
    16291640    public static function check_for_duplicate_ticket($data) {
    1630         global $wpdb;
    1631        
    1632         // Clear relevant caches to ensure fresh duplicate detection
    1633         $user_id = isset($data['user_id']) ? absint($data['user_id']) : get_current_user_id();
    1634         $email = isset($data['email']) ? sanitize_email($data['email']) : '';
    1635         $subject = sanitize_text_field($data['subject']);
    1636         $source = isset($data['source']) ? sanitize_text_field($data['source']) : '';
    1637        
     1641        if (!function_exists('nexlifydesk_find_duplicate_ticket')) {
     1642            require_once dirname(__FILE__) . '/nexlifydesk-functions.php';
     1643        }
    16381644        $settings = get_option('nexlifydesk_settings', array());
    16391645        $check_duplicates = isset($settings['check_duplicates']) ? $settings['check_duplicates'] : true;
    1640        
    16411646        if (!$check_duplicates) {
    16421647            return false;
    16431648        }
    1644        
    1645         $table_name = NexlifyDesk_Database::get_table('tickets');
    1646        
    1647         if ($user_id == 0 && !empty($email)) {
    1648            
    1649             // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    1650             $existing_ticket = $wpdb->get_row($wpdb->prepare(
    1651                 "SELECT t.* FROM {$table_name} t
    1652                  LEFT JOIN {$wpdb->prefix}postmeta pm ON pm.post_id = t.id AND pm.meta_key = 'customer_email'
    1653                  WHERE t.user_id = 0
    1654                  AND pm.meta_value = %s
    1655                  AND t.status IN ('open', 'pending', 'in_progress')
    1656                  ORDER BY t.created_at DESC
    1657                  LIMIT 1",
    1658                 $email
    1659             ));
    1660            
    1661             if (!$existing_ticket) {
    1662                 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    1663                 $existing_ticket = $wpdb->get_row($wpdb->prepare(
    1664                     "SELECT t.* FROM {$table_name} t
    1665                      WHERE t.user_id = 0
    1666                      AND t.message LIKE %s
    1667                      AND t.status IN ('open', 'pending', 'in_progress')
    1668                      ORDER BY t.created_at DESC
    1669                      LIMIT 1",
    1670                     '%Email: ' . $email . '%'
    1671                 ));
    1672             }
    1673            
    1674             if ($existing_ticket) {
    1675                 return $existing_ticket;
    1676             }
    1677         }
    1678        
    1679         if ($user_id > 0) {
    1680            
    1681             $cache_key = 'nexlifydesk_registered_user_duplicate_' . intval($user_id) . '_' . md5($subject);
    1682             $existing_ticket = wp_cache_get($cache_key);
    1683            
    1684             if (false === $existing_ticket) {
    1685                 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    1686                 $existing_ticket = $wpdb->get_row($wpdb->prepare(
    1687                     "SELECT * FROM {$table_name}
    1688                      WHERE user_id = %d
    1689                      AND subject = %s
    1690                      AND status IN ('open', 'pending', 'in_progress')
    1691                      AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
    1692                      ORDER BY created_at DESC
    1693                      LIMIT 1",
    1694                     $user_id,
    1695                     $subject
    1696                 ));
    1697                 wp_cache_set($cache_key, $existing_ticket, '', 300);
    1698             }
    1699            
    1700             if ($existing_ticket) {
    1701                 return $existing_ticket;
    1702             }
    1703         }
    1704        
    1705         return false;
     1649        return nexlifydesk_find_duplicate_ticket($data);
    17061650    }
    17071651
     
    18241768
    18251769    /**
    1826      * Update ticket priority
    18271770     *
    18281771     * @param int $ticket_id Ticket ID
  • nexlifydesk/trunk/includes/helpers.php

    r3330751 r3333095  
    114114    return true;
    115115}
    116 
    117 /**
    118  * Enhanced email parsing functions for better email address and name extraction
    119  */
    120116
    121117/**
     
    185181
    186182/**
    187  * Robust marketing email detection for NexlifyDesk.
     183 * Decode MIME-encoded email subjects to human-readable format
     184 * Fixes issue where subjects like "=?UTF-8?Q?Issue_with_Order_#5628_–_Missing_Items?="
     185 * appear encoded in tickets instead of being properly decoded
     186 *
     187 * @param string $subject The encoded email subject
     188 * @return string The decoded subject
     189 */
     190function nexlifydesk_decode_email_subject($subject) {
     191    if (empty($subject)) {
     192        return '';
     193    }
     194   
     195    $subject = trim($subject);
     196   
     197    // Check if subject contains MIME encoding
     198    if (strpos($subject, '=?') !== false && strpos($subject, '?=') !== false) {
     199        // Use imap_mime_header_decode to properly decode the subject
     200        $decoded_parts = imap_mime_header_decode($subject);
     201       
     202        if (is_array($decoded_parts) && count($decoded_parts) > 0) {
     203            $decoded_subject = '';
     204            foreach ($decoded_parts as $part) {
     205                $decoded_subject .= $part->text;
     206            }
     207            return trim($decoded_subject);
     208        }
     209    }
     210   
     211    // If no encoding detected or decoding failed, return original subject
     212    return $subject;
     213}
     214
     215/**
     216 * Marketing email detection for NexlifyDesk.
    188217 *
    189218 * @param string $email_address The sender's email address.
     
    194223 */
    195224function nexlifydesk_is_marketing_email($email_address, $subject, $message, $settings = []) {
    196     // Whitelist: never treat these as marketing
    197225    $whitelist = $settings['marketing_whitelist'] ?? [];
    198226    if (in_array(strtolower($email_address), array_map('strtolower', $whitelist), true)) {
     
    200228    }
    201229
    202     // Basic checks
    203230    if (empty($email_address) || empty($subject) || empty($message)) {
    204231        return false;
     
    232259    $message_lower = strtolower($message);
    233260
    234     // Sender check
    235261    $is_marketing_sender = false;
    236262    foreach ($marketing_senders as $indicator) {
     
    241267    }
    242268
    243     // Subject check
    244269    $has_marketing_subject = false;
    245270    foreach ($marketing_subject_keywords as $keyword) {
     
    250275    }
    251276
    252     // Content check
    253277    $has_marketing_content = false;
    254278    foreach ($marketing_content_keywords as $keyword) {
     
    259283    }
    260284
    261     // HTML patterns (common in marketing)
    262285    $html_patterns = [
    263286        '/<table[^>]*>.*?<\/table>/is', '/<img[^>]*src=[^>]*>/i',
     
    272295    }
    273296
    274     // Count links
    275297    preg_match_all('/https?:\/\/[^\s<>"{}|\\^`\[\]]+/i', $message, $matches);
    276298    $url_count = count($matches[0]);
    277299
    278     // Scoring system
    279300    $marketing_score = 0;
    280301    if ($is_marketing_sender) $marketing_score += 2;
     
    296317
    297318/**
    298  * Enhanced email thread detection and content extraction
    299  * Specifically designed to handle complex email thread patterns
    300319 *
    301320 * @param string $email_content The full email content
     
    306325        return $email_content;
    307326    }
    308    
    309327    if (!empty($from_email) && function_exists('nexlifydesk_is_admin_or_agent_email') && nexlifydesk_is_admin_or_agent_email($from_email)) {
    310328        return $email_content;
    311329    }
    312    
    313     if (!preg_match('/^(On\s+.*wrote:|From:.*@|-----Original Message-----)/im', $email_content)) {
    314         $has_email_markers = preg_match('/\b(wrote:|From:.*@|Original Message|Reply above|Sent from my)/i', $email_content);
    315         if (!$has_email_markers) {
    316             return $email_content;
    317         }
    318     }
    319    
    320     $thread_separators = [
    321         // Common formats from the user's example
    322         '/^-{3,}\s*On\s+.+wrote\s*-{3,}/',  // "---- On [date] [name] wrote ---"
    323         '/^-{2,}\s*On\s+\w+,\s*\d{1,2}\s+\w+\s+\d{4}.+wrote\s*-{2,}/',  // "-- On Tue, 15 Jul 2025 ... wrote --"
    324         '/^On\s+\w+,\s*\d{1,2}\s+\w+\s+\d{4}.+wrote\s*:?$/',  // "On Tue, 15 Jul 2025 ... wrote:"
    325         '/^At\s+\d{1,2}:\d{2}.*on\s+\d{1,2}\/\d{1,2}\/\d{4}.*wrote:/',  // "At 08:44 on 15/07/2025 ... wrote:"
    326        
    327         // Standard email patterns
    328         '/^-----Original Message-----/',
    329         '/^From:.*@.*/',
    330         '/^To:.*@.*/',
    331         '/^Sent:.*\d{4}/',
    332         '/^Date:.*\d{4}/',
    333         '/^Subject:.*/',
    334         '/^________________________________/',
    335         '/^______________/',
    336 
    337         '/^=+$/',
    338         '/^-{10,}$/',
    339         '/^_{10,}$/',
    340        
    341         // Gmail/Outlook specific
    342         '/^Reply above this line/',
    343         '/^Please reply above this line/',
    344         '/^Do not reply below this line/',
    345         '/^-----\s*Reply\s*above\s*this\s*line\s*-----/',
    346        
    347         // Mobile signatures
    348         '/^Sent from my iPhone/',
    349         '/^Sent from my iPad/',
    350         '/^Sent from my Android/',
    351         '/^Get Outlook for/',
    352        
    353         // International formats
    354         '/^Le\s+\d{1,2}\/\d{1,2}\/\d{4}.*a\s+écrit\s*:/',  // French
    355         '/^Am\s+\d{1,2}\.\d{1,2}\.\d{4}.*schrieb\s*:/',  // German
    356         '/^Il\s+\d{1,2}\/\d{1,2}\/\d{4}.*ha\s+scritto\s*:/',  // Italian
    357         '/^El\s+\d{1,2}\/\d{1,2}\/\d{4}.*escribió\s*:/',  // Spanish
    358         '/^Em\s+\d{1,2}\/\d{1,2}\/\d{4}.*escreveu\s*:/',  // Portuguese
     330    $body = str_replace(["\r\n", "\r"], "\n", $email_content);
     331    $header_pattern = '/^On\s.+?(?:at\s.+?)?\s?.+?\swrote:\s*$/mus';
     332    $reply_separator_regexes = [
     333        $header_pattern,
     334        '/^-{3,}\s*On\s+.+wrote\s*-{3,}/mus',
     335        '/^-{2,}\s*On\s+\w+,\s*\d{1,2}\s+\w+\s+\d{4}.+wrote\s*-{2,}/mus',
     336        '/^At\s+\d{1,2}:\d{2}.*on\s+\d{1,2}\/\d{1,2}\/\d{4}.*wrote:/mus',
     337        '/^-----Original Message-----/mus',
     338        '/^From:[\s\S]+?$/mus',
     339        '/^To:[\s\S]+?$/mus',
     340        '/^Sent:[\s\S]+?$/mus',
     341        '/^Date:[\s\S]+?$/mus',
     342        '/^Subject:[\s\S]+?$/mus',
     343        '/^Reply above this line$/mus',
     344        '/^Please reply above this line$/mus',
     345        '/^Do not reply below this line$/mus',
     346        '/^-----\s*Reply\s*above\s*this\s*line\s*-----/mus',
     347        '/^Le\s+\d{1,2}\/\d{1,2}\/\d{4}.*a\s+écrit\s*:/mus',
     348        '/^Am\s+\d{1,2}\.\d{1,2}\.\d{4}.*schrieb\s*:/mus',
     349        '/^Il\s+\d{1,2}\/\d{1,2}\/\d{4}.*ha\s+scritto\s*:/mus',
     350        '/^El\s+\d{1,2}\/\d{1,2}\/\d{4}.*escribió\s*:/mus',
     351        '/^Em\s+\d{1,2}\/\d{1,2}\/\d{4}.*escreveu\s*:/mus',
     352        '/=+$/mus',
     353        '/-{10,}/u',
     354        '/_{10,}/u',
    359355    ];
    360    
    361     $lines = explode("\n", $email_content);
    362     $clean_lines = [];
    363    
    364     foreach ($lines as $line) {
    365         $trimmed_line = trim($line);
    366        
    367         $is_thread_separator = false;
    368         foreach ($thread_separators as $pattern) {
    369             if (preg_match($pattern, $trimmed_line)) {
    370                 $is_thread_separator = true;
    371                 break;
    372             }
    373         }
    374        
    375         if ($is_thread_separator) {
     356    $separator_pos = false;
     357    foreach ($reply_separator_regexes as $regex) {
     358        if (preg_match($regex, $body, $match, PREG_OFFSET_CAPTURE)) {
     359            $separator_pos = $match[0][1];
    376360            break;
    377361        }
    378        
    379         if (preg_match('/^>\s*/', $line)) {
    380             if (stripos($line, 'wrote:') !== false ||
    381                 stripos($line, 'original message') !== false ||
    382                 stripos($line, 'from:') !== false ||
    383                 stripos($line, 'sent:') !== false ||
    384                 stripos($line, 'date:') !== false ||
    385                 stripos($line, 'to:') !== false ||
    386                 preg_match('/\d{4}/', $line)) {
    387                 break;
    388             }
    389         }
    390        
    391         $clean_lines[] = $line;
    392     }
    393    
    394     $clean_content = implode("\n", $clean_lines);
    395    
    396     $clean_content = preg_replace('/\n\s*\n\s*\n/', "\n\n", $clean_content);
    397     $clean_content = trim($clean_content);
    398    
     362    }
     363    if ($separator_pos !== false) {
     364        $body = substr($body, 0, $separator_pos);
     365    }
     366    $body = preg_replace('/^\s*(>|).*/m', '', $body);
     367    $body = preg_replace('/\n\s*-\s*\n+/u', "\n- ", $body);
     368    $body = preg_replace('/\n{2,}-/u', "\n-", $body);
     369    $body = preg_replace('/-\s*\n{2,}/u', "-\n", $body);
     370    $body = preg_replace('/-\s*\n+\s*-/', "-\n-", $body);
     371    $body = preg_replace('/\n{3,}/u', "\n\n", $body);
     372    $body = preg_replace('/[ \t]+$/m', '', $body);
     373    $clean_content = trim($body);
    399374    return $clean_content;
    400375}
    401376
    402 /**
    403  * Extract customer details from email messages
    404  * Enhanced version that handles email threads better
    405  *
    406  * @param string $message The email message content
    407  * @return array Array with 'name', 'email', and 'message' keys
    408  */
    409377function nexlifydesk_extract_customer_details($message) {
    410378    if (empty($message)) {
    411         return ['name' => '', 'email' => '', 'message' => $message];
    412     }
    413    
    414     // First decode the email content to handle special characters and emojis
     379        return ['name' => '', 'email' => '', 'message' => ''];
     380    }
     381   
    415382    $decoded_message = nexlifydesk_decode_email_content($message);
    416    
    417     // Then extract clean content
    418     $clean_message = nexlifydesk_extract_clean_email_content($decoded_message, '');
    419    
    420     if (strlen($clean_message) < 50 && strlen($decoded_message) > 100) {
    421         $clean_message = $decoded_message;
    422     }
     383    $clean_message = nexlifydesk_extract_clean_email_content($decoded_message);
    423384   
    424385    $name = '';
    425386    $email = '';
    426    
    427     if (preg_match('/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/', $clean_message, $matches)) {
    428         $email = $matches[1];
    429     }
    430    
    431     if (preg_match('/From:\s*([^<\n]+)\s*<([^>]+)>/', $clean_message, $matches)) {
    432         $name = trim($matches[1]);
    433         $email = trim($matches[2]);
    434     } elseif (preg_match('/Name:\s*([^\n]+)/', $clean_message, $matches)) {
    435         $name = trim($matches[1]);
    436     }
    437    
     387    $actual_message = $clean_message;
     388   
     389    // Handle structured format with [Customer Details] and [Message]/[Reply] sections
     390    if (preg_match('/\[Customer Details\]\s*(.*?)\s*\[(?:Message|Reply)\]\s*(.*)/s', $clean_message, $matches)) {
     391        $customer_section = trim($matches[1]);
     392        $actual_message = trim($matches[2]);
     393       
     394        // Extract name and email from customer details section
     395        if (preg_match('/Name:\s*([^\n\r]+)/i', $customer_section, $name_matches)) {
     396            $name = trim($name_matches[1]);
     397        }
     398        if (preg_match('/Email:\s*([^\n\r]+)/i', $customer_section, $email_matches)) {
     399            $email = trim($email_matches[1]);
     400        }
     401    } else {
     402        // Fallback to original logic for other formats
     403        if (preg_match('/From:\s*([^<\n]+)\s*<([^>]+)>/', $clean_message, $matches)) {
     404            $name = trim($matches[1]);
     405            $email = trim($matches[2]);
     406        } elseif (preg_match('/Name:\s*([^\n]+)/', $clean_message, $matches)) {
     407            $name = trim($matches[1]);
     408        }
     409       
     410        if (empty($email) && preg_match('/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/', $clean_message, $matches)) {
     411            $email = $matches[1];
     412        }
     413    }
     414   
     415    // Generate name from email if needed
    438416    if (empty($name) && !empty($email)) {
    439417        $email_parts = explode('@', $email);
     
    447425        'name' => $name,
    448426        'email' => $email,
    449         'message' => $clean_message
     427        'message' => $actual_message  // Now returns clean message without customer details
    450428    ];
    451429}
     
    536514    }
    537515   
    538     // Generate a new key for demonstration
    539516    $sample_key = nexlifydesk_generate_encryption_key();
    540517   
     
    580557}
    581558
    582 // Add AJAX handler for generating encryption keys
    583559add_action('wp_ajax_nexlifydesk_generate_encryption_key', 'nexlifydesk_ajax_generate_encryption_key');
    584560
  • nexlifydesk/trunk/includes/nexlifydesk-functions.php

    r3330741 r3333095  
    33    exit;
    44}
     5
     6// Load necessary interfaces and classes for text analysis
     7require_once __DIR__ . '/textanalysis/Interfaces/IDistance.php';
     8require_once __DIR__ . '/textanalysis/Interfaces/ISimilarity.php';
     9require_once __DIR__ . '/textanalysis/Interfaces/ITokenTransformation.php';
     10require_once __DIR__ . '/textanalysis/Interfaces/IStemmer.php';
     11require_once __DIR__ . '/textanalysis/Interfaces/IExtractStrategy.php';
     12require_once __DIR__ . '/textanalysis/Documents/DocumentAbstract.php';
     13require_once __DIR__ . '/textanalysis/Documents/TokensDocument.php';
     14require_once __DIR__ . '/textanalysis/Tokenizers/TokenizerAbstract.php';
     15require_once __DIR__ . '/textanalysis/Tokenizers/WhitespaceTokenizer.php';
     16require_once __DIR__ . '/textanalysis/Tokenizers/GeneralTokenizer.php';
     17require_once __DIR__ . '/textanalysis/Comparisons/CosineSimilarityComparison.php';
    518
    619/**
     
    132145    return '';
    133146}
     147
     148/**
     149 * Extract order numbers from subject or message using flexible patterns
     150 * @param string $text
     151 * @return array Array of detected order numbers (as strings)
     152 */
     153function nexlifydesk_extract_order_numbers($text) {
     154    $order_numbers = array();
     155    if (empty($text)) return $order_numbers;
     156    // Match patterns like Order #1234, Order number 1234, #1234, 1234
     157    $patterns = array(
     158        '/order\s*(number)?\s*#?\s*(\d{4,})/i', // Order #1234, Order number 1234
     159        '/#(\d{4,})/', // #1234
     160        '/\b(\d{4,})\b/' // 1234 (standalone, 4+ digits)
     161    );
     162    foreach ($patterns as $pattern) {
     163        if (preg_match_all($pattern, $text, $matches)) {
     164            foreach ($matches[2] ?? $matches[1] ?? array() as $num) {
     165                if ($num && !in_array($num, $order_numbers)) {
     166                    $order_numbers[] = $num;
     167                }
     168            }
     169        }
     170    }
     171    return $order_numbers;
     172}
     173
     174/**
     175 * Apply keyword mapping to normalize semantic tokens
     176 * @param array $tokens Array of tokens to normalize
     177 * @param array $map Keyword mapping array
     178 * @return array Normalized tokens
     179 */
     180function nexlifydesk_apply_keyword_map($tokens, $map) {
     181    foreach ($tokens as &$token) {
     182        if (isset($map[$token])) {
     183            $token = $map[$token];
     184        }
     185    }
     186    return $tokens;
     187}
     188
     189/**
     190 * Get common English stopwords for text processing
     191 * @return array Array of stopwords
     192 */
     193function nexlifydesk_get_stopwords() {
     194    return array('the','and','is','it','to','of','in','on','for','with','a','an','as','at','by','from','this','that','can','be','has','have','was','were','but','or','not','so','do','does','did','will','would','should','could','may','might','must','you','your','i','me','my','we','us','our','he','she','him','her','they','them','their');
     195}
     196
     197/**
     198 * - Registered users: Check any existing tickets for order numbers or subject matches
     199 * - If order numbers are found, check for existing tickets with matching order numbers
     200 * - Unregistered users: Consolidate by email address only, no content matching needed
     201 *
     202 * @param array $ticket_data (user_id, email, subject, message, source)
     203 * @return object|null Existing ticket if found, null otherwise
     204 */
     205function nexlifydesk_find_duplicate_ticket($ticket_data) {
     206    global $wpdb;
     207    $user_id = isset($ticket_data['user_id']) ? absint($ticket_data['user_id']) : 0;
     208    $email = isset($ticket_data['email']) ? sanitize_email($ticket_data['email']) : '';
     209    $subject = isset($ticket_data['subject']) ? sanitize_text_field($ticket_data['subject']) : '';
     210    $message = isset($ticket_data['message']) ? sanitize_text_field($ticket_data['message']) : '';
     211    $table_name = $wpdb->prefix . 'nexlifydesk_tickets';
     212   
     213    // Registered users
     214    if ($user_id > 0) {
     215        $cache_key = 'nexlifydesk_user_has_tickets_' . $user_id;
     216        $user_has_tickets = wp_cache_get($cache_key);
     217       
     218        if (false === $user_has_tickets) {
     219            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     220            $user_ticket_count = $wpdb->get_var( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table name is safe and required direct query.
     221                "SELECT COUNT(*) FROM `{$table_name}` WHERE user_id = %d AND status IN ('open','pending','in_progress')",
     222                $user_id
     223            ) );
     224            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     225            $user_has_tickets = (int)$user_ticket_count > 0;
     226            wp_cache_set($cache_key, $user_has_tickets, '', 300); // Cache for 5 minutes
     227        }
     228       
     229        if (!$user_has_tickets) {
     230            return null;
     231        }
     232       
     233        $order_numbers = array();
     234        if (!empty($subject)) {
     235            $order_numbers = array_merge($order_numbers, nexlifydesk_extract_order_numbers($subject));
     236        }
     237        if (!empty($message)) {
     238            $order_numbers = array_merge($order_numbers, nexlifydesk_extract_order_numbers($message));
     239        }
     240        $order_numbers = array_unique($order_numbers);
     241       
     242        if (!empty($order_numbers)) {
     243            $order_regex = implode('|', array_map('preg_quote', $order_numbers));
     244            $cache_key = 'nexlifydesk_user_order_match_' . md5($user_id . $order_regex);
     245            $existing_ticket = wp_cache_get($cache_key);
     246
     247            if (false === $existing_ticket) {
     248                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     249                $existing_ticket = $wpdb->get_row( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table name is safe and required direct query.
     250                    "SELECT * FROM `{$table_name}` WHERE user_id = %d AND (subject REGEXP %s OR message REGEXP %s) AND status IN ('open','pending','in_progress') ORDER BY created_at DESC LIMIT 1",
     251                    $user_id, $order_regex, $order_regex
     252                ) );
     253                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     254                wp_cache_set($cache_key, $existing_ticket, '', 60);
     255            }
     256            if ($existing_ticket) return $existing_ticket;
     257        }
     258       
     259        if (!empty($subject)) {
     260            $cache_key = 'nexlifydesk_user_subject_match_' . md5($user_id . $subject);
     261            $existing_ticket = wp_cache_get($cache_key);
     262
     263            if (false === $existing_ticket) {
     264                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     265                $existing_ticket = $wpdb->get_row( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table name is safe and required direct query.
     266                    "SELECT * FROM `{$table_name}` WHERE user_id = %d AND subject = %s AND status IN ('open','pending','in_progress') ORDER BY created_at DESC LIMIT 1",
     267                    $user_id, $subject
     268                ) );
     269                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     270                wp_cache_set($cache_key, $existing_ticket, '', 60);
     271            }
     272            if ($existing_ticket) return $existing_ticket;
     273        }
     274       
     275        if (class_exists('TextAnalysis\\Comparisons\\CosineSimilarityComparison') && class_exists('TextAnalysis\\Tokenizers\\GeneralTokenizer')) {
     276            $similarity_threshold = 0.12;
     277            $cache_key = 'nexlifydesk_user_tickets_semantic_' . $user_id;
     278            $user_tickets = wp_cache_get($cache_key);
     279
     280            if (false === $user_tickets) {
     281                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     282                $user_tickets = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table name is safe and required direct query.
     283                    "SELECT * FROM `{$table_name}` WHERE user_id = %d AND status IN ('open','pending','in_progress') ORDER BY created_at DESC LIMIT 10",
     284                    $user_id
     285                ) );
     286                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepated -- Table name is safe.
     287                wp_cache_set($cache_key, $user_tickets, '', 60);
     288            }
     289           
     290            if (!empty($user_tickets)) {
     291                $keyword_map = array(
     292                    'profile' => 'account', 'dashboard' => 'account', 'user' => 'account', 'panel' => 'account',
     293                    'restoring' => 'unlock', 'restored' => 'unlock', 'recovered' => 'unlock', 'reset' => 'unlock',
     294                    'unlock' => 'unlock', 'activate' => 'unlock', 'recover' => 'unlock', 'recovery' => 'unlock',
     295                    'lockedout' => 'locked', 'locked' => 'locked', 'blocked' => 'locked', 'out' => 'locked',
     296                    'credentials' => 'login', 'login' => 'login', 'signin' => 'login', 'sign' => 'login', 'access' => 'login',
     297                    'password' => 'password', 'passcode' => 'password', 'pwd' => 'password',
     298                    'help' => 'help', 'support' => 'help', 'assistance' => 'help', 'team' => 'help',
     299                    'issue' => 'problem', 'problem' => 'problem', 'error' => 'problem', 'failed' => 'problem',
     300                    'unable' => 'problem', 'cant' => 'problem', 'cannot' => 'problem', 'rejecting' => 'problem', 'not' => 'problem', 'anymore' => 'problem'
     301                );
     302               
     303                $tokenizer = new TextAnalysis\Tokenizers\GeneralTokenizer();
     304                $stopwords = nexlifydesk_get_stopwords();
     305                $incoming_text = $subject . ' ' . $message;
     306                $incoming_tokens = array_map(function($t) {
     307                    return strtolower(preg_replace('/[^a-z0-9]/i', '', $t));
     308                }, $tokenizer->tokenize($incoming_text));
     309                $incoming_tokens = array_diff($incoming_tokens, $stopwords);
     310                $incoming_tokens = nexlifydesk_apply_keyword_map($incoming_tokens, $keyword_map);
     311               
     312                foreach ($user_tickets as $ticket) {
     313                    $existing_text = $ticket->subject . ' ' . $ticket->message;
     314                    $existing_tokens = array_map(function($t) {
     315                        return strtolower(preg_replace('/[^a-z0-9]/i', '', $t));
     316                    }, $tokenizer->tokenize($existing_text));
     317                    $existing_tokens = array_diff($existing_tokens, $stopwords);
     318                    $existing_tokens = nexlifydesk_apply_keyword_map($existing_tokens, $keyword_map);
     319                   
     320                    $similarity = (new TextAnalysis\Comparisons\CosineSimilarityComparison())->similarity(
     321                        $incoming_tokens,
     322                        $existing_tokens
     323                    );
     324                    if ($similarity >= $similarity_threshold) {
     325                        return $ticket;
     326                    }
     327                }
     328            }
     329        }
     330       
     331        return null;
     332    }
     333
     334    if ($user_id == 0 && !empty($email)) {
     335        // For unregistered users, we simply check if there's ANY existing ticket for this email
     336        $cache_key = 'nexlifydesk_email_consolidation_' . md5($email);
     337        $existing_ticket = wp_cache_get($cache_key);
     338
     339        if (false === $existing_ticket) {
     340            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     341            $existing_ticket = $wpdb->get_row( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table name is safe and required direct query.
     342                "SELECT t.* FROM `{$table_name}` t
     343                 LEFT JOIN `{$wpdb->prefix}postmeta` pm ON pm.post_id = t.id AND pm.meta_key = 'customer_email'
     344                 WHERE t.user_id = 0 AND pm.meta_value = %s AND t.status IN ('open','pending','in_progress','resolved')
     345                 ORDER BY t.created_at DESC LIMIT 1",
     346                $email
     347            ) );
     348            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe.
     349            wp_cache_set($cache_key, $existing_ticket, '', 60);
     350        }
     351       
     352        return $existing_ticket;
     353    }
     354   
     355    return null;
     356}
     357
     358/**
     359 * Extract keywords from text for similarity comparison
     360 * @param string $text Input text to process
     361 * @return array Array of unique keywords
     362 */
     363function nexlifydesk_extract_keywords($text) {
     364    $text = strtolower($text);
     365    $text = preg_replace('/[^a-z0-9 ]/', ' ', $text);
     366    $words = array_filter(explode(' ', $text));
     367    $stopwords = nexlifydesk_get_stopwords();
     368    $keywords = array_diff($words, $stopwords);
     369    return array_unique($keywords);
     370}
     371
     372/**
     373 * Calculate similarity between two messages using keyword intersection
     374 * @param string $msg1 First message
     375 * @param string $msg2 Second message 
     376 * @return float Similarity score between 0 and 1
     377 */
     378function nexlifydesk_message_similarity($msg1, $msg2) {
     379    $kw1 = nexlifydesk_extract_keywords($msg1);
     380    $kw2 = nexlifydesk_extract_keywords($msg2);
     381    if (empty($kw1) || empty($kw2)) return 0;
     382    $intersection = array_intersect($kw1, $kw2);
     383    $union = array_unique(array_merge($kw1, $kw2));
     384    return count($intersection) / count($union);
     385}
  • nexlifydesk/trunk/nexlifydesk.php

    r3330879 r3333095  
    33 * Plugin Name: NexlifyDesk
    44 * Description: A modern, user-friendly support ticket system for WordPress with ticket submission, threaded replies, file attachments, agent assignment, and customizable.
    5  * Version: 1.0.2
     5 * Version: 1.0.3
    66 * Author URI: https://nexlifylabs.com
    77 * Supported Versions: 6.2+
     
    4848    // Signal that SDK was initiated.
    4949    do_action( 'nexlifydesk_loaded' );
     50   
     51    // Register Freemius uninstall handler
     52    nexlifydesk()->add_action('after_uninstall', 'nexlifydesk_freemius_uninstall_cleanup');
    5053}
    5154
    5255define('NEXLIFYDESK_PLUGIN_DIR', plugin_dir_path(__FILE__));
    5356define('NEXLIFYDESK_PLUGIN_URL', plugin_dir_url(__FILE__));
    54 define('NEXLIFYDESK_VERSION', '1.0.2');
     57define('NEXLIFYDESK_VERSION', '1.0.3');
    5558define('NEXLIFYDESK_TABLE_PREFIX', 'nexlifydesk_');
    5659define('NEXLIFYDESK_CAP_VIEW_ALL_TICKETS', 'nexlifydesk_view_all_tickets');
     
    9194}
    9295add_action('plugins_loaded', 'nexlifydesk_init');
     96add_action('admin_notices', 'nexlifydesk_show_template_update_notice');
     97add_action('wp_ajax_nexlifydesk_update_email_templates', 'nexlifydesk_ajax_update_email_templates');
     98
     99function nexlifydesk_show_template_update_notice() {
     100    // Only show to admins on NexlifyDesk pages
     101    if (!current_user_can('manage_options')) {
     102        return;
     103    }
     104   
     105    $screen = get_current_screen();
     106    if (!$screen || strpos($screen->id, 'nexlifydesk') === false) {
     107        return;
     108    }
     109   
     110    $existing_templates = get_option('nexlifydesk_email_templates', array());
     111    $needs_update = false;
     112   
     113    if (!empty($existing_templates)) {
     114        foreach (['new_ticket', 'new_reply', 'status_changed', 'sla_breach'] as $template_type) {
     115            if (isset($existing_templates[$template_type]) &&
     116                !empty($existing_templates[$template_type]) &&
     117                strpos($existing_templates[$template_type], '{user_name}') !== false) {
     118                $needs_update = true;
     119                break;
     120            }
     121        }
     122    }
     123   
     124    if ($needs_update) {
     125        ?>
     126        <div class="notice notice-warning is-dismissible" id="nexlifydesk-template-update-notice">
     127            <p>
     128                <strong><?php esc_html_e('NexlifyDesk Email Templates Update Available', 'nexlifydesk'); ?></strong><br>
     129                <?php esc_html_e('Your email templates are using the old format. Click below to update them to use the new professional template files with better styling and functionality.', 'nexlifydesk'); ?>
     130            </p>
     131            <p>
     132                <button type="button" class="button button-primary" onclick="nexlifydesk_update_templates()">
     133                    <?php esc_html_e('Update Email Templates', 'nexlifydesk'); ?>
     134                </button>
     135                <button type="button" class="button" onclick="nexlifydesk_dismiss_notice()">
     136                    <?php esc_html_e('Dismiss (Keep Current Templates)', 'nexlifydesk'); ?>
     137                </button>
     138            </p>
     139        </div>
     140        <script>
     141        function nexlifydesk_update_templates() {
     142            if (confirm('<?php esc_html_e('This will clear your current email templates and use the new professional template files. You can customize them later in Email Templates settings. Continue?', 'nexlifydesk'); ?>')) {
     143                var xhr = new XMLHttpRequest();
     144                xhr.open('POST', ajaxurl, true);
     145                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
     146                xhr.onreadystatechange = function() {
     147                    if (xhr.readyState === 4) {
     148                        if (xhr.status === 200) {
     149                            document.getElementById('nexlifydesk-template-update-notice').style.display = 'none';
     150                            alert('<?php esc_html_e('Email templates updated successfully! Check the Email Templates page to see the changes.', 'nexlifydesk'); ?>');
     151                        } else {
     152                            alert('<?php esc_html_e('Error updating templates. Please try again.', 'nexlifydesk'); ?>');
     153                        }
     154                    }
     155                };
     156                xhr.send('action=nexlifydesk_update_email_templates&nonce=<?php echo esc_attr( wp_create_nonce('nexlifydesk_update_templates') ); ?>');
     157            }
     158        }
     159       
     160        function nexlifydesk_dismiss_notice() {
     161            document.getElementById('nexlifydesk-template-update-notice').style.display = 'none';
     162        }
     163        </script>
     164        <?php
     165    }
     166}
     167
     168function nexlifydesk_ajax_update_email_templates() {
     169    // Verify nonce and permissions
     170    if (!current_user_can('manage_options') ||
     171        !isset($_POST['nonce']) ||
     172        !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk_update_templates')) {
     173        wp_die('Unauthorized');
     174    }
     175   
     176    $templates = array(
     177        'new_ticket' => '',
     178        'new_reply' => '',
     179        'status_changed' => '',
     180        'sla_breach' => '',
     181        'email_auto_response' => get_option('nexlifydesk_email_templates', array())['email_auto_response'] ?? '',
     182    );
     183   
     184    update_option('nexlifydesk_email_templates', $templates);
     185   
     186    wp_send_json_success(array('message' => __('Email templates updated successfully!', 'nexlifydesk')));
     187}
    93188
    94189// Activation function
     
    105200        update_option('nexlifydesk_settings', $default_settings);
    106201    }
     202
     203    nexlifydesk_load_default_email_templates();
     204}
     205
     206/**
     207 * Load email templates from template files and populate the admin interface
     208 */
     209function nexlifydesk_load_default_email_templates() {
     210    $existing_templates = get_option('nexlifydesk_email_templates', array());
     211   
     212    $needs_update = empty($existing_templates) ||
     213                   (isset($existing_templates['new_reply']) &&
     214                    strpos($existing_templates['new_reply'], '{user_name}') !== false);
     215   
     216    if ($needs_update) {
     217        $templates = array(
     218            'new_ticket' => '',
     219            'new_reply' => '',
     220            'status_changed' => '',
     221            'sla_breach' => '',
     222            'email_auto_response' => '<p>Thank you for contacting us. We have received your support request and have assigned it ticket ID <strong>#{ticket_id}</strong>.</p>
     223
     224<p><strong>Subject:</strong> {subject}</p>
     225
     226<p>Our support team will review your request and get back to you as soon as possible. You can reference this ticket ID in any future correspondence.</p>
     227
     228<p>Best regards,<br>
     229{site_name} Support Team<br>
     230<a href="{site_url}">{site_url}</a></p>',
     231        );
     232       
     233        update_option('nexlifydesk_email_templates', $templates);
     234    }
    107235}
    108236
     
    114242    wp_clear_scheduled_hook('nexlifydesk_auto_close_tickets');
    115243
    116     // Default email templates
    117     $default_templates = array(
    118         'new_ticket' => '<p>Dear {user_name},</p><p>Your ticket <strong>#{ticket_id}</strong> has been received.</p><p>Subject: {subject}</p><p>Message: {message}</p><p>We will get back to you soon.</p>',
    119         'new_reply' => '<p>Dear {user_name},</p><p>You have a new reply on ticket <strong>#{ticket_id}</strong>.</p><p>Reply: {reply_message}</p>',
    120         'status_changed' => '<p>Dear {user_name},</p><p>The status of your ticket <strong>#{ticket_id}</strong> has changed to <strong>{status}</strong>.</p>',
    121         'sla_breach' => '<p>Attention: Ticket <strong>#{ticket_id}</strong> has breached its SLA.</p>',
    122     );
    123 
    124     if (!get_option('nexlifydesk_email_templates')) {
    125         add_option('nexlifydesk_email_templates', $default_templates);
    126     }
    127244}
    128245
     
    289406}, 999);
    290407
    291 
    292408/**
    293  * Uninstall cleanup for NexlifyDesk.
    294  * Removes options, transients, roles, capabilities, custom tables, and uploaded files.
     409 * Freemius uninstall cleanup handler
    295410 */
    296 function nexlifydesk_uninstall_cleanup() {
    297     global $wpdb;
    298 
    299     // Get settings to check if data should be kept
    300     $settings = get_option('nexlifydesk_settings', array());
    301     $keep_data = isset($settings['keep_data_on_uninstall']) ? (bool)$settings['keep_data_on_uninstall'] : true;
    302 
    303     // Always remove scheduled hooks and transients
    304     wp_clear_scheduled_hook('nexlifydesk_sla_check');
    305     wp_clear_scheduled_hook('nexlifydesk_auto_close_tickets');
    306     wp_clear_scheduled_hook('nexlifydesk_check_orphaned_tickets');
    307     delete_transient('nexlifydesk_sla_tickets');
    308     delete_transient('nexlifydesk_resolved_tickets');
    309     wp_cache_flush();
    310 
    311     if ($keep_data) {
    312         // Remove only version option if keeping data
    313         delete_option('nexlifydesk_db_version');
    314         return;
    315     }
    316 
    317     // Remove all plugin options
    318     delete_option('nexlifydesk_settings');
    319     delete_option('nexlifydesk_email_templates');
    320     delete_option('nexlifydesk_last_ticket_number');
    321     delete_option('nexlifydesk_db_version');
    322 
    323     // Remove custom database tables
    324     $table_names = array(
    325         $wpdb->prefix . 'nexlifydesk_tickets',
    326         $wpdb->prefix . 'nexlifydesk_replies',
    327         $wpdb->prefix . 'nexlifydesk_attachments',
    328         $wpdb->prefix . 'nexlifydesk_categories'
    329     );
    330     require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    331     foreach ($table_names as $table_name) {
    332         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Plugin uninstall requires direct table deletion
    333         $wpdb->query("DROP TABLE IF EXISTS " . esc_sql($table_name));
    334     }
    335 
    336     // Remove custom roles
    337     remove_role('nexlifydesk_agent');
    338     remove_role('nexlifydesk_supervisor');
    339 
    340     // Remove custom capabilities from administrator
    341     $admin_role = get_role('administrator');
    342     if ($admin_role) {
    343         $capabilities = array(
    344             'nexlifydesk_manage_tickets',
    345             'nexlifydesk_view_all_tickets',
    346             'nexlifydesk_assign_tickets',
    347             'nexlifydesk_manage_categories',
    348             'nexlifydesk_view_reports'
    349         );
    350         foreach ($capabilities as $cap) {
    351             $admin_role->remove_cap($cap);
     411function nexlifydesk_freemius_uninstall_cleanup() {
     412    $uninstall_file = plugin_dir_path(__FILE__) . 'uninstall.php';
     413    if (file_exists($uninstall_file)) {
     414        if (!defined('WP_UNINSTALL_PLUGIN')) {
     415            define('WP_UNINSTALL_PLUGIN', true);
    352416        }
    353     }
    354 
    355     // Remove uploaded files directory
    356     $upload_dir = wp_upload_dir();
    357     $plugin_upload_dir = trailingslashit($upload_dir['basedir']) . 'nexlifydesk/';
    358     if (is_dir($plugin_upload_dir)) {
    359         nexlifydesk_delete_directory($plugin_upload_dir);
    360     }
    361 }
    362 
    363 /**
    364  * Recursively delete a directory and its files.
    365  */
    366 function nexlifydesk_delete_directory($dir) {
    367     if (!is_dir($dir)) {
    368         return false;
    369     }
    370     $files = array_diff(scandir($dir), array('.', '..'));
    371     foreach ($files as $file) {
    372         $path = $dir . DIRECTORY_SEPARATOR . $file;
    373         if (is_dir($path)) {
    374             nexlifydesk_delete_directory($path);
    375         } else {
    376             wp_delete_file($path);
    377         }
    378     }
    379     global $wp_filesystem;
    380     if (empty($wp_filesystem)) {
    381         require_once(ABSPATH . '/wp-admin/includes/file.php');
    382         WP_Filesystem();
    383     }
    384     return $wp_filesystem->rmdir($dir, true);
    385 }
    386 
    387 register_uninstall_hook(__FILE__, 'nexlifydesk_uninstall_cleanup');
     417        include_once $uninstall_file;
     418    }
     419}
     420
     421register_uninstall_hook(__FILE__, 'nexlifydesk_freemius_uninstall_cleanup');
    388422
    389423add_action('wp_ajax_nexlifydesk_update_status', 'nexlifydesk_update_status_callback');
     
    424458});
    425459
    426 // AJAX handler for Purge Data button
    427460add_action('wp_ajax_nexlifydesk_purge_data', function() {
    428461    if (!current_user_can('manage_options')) {
     
    432465    global $wpdb;
    433466    $purged = array();
    434     // Example: Purge tickets older than 1 year and their replies/attachments
     467   
    435468    $tickets_table = $wpdb->prefix . 'nexlifydesk_tickets';
    436469    $replies_table = $wpdb->prefix . 'nexlifydesk_replies';
     
    438471    $one_year_ago = gmdate('Y-m-d H:i:s', strtotime('-1 year'));
    439472   
    440     //Custom table maintenance requires direct queries without caching
    441473    // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is constructed from $wpdb->prefix and is safe
    442474    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table maintenance requires direct queries without caching
     
    470502add_action('nexlifydesk_fetch_emails_event', 'nexlifydesk_fetch_emails');
    471503
    472 // Function to get the current fetch interval setting
    473504function nexlifydesk_get_fetch_interval() {
    474505    $settings = get_option('nexlifydesk_imap_settings', []);
     
    483514}
    484515
    485 // Function to reschedule email fetch with new interval
    486516function nexlifydesk_reschedule_email_fetch() {
    487517    $interval = nexlifydesk_get_fetch_interval();
    488518    $schedule_name = "nexlifydesk_{$interval}_minutes";
    489519   
    490     // Clear existing schedule
    491520    wp_clear_scheduled_hook('nexlifydesk_fetch_emails_event');
    492521   
    493     // Schedule with new interval
    494522    if (!wp_next_scheduled('nexlifydesk_fetch_emails_event')) {
    495523        wp_schedule_event(time(), $schedule_name, 'nexlifydesk_fetch_emails_event');
     
    497525}
    498526
    499 // Schedule email fetch on plugin activation
    500527register_activation_hook(__FILE__, function() {
    501528    nexlifydesk_reschedule_email_fetch();
    502529});
    503530
    504 // Reschedule when settings are updated
    505531add_action('update_option_nexlifydesk_imap_settings', function($old_value, $value, $option) {
    506532    $old_interval = isset($old_value['fetch_interval']) ? intval($old_value['fetch_interval']) : 5;
    507533    $new_interval = isset($value['fetch_interval']) ? intval($value['fetch_interval']) : 5;
    508534   
    509     // If interval changed, reschedule
    510535    if ($old_interval !== $new_interval) {
    511536        nexlifydesk_reschedule_email_fetch();
     
    513538}, 10, 3);
    514539
    515 // Fallback: schedule on 'init' for multisite/cron edge cases
    516540add_action('init', function() {
    517541    if (!wp_next_scheduled('nexlifydesk_fetch_emails_event')) {
     
    520544});
    521545
    522 // Add custom intervals
    523546add_filter('cron_schedules', function($schedules) {
    524547    $schedules['nexlifydesk_2_minutes'] = array(
     
    537560});
    538561
    539 // AJAX handler for instant email fetch
    540562add_action('wp_ajax_nexlifydesk_fetch_emails_now', function() {
    541563    check_ajax_referer('nexlifydesk_fetch_emails_now');
    542564   
    543     // Use a more permissive permission check
    544565    if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) {
    545566        wp_send_json_error(__('You do not have permission.', 'nexlifydesk'));
  • nexlifydesk/trunk/readme.txt

    r3330879 r3333095  
    44Requires at least: 6.2
    55Tested up to: 6.8
    6 Stable tag: 1.0.2
     6Stable tag: 1.0.3
    77License: GPLv2 or later
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
    99Requires PHP: 7.4
    1010
    11 Hey there! NexlifyDesk is my awesome WordPress support ticketing system that turns your site into a pro customer service hub. It’s built for scalability and efficiency, giving you everything you need to wow your customers while keeping full control of their data.
     11Enterprise-grade WordPress helpdesk solution with intelligent ticket management, email piping, agent workflows, and WooCommerce integration.
     12
     13== Description ==
     14
     15NexlifyDesk transforms your WordPress site into a comprehensive customer support platform. Featuring advanced ticket management, intelligent duplicate detection, multi-channel email integration, and seamless WooCommerce connectivity, it delivers enterprise-level support capabilities while maintaining complete data control and security.
    1216
    1317**Documentation**: Check out the [Full Documentation & Setup Guide](https://nexlifylabs.com/nexlifydesk-documentation/getting-started/)
    1418
    15 == Description ==
    16 
    17 NexlifyDesk is my go-to, enterprise-grade ticketing solution for WordPress. I designed it to make customer support a breeze while keeping things organized and secure. Here’s what I’ve packed into it:
    18 
    19 - A complete system to manage tickets, agents, and workflows
    20 - Internal notes for team chats
    21 - SLA monitoring to keep things on track
    22 - Seamless WooCommerce integration
    23 
    24 It’s all about delivering top-notch support experiences—trust me, you’ll love it!
     19NexlifyDesk is a comprehensive, enterprise-grade helpdesk solution designed specifically for WordPress. Built with scalability, security, and efficiency at its core, it provides everything you need to deliver exceptional customer support while maintaining complete control over your data and workflows.
     20
     21**Why Choose NexlifyDesk?**
     22
     23Transform your customer support with a professional ticketing system that grows with your business. Whether you're a small business handling dozens of tickets or an enterprise managing thousands, NexlifyDesk provides the tools, automation, and insights you need to deliver outstanding customer experiences.
     24
     25**Core Capabilities:**
     26- **Advanced Ticket Management** - Complete lifecycle management with intelligent routing and automation
     27- **Multi-Channel Email Integration** - Convert emails to tickets with IMAP/POP3, AWS WorkMail, and Google Workspace support
     28- **Intelligent Duplicate Detection** - AI-powered semantic analysis prevents ticket fragmentation
     29- **WooCommerce Integration** - Deep integration with order history and customer context
     30- **Enterprise Security** - Built-in spam protection, rate limiting, and secure file handling
    2531
    2632== Key Features ==
    2733
    2834**Frontend Customer Experience**
    29 - Ticket Submission & Management: I’ve added a clean, user-friendly interface where customers can submit tickets, check their history, and track progress. Super easy!
    30 - Real-time Updates: An AJAX-powered setup with live status updates and instant reply notifications—keeps everything snappy!
    31 - File Attachments: Supports multiple file types with configurable size limits and security checks. Safety first!
    32 - Responsive Design: Works like a charm on desktop, tablet, or mobile—optimized for all!
     35- **Ticket Submission & Management** - Clean, user-friendly interface for ticket submission, history tracking, and progress monitoring
     36- **Real-time Updates** - AJAX-powered interface with live status updates and instant reply notifications
     37- **File Attachments** - Multiple file type support with configurable size limits and security validation
     38- **Responsive Design** - Optimized experience across desktop, tablet, and mobile devices
    3339
    3440**Advanced Admin Dashboard**
    35 - Centralized Ticket Management: A comprehensive admin interface with filtering, search, and bulk actions. I made it powerful yet simple!
    36 - Real-time Statistics: A live dashboard showing ticket counts, response times, and performance metrics. Love the insights!
    37 - Agent Assignment: Intelligent auto-assignment or manual options with load balancing—keeps the workload fair!
    38 - Status Management: Five ticket statuses (Open, In Progress, Pending, Resolved, Closed) with automatic workflows. Smooth sailing!
     41- **Centralized Ticket Management** - Comprehensive admin interface with filtering, search, and bulk operations
     42- **Real-time Statistics** - Live dashboard with ticket counts, response times, and performance metrics
     43- **Agent Assignment** - Intelligent auto-assignment with manual override and load balancing capabilities
     44- **Status Management** - Five ticket statuses (Open, In Progress, Pending, Resolved, Closed) with automated workflows
    3945
    4046**Agent Roles & Permissions System**
    41 - Custom Agent Positions: I let you create unlimited agent roles with granular permission control—total flexibility!
    42 - Capability Management: Fine-tune what each agent can do (view all tickets, assign them, manage categories, etc.). You’re in charge!
    43 - Agent Performance Tracking: Monitor response times, resolution rates, and workload distribution. Great for team management!
    44 - Orphaned Ticket Management: Automatically reassigns tickets if an agent leaves—handled it for you!
     47- **Custom Agent Positions** - Unlimited agent roles with granular permission control and capability management
     48- **Performance Tracking** - Monitor response times, resolution rates, and workload distribution analytics
     49- **Capability Management** - Fine-grained control over agent permissions (view tickets, assignments, categories, reports)
     50- **Orphaned Ticket Management** - Automatic ticket reassignment when agents are deactivated or removed
    4551
    4652**Professional Communication Tools**
    47 - Internal Notes: Private agent-to-agent chats within tickets (customers won’t see them—shh!). Perfect for teamwork!
    48 - Email Notifications: Fully customizable templates with dynamic placeholders for every scenario. I made it fun to tweak!
    49 - Notification Control: Granular settings for when and how notifications go out—your call!
    50 - Professional Templates: Pre-designed email templates for new tickets, replies, status changes, and SLA breaches. Ready to go!
     53- **Internal Notes** - Private agent-to-agent communication within tickets (invisible to customers)
     54- **Email Notifications** - Fully customizable templates with dynamic placeholders and conditional delivery
     55- **Notification Control** - Granular settings for notification timing, recipients, and trigger conditions
     56- **Professional Templates** - Pre-designed email templates for all ticket lifecycle events and SLA notifications
    5157
    5258**Intelligent Automation**
    53 - Enhanced Duplicate Detection: My advanced multi-layer algorithms spot similar tickets and merge them automatically—way more accurate now!
    54 - Auto-Assignment: Smartly distributes tickets to available agents based on workload. Hands-off genius!
    55 - SLA Monitoring: Tracks and notifies on breaches to maintain service standards—keeps you on your toes!
    56 - Auto-Closure: Resolved tickets close after 48 hours of inactivity with system notifications. Neat, right?
     59- **Enhanced Duplicate Detection** - Advanced multi-layer algorithms with semantic text analysis and automatic merging
     60- **Smart Auto-Assignment** - Intelligent ticket distribution based on agent availability and workload balancing
     61- **SLA Monitoring** - Automated tracking and breach notifications to maintain service level agreements
     62- **Auto-Closure** - Automatic ticket closure after 48 hours of inactivity with system notifications
    5763
    5864**Email Piping & Integration**
    59 - Multi-Provider Support: Turns emails into tickets with Custom IMAP/POP3, AWS WorkMail, and Google Workspace/Gmail. I’ve got you covered!
    60 - Flexible Email Management: Option to keep or delete emails after ticket creation—your choice!
    61 - Advanced Spam Protection: Built-in filtering, blocking, and rate limiting to stop abuse. Security’s my thing!
    62 - Intelligent Email Processing: Auto sender detection, thread management, and duplicate prevention. Smart stuff!
     65- **Multi-Provider Support** - Convert emails to tickets with Custom IMAP/POP3, AWS WorkMail, and Google Workspace/Gmail integration
     66- **Flexible Email Management** - Configurable options to retain or delete emails after ticket creation
     67- **Advanced Spam Protection** - Built-in filtering, blocking, and rate limiting to prevent abuse and maintain security
     68- **Intelligent Email Processing** - Automatic sender detection, thread management, and duplicate prevention algorithms
    6369
    6470**Enhanced Admin Experience**
    65 - Real-time Ticket List: Live-updating interface with read/unread status indicators. I love the flow!
    66 - Smart Sorting: Unread tickets show up first—prioritizes what matters!
    67 - Instant Notifications: Real-time updates without refreshing—smooth as butter!
    68 - Bulk Operations: Enhanced bulk actions for quick ticket management. Saves time!
     71- **Real-time Ticket Management** - Live-updating interface with read/unread status indicators and priority sorting
     72- **Smart Prioritization** - Unread tickets automatically surface first for immediate attention
     73- **Instant Notifications** - Real-time updates without page refresh for seamless workflow management
     74- **Bulk Operations** - Enhanced bulk actions for efficient ticket management and workflow optimization
    6975
    7076**Categories & Organization**
    71 - Unlimited Categories: Organize tickets with custom categories and descriptions. Go wild!
    72 - Priority Levels: Four levels (Low, Medium, High, Urgent) with visual indicators. Easy to spot!
    73 - Search & Filtering: Advanced search across all fields with multiple filter options. Find anything!
    74 - Bulk Operations: Manage multiple tickets at once—efficiency boost!
     77- **Unlimited Categories** - Organize tickets with custom categories, descriptions, and hierarchical structures
     78- **Priority Management** - Four priority levels (Low, Medium, High, Urgent) with visual indicators and automated workflows
     79- **Advanced Search & Filtering** - Comprehensive search across all fields with multiple filter combinations
     80- **Bulk Operations** - Manage multiple tickets simultaneously for improved efficiency
    7581
    7682**WooCommerce Integration**
    77 - Order History: Built-in order lookup for WooCommerce stores. Handy!
    78 - Customer Context: Access order info directly from tickets—context is king!
    79 - Order-based Duplicate Detection: Links tickets to existing order conversations automatically. Clever, huh?
     83- **Order History Access** - Built-in order lookup functionality for WooCommerce stores with comprehensive order details
     84- **Customer Context** - Direct access to order information from within tickets for enhanced customer support
     85- **Order-based Duplicate Detection** - Intelligent linking of tickets to existing order conversations for context preservation
    8086
    8187**Reporting & Analytics**
    82 - Performance Metrics: Reports on ticket volume, response times, and agent performance. Data nerd approved!
    83 - Visual Charts: Interactive charts for ticket trends, priority distribution, and monthly stats. Pretty cool!
    84 - Recent Activity: Real-time feed of all support activities across your team. Stay in the loop!
     88- **Performance Metrics** - Comprehensive reports on ticket volume, response times, and agent performance analytics
     89- **Visual Analytics** - Interactive charts for ticket trends, priority distribution, and monthly statistical analysis
     90- **Activity Monitoring** - Real-time feed of all support activities across your team for operational transparency
    8591
    8692**Developer & Advanced Features**
    87 - Template Override System: Customize frontend templates by copying to your theme. I made it developer-friendly!
    88 - Shortcode System: Flexible shortcodes like `[nexlifydesk_ticket_form]` and `[nexlifydesk_ticket_list]` with customizable attributes.
    89 - Data Management: Configurable retention with optional purge functionality. Your data, your rules!
    90 - Rate Limiting: Built-in protection against spam and abuse. Safety first!
    91 - Caching System: Optimized performance with smart cache management. Fast and reliable!
     93- **Template Override System** - Customize frontend templates by copying to your theme for complete design control
     94- **Shortcode System** - Flexible shortcodes like `[nexlifydesk_ticket_form]` and `[nexlifydesk_ticket_list]` with customizable attributes
     95- **Data Management** - Configurable retention policies with optional purge functionality for compliance requirements
     96- **Rate Limiting** - Built-in protection against spam and abuse with configurable thresholds
     97- **Caching System** - Optimized performance with intelligent cache management for enhanced responsiveness
    9298
    9399== Duplicate Ticket Detection ==
    94100
    95 I’ve built a slick three-layer system to keep your support queues tidy:
    96 
    97 - **Exact Subject Matching**: Spots identical subjects from the same user in the last 30 days and adds new messages as replies. No fragmentation!
    98 - **Order Number Pattern Recognition**: Recognizes order/invoice numbers (like "Order #12345" or "#ABC123") and links all related chats to one thread. Smart!
    99 - **Content Similarity Analysis**: Compares keywords between new and recent tickets from the same user, with a configurable 80% similarity threshold. Filters out stop words too!
     101NexlifyDesk features an advanced three-layer duplicate detection system designed to maintain organized support queues and prevent ticket fragmentation:
     102
     103**Detection Layers:**
     104- **Exact Subject Matching** - Identifies identical subjects from the same user within the last 30 days and automatically adds new messages as replies to existing tickets
     105- **Order Number Pattern Recognition** - Recognizes order/invoice number patterns (such as "Order #12345" or "#ABC123") and intelligently links related communications to unified conversation threads
     106- **Semantic Content Analysis** - Employs advanced cosine similarity algorithms to compare keywords between new and recent tickets from the same user, with configurable 80% similarity threshold and intelligent stopword filtering
    100107
    101108**User Experience**
    102 When a duplicate pops up, users get a clear note that their message joined an existing convo—keeps things contextual!
     109When duplicate tickets are detected, users receive clear notifications that their message has been added to an existing conversation, maintaining contextual continuity and communication history.
    103110
    104111**Administrative Control**
    105 - Turn duplicate detection on/off in NexlifyDesk > Settings
    106 - Tweak the sensitivity threshold to fit your workflow
    107 - Check duplicate stats in the reports dashboard
     112- Enable or disable duplicate detection in NexlifyDesk > Settings
     113- Adjust sensitivity thresholds to match your workflow requirements
     114- Monitor duplicate detection statistics and effectiveness in the reports dashboard
     115- Configure semantic analysis parameters for optimal accuracy
    108116
    109117== Installation ==
    110118
    111 Here’s how I suggest you get started:
    112 
    113 1. **Upload the Plugin**: Pop the `nexlifydesk` folder into `/wp-content/plugins/` via FTP, or upload the ZIP directly in WordPress admin.
    114 2. **Activate**: Turn it on via the 'Plugins' menu in WordPress.
    115 3. **Initial Configuration**: Head to NexlifyDesk > Settings to set up:
    116    - Email notification prefs
    117    - File upload limits and types
    118    - Default ticket priority and category
    119    - SLA response time targets
    120    - Auto-assignment rules
    121 4. **Create Frontend Pages**: Set up user pages:
    122    - **Ticket Submission Page**: Make a page and add `[nexlifydesk_ticket_form]`
    123    - **Ticket History Page**: Add `[nexlifydesk_ticket_list]` to another page
    124    - Link them in NexlifyDesk > Settings for navigation
     119**Quick Setup Process:**
     120
     1211. **Plugin Installation**: Upload the `nexlifydesk` folder to `/wp-content/plugins/` via FTP, or upload the ZIP file directly through WordPress admin dashboard
     1222. **Activation**: Activate the plugin through the 'Plugins' menu in WordPress administration
     1233. **Initial Configuration**: Navigate to NexlifyDesk > Settings to configure:
     124   - Email notification preferences and delivery settings
     125   - File upload limits, allowed types, and security validation
     126   - Default ticket priority and category assignments
     127   - SLA response time targets and breach notifications
     128   - Auto-assignment rules and agent load balancing
     1294. **Frontend Page Setup**: Create user-facing pages for ticket management:
     130   - **Ticket Submission Page**: Create a new page and add the shortcode `[nexlifydesk_ticket_form]`
     131   - **Ticket History Page**: Create another page and add the shortcode `[nexlifydesk_ticket_list]`
     132   - Configure page links in NexlifyDesk > Settings for seamless navigation
    1251335. **Email Piping Setup** (Optional): Configure email-to-ticket conversion:
    126    - Pick your provider (Custom IMAP/POP3, AWS WorkMail, or Google Workspace)
    127    - Enter connection details and auth
    128    - Set spam protection and filtering rules
    129    - Decide to keep or delete emails
    130 6. **Agent Setup** (Optional): Get your team ready:
    131    - Create user accounts for agents
    132    - Assign the "NexlifyDesk Agent" role
    133    - Define custom positions with capabilities
    134    - Set auto-assignment rules
     134   - Select your email provider (Custom IMAP/POP3, AWS WorkMail, or Google Workspace)
     135   - Enter connection details and authentication credentials
     136   - Configure spam protection rules and filtering criteria
     137   - Set email retention preferences (keep or delete after processing)
     1386. **Agent Team Setup** (Optional): Prepare your support team:
     139   - Create WordPress user accounts for support agents
     140   - Assign the "NexlifyDesk Agent" role to team members
     141   - Define custom agent positions with specific capabilities
     142   - Configure automatic assignment rules for optimal workload distribution
    135143
    136144== Frequently Asked Questions ==
    137145
    138146= How do I create a support ticket submission form? =
    139 Create a new WordPress page and drop in `[nexlifydesk_ticket_form]`. Tweak it with:
    140 - `show_title="no"` to hide the title
    141 - Includes fields for subject, message, category, priority, and file attachments
     147Create a new WordPress page and add the shortcode `[nexlifydesk_ticket_form]`. You can customize the form with additional attributes:
     148- `show_title="no"` to hide the page title
     149- The form includes fields for subject, message, category selection, priority level, and file attachments
    142150
    143151= How can customers view their submitted tickets? =
    144 Make a page with `[nexlifydesk_ticket_list]`. Logged-in users see their tickets, agents see assigned ones. Try:
    145 - `show_title="no"` to hide the title
    146 - `status="open"` to filter by status
     152Create a page with the shortcode `[nexlifydesk_ticket_list]`. Logged-in customers will see their own tickets, while agents will see their assigned tickets. Customization options include:
     153- `show_title="no"` to hide the page title
     154- `status="open"` to filter tickets by specific status
    147155
    148156= Can I customize the email notifications? =
    149 Yep! Go to NexlifyDesk > Email Templates to tweak:
    150 - New Ticket, New Reply, Status Changed, SLA Breach
    151 Use placeholders like `{ticket_id}`, `{user_name}`, `{subject}`, and more!
     157Yes! Navigate to NexlifyDesk > Email Templates to customize all notification emails:
     158- New Ticket notifications, New Reply alerts, Status Change notifications, and SLA Breach warnings
     159- Use dynamic placeholders like `{ticket_id}`, `{user_name}`, `{subject}`, `{ticket_content}`, and many more for personalized communications
    152160
    153161= How do I set up support agents? =
    154 **Basic Setup:**
    155 1. Go to Users > Add New for agent accounts
    156 2. Assign the "NexlifyDesk Agent" role
    157 
    158 **Advanced Setup:**
    159 1. Visit NexlifyDesk > Agent Positions for custom roles
    160 2. Set capabilities (e.g., "Level 1 Support")
    161 3. Assign agents via their profiles
    162 4. Configure auto-assignment in Settings
     162**Basic Agent Setup:**
     1631. Go to Users > Add New to create agent accounts
     1642. Assign the "NexlifyDesk Agent" role to new users
     165
     166**Advanced Agent Configuration:**
     1671. Visit NexlifyDesk > Agent Positions to create custom roles
     1682. Define specific capabilities (e.g., "Level 1 Support", "Senior Agent")
     1693. Assign agents to positions through their user profiles
     1704. Configure automatic assignment rules in Settings
    163171
    164172= What file types can be attached to tickets? =
    165 Default: JPG, PNG, PDF, and common docs. Customize in NexlifyDesk > Settings:
    166 - Allowed extensions
    167 - Max file size
    168 - Security validation
     173Default supported formats include JPG, PNG, PDF, and common document types. You can customize file handling in NexlifyDesk > Settings:
     174- Configure allowed file extensions
     175- Set maximum file size limits
     176- Enable security validation and virus scanning integration
    169177
    170178= How does the SLA monitoring work? =
    171 Set your response time in NexlifyDesk > Settings (in hours). It:
    172 - Tracks when tickets hit or miss SLA targets
    173 - Sends breach alerts to admins and agents
    174 - Shows SLA status in the dashboard
    175 - Includes metrics in reports
     179Configure response time targets in NexlifyDesk > Settings (specified in hours). The system will:
     180- Track response times against SLA targets
     181- Send breach notification alerts to administrators and assigned agents
     182- Display SLA status indicators in the dashboard interface
     183- Include SLA performance metrics in comprehensive reports
    176184
    177185= Can I integrate with WooCommerce? =
    178 Absolutely! Built-in integration:
    179 - Order History page for lookups
    180 - Order-based duplicate detection
    181 - Customer context in tickets
     186Absolutely! NexlifyDesk includes native WooCommerce integration features:
     187- Built-in Order History lookup page for comprehensive order details
     188- Automatic order-based duplicate detection for context preservation
     189- Direct customer context access within tickets for enhanced support quality
    182190
    183191= How do I set up email piping? =
    184 Turns emails into tickets. Setup varies:
    185 
    186 **Custom IMAP/POP3:**
    187 1. Go to NexlifyDesk > Settings > Email Piping
    188 2. Pick "Custom IMAP/POP3"
    189 3. Enter server details (host, port, username, password)
    190 4. Set spam and filtering rules
    191 5. Choose to delete emails or not
    192 
    193 **AWS WorkMail:**
    194 1. Enable SSL on your site
    195 2. Pick "AWS WorkMail"
    196 3. Enter region, org ID, and credentials
    197 4. Optional SES setup
    198 
    199 **Google Workspace/Gmail:**
    200 1. Set up OAuth in Google Cloud Console
    201 2. Pick "Google Workspace"
    202 3. Authenticate via OAuth
    203 4. Configure processing prefs
     192Email piping converts incoming emails into support tickets. Setup varies by provider:
     193
     194**Custom IMAP/POP3 Configuration:**
     1951. Navigate to NexlifyDesk > Settings > Email Piping
     1962. Select "Custom IMAP/POP3" as your provider
     1973. Enter mail server details (host, port, username, password, encryption)
     1984. Configure spam filtering and processing rules
     1995. Choose email retention preferences (delete or keep emails after processing)
     200
     201**AWS WorkMail Integration:**
     2021. Ensure SSL is enabled on your WordPress site
     2032. Select "AWS WorkMail" as your provider
     2043. Enter AWS region, organization ID, and authentication credentials
     2054. Optionally configure SES for enhanced email delivery
     206
     207**Google Workspace/Gmail Setup:**
     2081. Create OAuth credentials in Google Cloud Console
     2092. Select "Google Workspace" as your provider
     2103. Complete OAuth authentication flow
     2114. Configure email processing preferences and filters
    204212
    205213= How does automatic ticket assignment work? =
    206 My smart system:
    207 1. Finds available agents
    208 2. Balances workload with fewest open tickets
    209 3. Falls back to admins if needed
    210 4. Reassigns orphaned tickets
     214The intelligent assignment system operates through multiple algorithms:
     2151. Identifies available agents based on online status and workload
     2162. Balances ticket distribution by assigning to agents with fewest open tickets
     2173. Falls back to administrator assignment if no agents are available
     2184. Automatically reassigns orphaned tickets when agents are deactivated
    211219
    212220= What happens to closed tickets? =
    213 - Agents can close manually
    214 - Resolved tickets auto-close after 48 hours inactivity
    215 - Admins can reopen
    216 - Customers can’t reply to closed tickets—prompts new ones
    217 
    218 = Is my data safe when uninstalling? =
    219 By default, YES! Keeps all data (tickets, attachments, etc.) unless you change it in Settings to remove everything. Backup first!
     221Ticket closure follows a structured workflow:
     222- Agents can manually close tickets at any time
     223- Resolved tickets automatically close after 48 hours of customer inactivity
     224- Administrators can reopen closed tickets when necessary
     225- Customers cannot reply to closed tickets and are prompted to create new ones
     226
     227= Is my data safe when uninstalling the plugin? =
     228By default, YES! All plugin data (tickets, attachments, categories, etc.) is preserved during uninstallation for data safety. You can modify this behavior in Settings to enable complete data removal. Always create a backup before uninstalling any plugin.
    220229
    221230= How do internal notes work? =
    222 Private agent chats:
    223 - Only for team, not customers
    224 - Great for context or escalation
    225 - Add via "Add Internal Note" in admin
    226 
    227 = Can I customize the appearance? =
    228 Oh yes!
    229 - CSS styling in your theme
    230 - Template overrides
    231 - Color and label tweaks in Settings
    232 - Full email template control
     231Internal notes provide private agent-to-agent communication:
     232- Notes are completely invisible to customers
     233- Ideal for sharing context, escalation information, or troubleshooting details
     234- Add notes through the "Add Internal Note" tab in the admin ticket interface
     235- All internal notes are logged with timestamps and agent attribution
     236
     237= Can I customize the plugin's appearance? =
     238Multiple customization options are available:
     239- Add custom CSS styling through your theme's stylesheet
     240- Override plugin templates by copying them to your active theme directory
     241- Customize colors, labels, and visual elements through Settings
     242- Full control over email template design and content
     243- Responsive design elements for mobile optimization
    233244
    234245== Screenshots ==
    235246
    236 1. Frontend Ticket Submission Form: Clean form for customers with attachments and priority.
    237 2. Customer Ticket Dashboard: Easy interface for ticket history.
    238 3. Frontend Ticket Conversation: Threaded view with attachments.
    239 4. Admin Dashboard: Ticket management with stats.
    240 5. Admin Ticket Details: Single-ticket view with notes.
    241 6. Category Management: Organize with custom categories.
    242 7. Comprehensive Settings: Tons of config options.
    243 8. Agent Positions & Permissions: Granular role control.
    244 9. Email Template Editor: Customize notifications.
    245 10. Reports & Analytics: Charts and metrics galore.
     2471. **Frontend Ticket Submission Form** - Clean, user-friendly form interface with file attachments and priority selection
     2482. **Customer Ticket Dashboard** - Intuitive interface for customers to view and manage their ticket history
     2493. **Frontend Ticket Conversation** - Threaded conversation view with file attachments and status updates
     2504. **Admin Dashboard Overview** - Comprehensive ticket management interface with real-time statistics
     2515. **Admin Ticket Details** - Single-ticket management view with internal notes and agent actions
     2526. **Category Management** - Organize and manage custom ticket categories with descriptions
     2537. **Comprehensive Settings Panel** - Extensive configuration options for all plugin features
     2548. **Agent Positions & Permissions** - Granular role control and capability management system
     2559. **Email Template Editor** - Customize all notification emails with dynamic placeholders
     25610. **Reports & Analytics Dashboard** - Visual charts and comprehensive metrics for performance analysis
    246257
    247258== Usage ==
    248259
    249 === Shortcodes ===
    250 
    251 **`[nexlifydesk_ticket_form]`** - Shows the ticket form
    252 - `show_title="no"` - Hides title
    253 - `category="5"` - Pre-select category
    254 - `priority="high"` - Set default priority
    255 
    256 **`[nexlifydesk_ticket_list]`** - Shows ticket history
    257 - `show_title="no"` - Hides title
    258 - `status="open"` - Filter by status
    259 - `limit="10"` - Limit tickets
    260 
    261 === Admin Menu Structure ===
     260=== Shortcode Reference ===
     261
     262**`[nexlifydesk_ticket_form]`** - Display the ticket submission form
     263- `show_title="no"` - Hide the page title
     264- `category="5"` - Pre-select a specific category by ID
     265- `priority="high"` - Set default priority level (low, medium, high, urgent)
     266
     267**`[nexlifydesk_ticket_list]`** - Display ticket history and management
     268- `show_title="no"` - Hide the page title
     269- `status="open"` - Filter tickets by status (open, in-progress, pending, resolved, closed)
     270- `limit="10"` - Limit the number of tickets displayed per page
     271
     272=== Administrative Menu Structure ===
    262273
    263274**NexlifyDesk** (Main Menu)
    264 - All Tickets: Full management with filters
    265 - Categories: Custom ticket categories
    266 - Settings: All config options
    267 - Reports: Analytics and charts
    268 - Agent Positions: Role and capability setup
    269 - Order History: WooCommerce lookups
    270 - Email Templates: Notification tweaks
    271 - Support: Get help from me!
    272 
    273 === Ticket Statuses ===
    274 
    275 Five statuses for smooth workflows:
    276 - Open: New tickets
    277 - In Progress: Being worked on
    278 - Pending: Waiting on customer
    279 - Resolved: Solved, awaiting confirmation
    280 - Closed: Done (auto or manual)
    281 
    282 === Agent Capabilities ===
    283 
    284 Custom roles with:
    285 - View All Tickets: Org-wide access
    286 - Assign Tickets: Delegate to others
    287 - Manage Categories: Organize tickets
    288 - View Reports: See analytics
     275- **All Tickets** - Comprehensive ticket management with advanced filtering and bulk operations
     276- **Categories** - Create and manage custom ticket categories and hierarchies
     277- **Settings** - Complete configuration panel for all plugin features and integrations
     278- **Reports** - Analytics dashboard with charts, metrics, and performance insights
     279- **Agent Positions** - Role and capability management for team members
     280- **Order History** - WooCommerce order lookup and integration features
     281- **Email Templates** - Customize all notification templates with dynamic content
     282- **Support** - Direct access to plugin support and documentation resources
     283
     284=== Ticket Status Workflow ===
     285
     286Five distinct statuses provide complete ticket lifecycle management:
     287- **Open** - Newly submitted tickets awaiting initial agent response
     288- **In Progress** - Tickets actively being worked on by assigned agents
     289- **Pending** - Tickets waiting for customer response or additional information
     290- **Resolved** - Tickets marked as solved, awaiting customer confirmation
     291- **Closed** - Completed tickets (closed manually or automatically after 48 hours)
     292
     293=== Agent Capability System ===
     294
     295Custom agent roles support granular permission control:
     296- **View All Tickets** - Organization-wide ticket access across all agents and departments
     297- **Assign Tickets** - Ability to delegate tickets to other agents or departments
     298- **Manage Categories** - Create, edit, and organize ticket categories and hierarchies
     299- **View Reports** - Access to analytics, performance metrics, and statistical dashboards
    289300
    290301== Customization ==
    291302
    292 Lots of ways to make it yours!
    293 
    294303**Settings Panel Configuration**
    295 - Email Notifications: Control timing and delivery
    296 - Default Values: Set priority, category, etc.
    297 - File Upload Controls: Manage types and sizes
    298 - SLA Management: Set response targets
    299 - Automation Rules: Auto-assign, duplicates, closures
     304- **Email Notifications** - Control delivery timing, recipients, and notification triggers
     305- **Default Values** - Configure default priority levels, categories, and agent assignments
     306- **File Upload Controls** - Manage allowed file types, size limits, and security validation
     307- **SLA Management** - Set response time targets, breach notifications, and escalation rules
     308- **Automation Rules** - Configure auto-assignment, duplicate detection, and closure policies
    300309
    301310**Email Template Customization**
    302 - Dynamic placeholders (e.g., `{ticket_id}`)
    303 - HTML support with preview
    304 - Separate templates per notification
    305 - Multi-language ready
     311- **Dynamic Placeholders** - Use variables like `{ticket_id}`, `{user_name}`, `{subject}`, `{ticket_content}` for personalized communications
     312- **HTML Support** - Rich text formatting with live preview functionality
     313- **Multiple Templates** - Separate customization for each notification type and event
     314- **Multi-language Ready** - Support for internationalization and localization
    306315
    307316**Visual Customization**
    308 - CSS override in your theme
    309 - Template system for structural changes
    310 - Responsive design tweaks
     317- **CSS Override** - Add custom styles through your theme's stylesheet
     318- **Template System** - Override plugin templates for complete structural control
     319- **Responsive Design** - Mobile-first approach with tablet and desktop optimization
     320- **Color Schemes** - Customize visual elements to match your brand identity
    311321
    312322**Advanced Integration**
    313 - WooCommerce support
    314 - Multisite ready
    315 - Developer hooks
    316 - REST API future-proof
     323- **WooCommerce Support** - Deep integration with order management and customer history
     324- **Multisite Compatibility** - Full support for WordPress multisite networks
     325- **Developer Hooks** - Extensive action and filter hooks for custom functionality
     326- **REST API Ready** - Prepared for future API integrations and third-party connections
    317327
    318328== Performance & Security ==
    319329
    320330**Optimized Performance**
    321 - Smart caching
    322 - AJAX interface
    323 - Optimized database
    324 - Background email processing
     331- **Smart Caching** - Intelligent cache management for database queries and duplicate detection
     332- **AJAX Interface** - Seamless user experience with real-time updates and no page reloads
     333- **Optimized Database** - Efficient query structure and indexing for large ticket volumes
     334- **Background Processing** - Email handling and notifications processed asynchronously
    325335
    326336**Security Features**
    327 - Nonce verification
    328 - Data sanitization
    329 - File upload security
    330 - Rate limiting
    331 - Capability-based access
     337- **Nonce Verification** - WordPress security tokens for all form submissions and AJAX requests
     338- **Data Sanitization** - Comprehensive input validation and output escaping
     339- **File Upload Security** - MIME type validation, file extension verification, and size limits
     340- **Rate Limiting** - Built-in protection against spam, abuse, and automated attacks
     341- **Capability-based Access** - Role-based permissions with granular access control
    332342
    333343**Data Management**
    334 - Configurable retention
    335 - Optional purging
    336 - Database tools
    337 - Full export options
     344- **Configurable Retention** - Flexible data retention policies for compliance requirements
     345- **Optional Purging** - Safe data removal options with confirmation safeguards
     346- **Database Tools** - Maintenance utilities for optimization and cleanup
     347- **Full Export Options** - Complete data portability for migrations and backups
    338348
    339349== Changelog ==
    340350
     351= 1.0.3 =
     352- Fixed AWS Test Connection functionality for WorkMail and SES integration
     353- Added comprehensive AWS System Diagnostic tools for enhanced troubleshooting capabilities
     354- Improved frontend ticket page UI with mobile-friendly design and cleaner interface elements
     355- Enhanced duplicate detection with advanced semantic text analysis using cosine similarity algorithms
     356- Implemented intelligent keyword mapping and stopword filtering for more accurate duplicate ticket identification
     357- Optimized TextAnalysis library for improved performance and security compliance
     358- Added comprehensive caching system for duplicate detection queries to improve response times
     359- Minor improvements in core ticketing functionality and email piping reliability
     360
    341361= 1.0.2 =
    342 - Fixed email delivery with SMTP plugins (RFC 5322 compliance). Update now if you use SMTP!
     362- Fixed critical email delivery issues with SMTP plugins (RFC 5322 compliance)
     363- Improved compatibility with popular SMTP plugins and mail delivery services
     364- Enhanced email header formatting for better deliverability and spam prevention
    343365
    344366= 1.0.1 =
    345 - Added email piping (IMAP/POP3, AWS, Google), better duplicate detection, real-time ticket list, and spam protection. Big update!
     367- Added comprehensive email piping support (IMAP/POP3, AWS WorkMail, Google Workspace)
     368- Enhanced duplicate detection algorithms with improved accuracy and performance
     369- Implemented real-time ticket list updates with live status indicators
     370- Added advanced spam protection and rate limiting for email processing
     371- Improved admin interface with better filtering and search capabilities
    346372
    347373= 1.0.0 =
    348 - Initial release with full ticketing, agent management, internal notes, WooCommerce, SLA, duplicate detection, emails, roles, categories, files, and shortcodes!
     374- Initial release with complete ticketing system functionality
     375- Full agent management with roles and permissions
     376- Internal notes system for private agent communication
     377- Native WooCommerce integration with order lookup
     378- SLA monitoring and breach notifications
     379- Advanced duplicate detection algorithms
     380- Customizable email templates and notifications
     381- Granular user roles and capability management
     382- Custom categories and priority levels
     383- File attachment support with security validation
     384- Comprehensive shortcode system for frontend integration
    349385
    350386== Upgrade Notice ==
    351387
     388= 1.0.3 =
     389Major update: Fixes AWS connectivity issues, adds comprehensive system diagnostics, improves mobile UI responsiveness, and introduces advanced Powerful duplicate detection with semantic text analysis for significantly more accurate and intelligent ticket management.
     390
    352391= 1.0.2 =
    353 Critical fix for email issues with SMTP plugins. Update ASAP if you’re using one!
     392Critical fix for email delivery issues when using SMTP plugins. Update immediately if you're using any SMTP delivery service to ensure proper email functionality.
    354393
    355394= 1.0.1 =
    356 Major update with email piping and enhancements. Highly recommended!
     395Major feature update including email piping, enhanced duplicate detection, real-time interfaces, and spam protection. Highly recommended for all users to improve support workflow efficiency.
    357396
    358397= 1.0.0 =
    359 First release—no upgrade needed.
     398Initial release - no upgrade necessary. Welcome to NexlifyDesk!
    360399
    361400== Support & Documentation ==
    362401
    363 **Getting Help**
    364 - Email: [email protected]
    365 - Website: https://nexlifylabs.com
    366 - Docs: Full guides on the site
    367 - Community: WordPress.org forums
    368 
    369 **Premium Support**
    370 Go Pro for priority help and extra features!
    371 
    372 **Feature Requests**
    373 I love feedback! Send ideas via support channels.
     402**Getting Help and Support**
     403- **Email Support**: [email protected] for technical assistance and general inquiries
     404- **Official Website**: https://nexlifylabs.com for documentation, tutorials, and updates
     405- **Comprehensive Documentation**: Complete setup guides and feature documentation available on our website
     406- **Community Support**: WordPress.org plugin forums for community assistance and discussions
     407
     408**Premium Support Services**
     409Upgrade to premium support for priority assistance, advanced features, and dedicated technical support with faster response times.
     410
     411**Feature Requests and Feedback**
     412We value your input! Send feature suggestions and feedback through our support channels to help shape future development.
    374413
    375414== Privacy & Data Protection ==
    376415
    377 **Data Storage**
    378 - Stored in your WordPress DB
    379 - No external servers
    380 - Full control
    381 
    382 **Email Handling**
    383 - Uses your mail system
    384 - No third-party services
    385 - Privacy-compliant
    386 
    387 **Data Portability**
    388 - Export all data
    389 - WordPress formats
    390 - GDPR-ready
    391 
    392 **Security Measures**
    393 - Encryption
    394 - Regular audits
    395 - Best practices
    396 
    397 See [Privacy Policy](https://nexlifylabs.com/privacy-policy) for details.
     416**Data Storage and Security**
     417- **Local Storage**: All ticket data is stored in your WordPress database with no external servers
     418- **Complete Control**: Maintain full control over your support data and customer information
     419- **No Third-party Dependencies**: Core functionality operates independently without external service requirements
     420
     421**Email Handling and Privacy**
     422- **Your Mail System**: Uses your existing email infrastructure and SMTP settings
     423- **No External Services**: Email processing occurs on your server without third-party involvement
     424- **Privacy Compliant**: Designed to meet GDPR, CCPA, and other privacy regulation requirements
     425
     426**Data Portability and Export**
     427- **Complete Export**: Export all ticket data, attachments, and configurations in standard formats
     428- **WordPress Compatibility**: Data exports use WordPress-standard formats for easy migration
     429- **GDPR Ready**: Built-in tools for data export, modification, and deletion to support privacy rights
     430
     431**Security Measures and Compliance**
     432- **Data Encryption**: Sensitive data encryption for secure storage and transmission
     433- **Regular Security Audits**: Ongoing security reviews and updates for vulnerability protection
     434- **Best Practices**: Implementation follows WordPress security best practices and guidelines
     435
     436For complete privacy policy details, visit: [Privacy Policy](https://nexlifylabs.com/privacy-policy)
    398437
    399438== Uninstall Process ==
    400439
    401 **Data Retention (Default)**
    402 - Keeps all data (tickets, attachments, etc.) on uninstall—your history stays safe!
    403 
    404 **Complete Removal (Optional)**
    405 1. Go to NexlifyDesk > Settings
    406 2. Uncheck "Keep all tickets and data"
    407 3. Save and uninstall
    408 
    409 **Warning**: Deletes EVERYTHING—backup first!
     440**Data Retention (Default Behavior)**
     441- **Safe Preservation**: All plugin data (tickets, attachments, categories, settings) is preserved during uninstallation
     442- **Data Safety**: Your support history and customer data remain intact for future plugin reinstallation
     443- **WordPress Standard**: Follows WordPress plugin standards for data preservation
     444
     445**Complete Data Removal (Optional)**
     4461. Navigate to NexlifyDesk > Settings before uninstalling
     4472. Uncheck "Preserve all tickets and plugin data during uninstallation"
     4483. Save settings and proceed with plugin uninstallation
     4494. All plugin data will be permanently removed from your database
     450
     451**Important Warning**: Complete data removal is irreversible. Always create a full backup before enabling data removal or uninstalling the plugin. This ensures you can restore your support data if needed.
  • nexlifydesk/trunk/templates/admin/imap-auth.php

    r3330751 r3333095  
    278278                        <button type="button" id="test-aws-connection" class="button" <?php echo !$is_ssl_enabled ? 'disabled' : ''; ?>>Test AWS Connection</button>
    279279                        <button type="button" id="test-aws-fetch-emails" class="button button-secondary" style="margin-left: 10px;" <?php echo !$is_ssl_enabled ? 'disabled' : ''; ?>>Test Email Fetch</button>
     280                        <button type="button" id="aws-diagnostics" class="button button-secondary" style="margin-left: 10px;">System Diagnostics</button>
    280281                        <div id="aws-connection-result" style="margin-top: 10px;"></div>
    281282                        <div id="aws-fetch-result" style="margin-top: 10px;"></div>
    282                         <p class="description"><?php esc_html_e('Test the connection to AWS WorkMail and manually fetch emails for testing.', 'nexlifydesk'); ?></p>
     283                        <div id="aws-diagnostics-result" style="margin-top: 10px;"></div>
     284                        <p class="description"><?php esc_html_e('Test the connection to AWS WorkMail, manually fetch emails for testing, or run system diagnostics.', 'nexlifydesk'); ?></p>
    283285                    </td>
    284286                </tr>
  • nexlifydesk/trunk/templates/admin/settings.php

    r3330741 r3333095  
    3535        } elseif (in_array($key, array('max_file_size', 'default_category', 'sla_response_time', 'ticket_page_id', 'ticket_id_start', 'duplicate_threshold'), true)) {
    3636            $settings[$key] = (int)$value;
    37         } elseif (in_array($key, array('check_duplicates'), true)) {
     37        } elseif (in_array($key, array('check_duplicates', 'keep_data_on_uninstall'), true)) {
    3838            $settings[$key] = $value ? 1 : 0;
    3939        }
     
    296296    </form>
    297297</div>
     298
     299<script type="text/javascript">
     300jQuery(document).ready(function($) {
     301    // Handle data retention warning
     302    $('#keep_data_on_uninstall').on('change', function() {
     303        if (!this.checked) {
     304            $('#data-deletion-warning').show();
     305        } else {
     306            $('#data-deletion-warning').hide();
     307        }
     308    });
     309
     310    // Show warning if already unchecked on page load
     311    if (!$('#keep_data_on_uninstall').is(':checked')) {
     312        $('#data-deletion-warning').show();
     313    }
     314});
     315</script>
  • nexlifydesk/trunk/templates/admin/ticket-single.php

    r3330741 r3333095  
    1515$user_avatar_url = $user ? get_avatar_url($user->ID) : get_avatar_url(0);
    1616
     17// Extract customer details for non-registered users, but use clean message for display
     18// to avoid redundancy with the "About Customer" sidebar section
    1719$customer_details = function_exists('nexlifydesk_extract_customer_details') ?
    1820    nexlifydesk_extract_customer_details($ticket->message) :
     
    2123$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
    2224$customer_email = $user ? $user->user_email : ($customer_details['email'] ?: 'N/A');
     25// Use clean message without embedded customer details for admin panel display
    2326$display_message = $customer_details['message'];
    2427?>
     
    8891                    <button class="tab-link active" data-tab="reply"><?php esc_html_e('Reply', 'nexlifydesk'); ?></button>
    8992                    <button class="tab-link" data-tab="note"><?php esc_html_e('Add Internal Note', 'nexlifydesk'); ?></button>
     93                    <!--<button class="tab-link" data-tab="order"><?php esc_html_e('Order Lookup', 'nexlifydesk'); ?></button>-->
    9094                </div>
    9195
     
    150154                   
    151155                    if (!$reply_user) {
     156                        // For non-registered customers: Extract clean message without customer details
     157                        // to avoid redundancy with the "About Customer" sidebar section
    152158                        $reply_customer_details = function_exists('nexlifydesk_extract_customer_details') ?
    153159                            nexlifydesk_extract_customer_details($reply->message) :
    154160                            ['name' => '', 'email' => '', 'message' => $reply->message];
    155161                        $reply_customer_name = $reply_customer_details['name'] ?: 'Guest';
     162                        // Use clean message without embedded customer details for admin panel display
    156163                        $reply_display_message = $reply_customer_details['message'];
    157164                    } else {
  • nexlifydesk/trunk/templates/admin/tickets-list.php

    r3330741 r3333095  
    99
    1010// Ticket statistics
     11
    1112$stats = array();
    1213if (class_exists('NexlifyDesk_Reports')) {
     
    1415}
    1516?>
    16 
     17<div class="wrap">
     18    <h1 style="display: none;"></h1>
     19</div>
     20
     21<!-- Main ticket list UI -->
    1722<div class="wrap nexlifydesk-admin-ticket-list-ui">
     23
    1824    <div class="header">
    1925        <h1><?php esc_html_e('Support Tickets', 'nexlifydesk'); ?></h1>
  • nexlifydesk/trunk/templates/emails/new_reply.php

    r3326104 r3333095  
    33    exit;
    44}
     5
     6$user = get_userdata($ticket->user_id);
     7$customer_details = nexlifydesk_extract_customer_details($ticket->message);
     8$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Customer');
     9
     10// Get the reply information
     11$reply = null;
     12if ($reply_id) {
     13    $replies = NexlifyDesk_Tickets::get_replies($ticket->id);
     14    foreach ($replies as $r) {
     15        if ($r->id == $reply_id) {
     16            $reply = $r;
     17            break;
     18        }
     19    }
     20}
     21
     22$reply_customer_details = $reply ? nexlifydesk_extract_customer_details($reply->message) : array();
     23$reply_message = $reply ? ($reply_customer_details['message'] ?: $reply->message) : '';
     24$reply_user = $reply ? get_userdata($reply->user_id) : null;
     25$reply_user_name = $reply_user ? $reply_user->display_name : ($reply_customer_details['name'] ?: 'Guest');
     26
     27$ticket_url = add_query_arg(
     28    array('ticket_id' => $ticket->ticket_id),
     29    NexlifyDesk_Admin::get_ticket_page_url()
     30);
     31?>
     32<p>Hello <?php echo esc_html($customer_name); ?>,</p>
     33
     34<p>A new reply has been added to your support ticket.</p>
     35
     36<p><strong>Ticket Details:</strong></p>
     37<ul>
     38    <li><strong>Ticket ID:</strong> #<?php echo esc_html($ticket->ticket_id); ?></li>
     39    <li><strong>Subject:</strong> <?php echo esc_html($ticket->subject); ?></li>
     40    <li><strong>Status:</strong> <?php echo esc_html(ucfirst($ticket->status)); ?></li>
     41    <li><strong>Reply From:</strong> <?php echo esc_html($reply_user_name); ?></li>
     42</ul>
     43
     44<?php if ($reply_message): ?>
     45<p><strong>New Reply:</strong></p>
     46<div style="background-color: #f9f9f9; padding: 15px; border-left: 4px solid #0073aa;">
     47    <?php echo wp_kses_post(wpautop($reply_message)); ?>
     48</div>
     49<?php endif; ?>
     50
     51<p>You can view the full conversation and respond to this ticket by clicking the link below:</p>
     52
     53<p><a href="<?php echo esc_url($ticket_url); ?>" style="background-color: #0073aa; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Ticket</a></p>
     54
     55<p>Best regards,<br>
     56<?php echo esc_html(get_bloginfo('name')); ?> Support Team</p>
  • nexlifydesk/trunk/templates/emails/new_ticket.php

    r3326104 r3333095  
    33    exit;
    44}
     5
     6$user = get_userdata($ticket->user_id);
     7$customer_details = nexlifydesk_extract_customer_details($ticket->message);
     8$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Customer');
     9$customer_email = $user ? $user->user_email : ($customer_details['email'] ?: '');
     10$clean_message = $customer_details['message'] ?: $ticket->message;
     11
     12$ticket_url = add_query_arg(
     13    array('ticket_id' => $ticket->ticket_id),
     14    NexlifyDesk_Admin::get_ticket_page_url()
     15);
     16?>
     17<p>Hello <?php echo esc_html($customer_name); ?>,</p>
     18
     19<p>Thank you for contacting us. Your support ticket has been created successfully.</p>
     20
     21<p><strong>Ticket Details:</strong></p>
     22<ul>
     23    <li><strong>Ticket ID:</strong> #<?php echo esc_html($ticket->ticket_id); ?></li>
     24    <li><strong>Subject:</strong> <?php echo esc_html($ticket->subject); ?></li>
     25    <li><strong>Priority:</strong> <?php echo esc_html(ucfirst($ticket->priority)); ?></li>
     26    <li><strong>Status:</strong> <?php echo esc_html(ucfirst($ticket->status)); ?></li>
     27    <li><strong>Created:</strong> <?php echo esc_html(gmdate(get_option('date_format') . ' ' . get_option('time_format'), strtotime($ticket->created_at))); ?></li>
     28</ul>
     29
     30<p><strong>Your Message:</strong></p>
     31<div style="background-color: #f9f9f9; padding: 15px; border-left: 4px solid #ddd;">
     32    <?php echo wp_kses(wpautop(wp_kses_post($clean_message)), array(
     33        'p' => array(),
     34        'br' => array(),
     35        'strong' => array(),
     36        'em' => array(),
     37        'ul' => array(),
     38        'ol' => array(),
     39        'li' => array(),
     40        'a' => array(
     41            'href' => array(),
     42            'title' => array(),
     43            'rel' => array(),
     44            'target' => array(),
     45        ),
     46    )); ?>
     47</div>
     48
     49<p>Our support team will review your request and get back to you as soon as possible. You can view and track your ticket status at any time by clicking the link below:</p>
     50
     51<p><a href="<?php echo esc_url($ticket_url); ?>" style="background-color: #0073aa; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Ticket</a></p>
     52
     53<p>Please keep this ticket ID (#<?php echo esc_html($ticket->ticket_id); ?>) for your records and reference it in any future correspondence.</p>
     54
     55<p>Best regards,<br>
     56<?php echo esc_html(get_bloginfo('name')); ?> Support Team</p>
  • nexlifydesk/trunk/templates/emails/sla_breach.php

    r3326104 r3333095  
    33    exit;
    44}
     5
     6$user = get_userdata($ticket->user_id);
     7$customer_details = nexlifydesk_extract_customer_details($ticket->message);
     8$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Customer');
     9
     10$ticket_admin_url = add_query_arg(
     11    array(
     12        'page' => 'nexlifydesk_tickets',
     13        'ticket_id' => $ticket->id,
     14    ),
     15    admin_url('admin.php')
     16);
     17?>
     18<p><strong>SLA Breach Alert</strong></p>
     19
     20<p>This is an urgent notification that a support ticket has breached its SLA (Service Level Agreement) response time.</p>
     21
     22<p><strong>Ticket Details:</strong></p>
     23<ul>
     24    <li><strong>Ticket ID:</strong> #<?php echo esc_html($ticket->ticket_id); ?></li>
     25    <li><strong>Subject:</strong> <?php echo esc_html($ticket->subject); ?></li>
     26    <li><strong>Customer:</strong> <?php echo esc_html($customer_name); ?></li>
     27    <li><strong>Priority:</strong> <?php echo esc_html(ucfirst($ticket->priority)); ?></li>
     28    <li><strong>Status:</strong> <?php echo esc_html(ucfirst($ticket->status)); ?></li>
     29    <li><strong>Created:</strong> <?php echo esc_html(gmdate(get_option('date_format') . ' ' . get_option('time_format'), strtotime($ticket->created_at))); ?></li>
     30    <li><strong>Last Updated:</strong> <?php echo esc_html(gmdate(get_option('date_format') . ' ' . get_option('time_format'), strtotime($ticket->updated_at))); ?></li>
     31</ul>
     32
     33<p><strong>Immediate Action Required:</strong></p>
     34<p>This ticket requires immediate attention to prevent further SLA violations. Please review and respond promptly.</p>
     35
     36<p><a href="<?php echo esc_url($ticket_admin_url); ?>" style="background-color: #d63638; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Ticket Now</a></p>
     37
     38<p>This is an automated alert from the <?php echo esc_html(get_bloginfo('name')); ?> support system.</p>
  • nexlifydesk/trunk/templates/emails/status_changed.php

    r3326104 r3333095  
    33    exit;
    44}
     5
     6$user = get_userdata($ticket->user_id);
     7$customer_details = nexlifydesk_extract_customer_details($ticket->message);
     8$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Customer');
     9
     10$ticket_url = add_query_arg(
     11    array('ticket_id' => $ticket->ticket_id),
     12    NexlifyDesk_Admin::get_ticket_page_url()
     13);
     14?>
     15<p>Hello <?php echo esc_html($customer_name); ?>,</p>
     16
     17<p>The status of your support ticket has been updated.</p>
     18
     19<p><strong>Ticket Details:</strong></p>
     20<ul>
     21    <li><strong>Ticket ID:</strong> #<?php echo esc_html($ticket->ticket_id); ?></li>
     22    <li><strong>Subject:</strong> <?php echo esc_html($ticket->subject); ?></li>
     23    <li><strong>Previous Status:</strong> <?php echo esc_html(ucfirst($ticket->status)); ?></li>
     24    <li><strong>Current Status:</strong> <?php echo esc_html(ucfirst($ticket->status)); ?></li>
     25    <li><strong>Priority:</strong> <?php echo esc_html(ucfirst($ticket->priority)); ?></li>
     26    <li><strong>Updated:</strong> <?php echo esc_html(gmdate(get_option('date_format') . ' ' . get_option('time_format'), strtotime($ticket->updated_at))); ?></li>
     27</ul>
     28
     29<p>You can view your ticket and track its progress by clicking the link below:</p>
     30
     31<p><a href="<?php echo esc_url($ticket_url); ?>" style="background-color: #0073aa; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">View Ticket</a></p>
     32
     33<p>Thank you for your patience. Our support team is working to resolve your request.</p>
     34
     35<p>Best regards,<br>
     36<?php echo esc_html(get_bloginfo('name')); ?> Support Team</p>
  • nexlifydesk/trunk/templates/frontend/ticket-list.php

    r3326104 r3333095  
    1414}
    1515?>
    16 <div class="nexlifydesk-frontend-container">
    17     <div class="nexlifydesk-ticket-list-header">
    18         <div class="nexlifydesk-ticket-list-header__left">
    19             <h1 class="nexlifydesk-ticket-list-title"><?php esc_html_e('Your Support Tickets', 'nexlifydesk'); ?></h1>
    20             <p class="nexlifydesk-ticket-list-desc"><?php esc_html_e('View and track your support requests', 'nexlifydesk'); ?></p>
     16<div class="nexlifydesk-table-container">
     17    <header class="nexlifydesk-table-header">
     18        <h1><?php esc_html_e('Support Tickets', 'nexlifydesk'); ?></h1>
     19        <div class="nexlifydesk-header-actions">
     20            <?php if (!empty($submit_url) && $submit_url !== home_url()) : ?>
     21                <a href="<?php echo esc_url($submit_url); ?>" class="nexlifydesk-btn-primary">
     22                    <?php esc_html_e('New Ticket', 'nexlifydesk'); ?>
     23                </a>
     24            <?php endif; ?>
    2125        </div>
    22         <?php if (!empty($submit_url) && $submit_url !== home_url()) : ?>
    23             <a href="<?php echo esc_url($submit_url); ?>" class="btn btn-primary nexlifydesk-ticket-list-header__btn">
    24                 <?php esc_html_e('Submit New Ticket', 'nexlifydesk'); ?>
    25             </a>
    26         <?php endif; ?>
    27     </div>
     26    </header>
    2827
    2928    <?php if (empty($user_tickets)) : ?>
    30         <div class="nexlifydesk-no-tickets">
    31             <div class="nexlifydesk-no-tickets__icon">📄</div>
    32             <h3 class="nexlifydesk-no-tickets__title"><?php esc_html_e('No tickets found', 'nexlifydesk'); ?></h3>
    33             <p class="nexlifydesk-no-tickets__desc"><?php esc_html_e('You haven\'t submitted any support tickets yet.', 'nexlifydesk'); ?></p>
     29        <div class="nexlifydesk-no-tickets-table">
     30            <div class="nexlifydesk-no-tickets-icon">📄</div>
     31            <h3><?php esc_html_e('No tickets found', 'nexlifydesk'); ?></h3>
     32            <p><?php esc_html_e('You haven\'t submitted any support tickets yet.', 'nexlifydesk'); ?></p>
    3433            <?php if (!empty($submit_url) && $submit_url !== home_url()) : ?>
    35                 <a href="<?php echo esc_url($submit_url); ?>" class="btn btn-primary nexlifydesk-no-tickets__btn">
     34                <a href="<?php echo esc_url($submit_url); ?>" class="nexlifydesk-btn-primary">
    3635                    <?php esc_html_e('Submit Your First Ticket', 'nexlifydesk'); ?>
    3736                </a>
     
    3938        </div>
    4039    <?php else : ?>
    41         <div class="nexlifydesk-ticket-list-grid">
    42             <?php foreach ($user_tickets as $ticket) : ?>
    43                 <div class="nexlifydesk-ticket-card">
    44                     <div class="nexlifydesk-ticket-card__top">
    45                         <span class="nexlifydesk-ticket-card__id">#<?php echo esc_html($ticket->ticket_id); ?></span>
    46                         <span class="nexlifydesk-ticket-card__status status-<?php echo esc_attr($ticket->status); ?>">
    47                             <?php echo esc_html(ucfirst($ticket->status)); ?>
    48                         </span>
    49                     </div>
    50                     <div class="nexlifydesk-ticket-card__title">
    51                         <a href="<?php echo esc_url(add_query_arg('ticket_id', $ticket->ticket_id, get_permalink())); ?>">
     40        <table class="nexlifydesk-ticket-table">
     41            <thead>
     42                <tr>
     43                    <th><?php esc_html_e('ID', 'nexlifydesk'); ?></th>
     44                    <th><?php esc_html_e('Subject', 'nexlifydesk'); ?></th>
     45                    <th><?php esc_html_e('Status', 'nexlifydesk'); ?></th>
     46                    <th><?php esc_html_e('Priority', 'nexlifydesk'); ?></th>
     47                    <th><?php esc_html_e('Date', 'nexlifydesk'); ?></th>
     48                    <th><?php esc_html_e('Actions', 'nexlifydesk'); ?></th>
     49                </tr>
     50            </thead>
     51            <tbody>
     52                <?php foreach ($user_tickets as $ticket) : ?>
     53                    <tr>
     54                        <td data-label="<?php esc_attr_e('ID', 'nexlifydesk'); ?>">#<?php echo esc_html($ticket->ticket_id); ?></td>
     55                        <td data-label="<?php esc_attr_e('Subject', 'nexlifydesk'); ?>" class="nexlifydesk-subject-cell">
    5256                            <?php echo esc_html($ticket->subject); ?>
    53                         </a>
    54                     </div>
    55                     <div class="nexlifydesk-ticket-card__meta">
    56                         <span class="nexlifydesk-ticket-card__priority priority-<?php echo esc_attr($ticket->priority); ?>">
    57                             <?php echo esc_html(ucfirst($ticket->priority)); ?>
    58                         </span>
    59                         <span class="nexlifydesk-ticket-card__created">
     57                            <?php if (!empty($ticket->message)) : ?>
     58                                <div class="nexlifydesk-ticket-preview">
     59                                    <?php echo esc_html(wp_trim_words($ticket->message, 15, '...')); ?>
     60                                </div>
     61                            <?php endif; ?>
     62                        </td>
     63                        <td data-label="<?php esc_attr_e('Status', 'nexlifydesk'); ?>">
     64                            <span class="nexlifydesk-status <?php echo esc_attr(str_replace('_', '-', $ticket->status)); ?>">
     65                                <?php echo esc_html(ucfirst(str_replace('_', ' ', $ticket->status))); ?>
     66                            </span>
     67                        </td>
     68                        <td data-label="<?php esc_attr_e('Priority', 'nexlifydesk'); ?>">
     69                            <span class="nexlifydesk-priority <?php echo esc_attr($ticket->priority); ?>">
     70                                <?php echo esc_html(ucfirst($ticket->priority)); ?>
     71                            </span>
     72                        </td>
     73                        <td data-label="<?php esc_attr_e('Date', 'nexlifydesk'); ?>">
    6074                            <?php echo esc_html(date_i18n(get_option('date_format'), strtotime($ticket->created_at))); ?>
    61                         </span>
    62                     </div>
    63                     <div class="nexlifydesk-ticket-card__actions">
    64                         <a href="<?php echo esc_url(add_query_arg('ticket_id', $ticket->ticket_id, get_permalink())); ?>" class="btn btn-secondary">
    65                             <?php esc_html_e('View Ticket', 'nexlifydesk'); ?>
    66                         </a>
    67                     </div>
    68                 </div>
    69             <?php endforeach; ?>
    70         </div>
     75                        </td>
     76                        <td data-label="<?php esc_attr_e('Actions', 'nexlifydesk'); ?>">
     77                            <a href="<?php echo esc_url(add_query_arg('ticket_id', $ticket->ticket_id, get_permalink())); ?>" class="nexlifydesk-view-btn">
     78                                <?php esc_html_e('View', 'nexlifydesk'); ?>
     79                            </a>
     80                        </td>
     81                    </tr>
     82                <?php endforeach; ?>
     83            </tbody>
     84        </table>
     85       
     86        <footer class="nexlifydesk-table-footer">
     87            <p>
     88                <?php
     89                printf(
     90                    /* translators: %s: Number of tickets */
     91                    esc_html__('Showing %s tickets', 'nexlifydesk'),
     92                    esc_html(count($user_tickets))
     93                );
     94                ?>
     95            </p>
     96        </footer>
    7197    <?php endif; ?>
    7298</div>
  • nexlifydesk/trunk/templates/frontend/ticket-single.php

    r3330741 r3333095  
    4444
    4545$current_user = wp_get_current_user();
    46 $is_agent = in_array('nexlifydesk_agent', $current_user->roles) || current_user_can('manage_options');
     46// SECURITY FIX: Frontend access should ONLY be for ticket owners
     47// Agents and administrators should ONLY access tickets from admin panel
    4748$is_ticket_owner = ($ticket && (int)$ticket->user_id === (int)$current_user->ID);
    48 $can_view_ticket = $is_agent || $is_ticket_owner;
     49
     50// Handle unregistered user tickets (user_id = 0) by checking customer_email
     51if (!$is_ticket_owner && $ticket && (int)$ticket->user_id === 0) {
     52    $customer_email = get_post_meta($ticket->id, 'customer_email', true);
     53    $is_ticket_owner = ($customer_email && $customer_email === $current_user->user_email);
     54}
     55
     56$can_view_ticket = $is_ticket_owner;
    4957
    5058if (!$ticket || !$can_view_ticket) {
     
    6775$initial_attachments = NexlifyDesk_Tickets::get_attachments($ticket->id);
    6876
    69 // Extract customer details from ticket content for non-registered users
    7077$customer_details = nexlifydesk_extract_customer_details($ticket->message);
    7178$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
Note: See TracChangeset for help on using the changeset viewer.