Plugin Directory

Changeset 3330741


Ignore:
Timestamp:
07/19/2025 10:20:01 PM (7 months ago)
Author:
nexlifycreator
Message:

Released version 1.0.1 – Added Email Piping with OAuth2, bug fixes, and improvements.

Location:
nexlifydesk/trunk
Files:
15 edited

Legend:

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

    r3326104 r3330741  
    210210.nexlifydesk-admin-ticket-list-ui {
    211211    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    212     max-width: 1200px;
     212    max-width: 1440px;
     213    border-radius: 10px;
     214    background-color: white;
     215    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    213216    margin: 0 auto;
    214217    padding: 20px;
     
    334337}
    335338
    336 .nexlifydesk-admin-ticket-list-ui .ticket-grid {
     339/* Gmail-style ticket list */
     340.nexlifydesk-admin-ticket-list-ui .bulk-actions {
     341    display: flex;
     342    align-items: center;
     343    gap: 10px;
     344    margin-right: auto;
     345}
     346
     347.nexlifydesk-admin-ticket-list-ui .bulk-action-dropdown {
     348    padding: 8px 12px;
     349    border: 1px solid #e2e8f0;
     350    border-radius: 6px;
     351    background: white;
     352    font-size: 14px;
     353}
     354
     355.nexlifydesk-admin-ticket-list-ui .ticket-list {
     356    background: white;
     357    border-radius: 8px;
     358    border: 1px solid #e2e8f0;
     359    overflow: hidden;
     360}
     361
     362.nexlifydesk-admin-ticket-list-ui .ticket-list-header {
    337363    display: grid;
    338     gap: 1.5rem;
    339     grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
    340 }
    341 
    342 .nexlifydesk-admin-ticket-list-ui .loading-tickets {
    343     grid-column: 1 / -1;
     364    grid-template-columns: 40px 3fr 2fr 80px 80px 120px 100px;
     365    gap: 12px;
     366    padding: 12px 16px;
     367    background: #f8fafc;
     368    border-bottom: 1px solid #e2e8f0;
     369    font-size: 12px;
     370    font-weight: 600;
     371    color: #64748b;
     372    text-transform: uppercase;
     373    letter-spacing: 0.05em;
     374}
     375
     376.nexlifydesk-admin-ticket-list-ui .ticket-row {
     377    display: grid;
     378    grid-template-columns: 40px 3fr 2fr 80px 80px 120px 100px;
     379    gap: 12px;
     380    padding: 16px;
     381    border-bottom: 1px solid #f1f5f9;
     382    transition: background-color 0.2s ease;
     383    cursor: pointer;
     384}
     385
     386.nexlifydesk-admin-ticket-list-ui .ticket-row.unread {
     387    background-color: #f8fafc;
     388    border-left: 4px solid #3b82f6;
     389}
     390
     391@media (max-width: 1200px) {
     392    .nexlifydesk-admin-ticket-list-ui .ticket-list-header,
     393    .nexlifydesk-admin-ticket-list-ui .ticket-row {
     394        grid-template-columns: 40px 4fr 2fr 70px 70px 100px 90px;
     395        gap: 8px;
     396    }
     397}
     398
     399@media (max-width: 992px) {
     400    .nexlifydesk-admin-ticket-list-ui .ticket-list-header,
     401    .nexlifydesk-admin-ticket-list-ui .ticket-row {
     402        grid-template-columns: 40px 5fr 2fr 60px 60px 80px 80px;
     403        gap: 6px;
     404    }
     405}
     406
     407.nexlifydesk-admin-ticket-list-ui .ticket-row:hover {
     408    background-color: #f8fafc;
     409}
     410
     411.nexlifydesk-admin-ticket-list-ui .ticket-row:last-child {
     412    border-bottom: none;
     413}
     414
     415.nexlifydesk-admin-ticket-list-ui .row-checkbox,
     416.nexlifydesk-admin-ticket-list-ui .header-checkbox {
     417    display: flex;
     418    align-items: center;
     419    justify-content: center;
     420}
     421
     422.nexlifydesk-admin-ticket-list-ui .row-subject {
     423    display: flex;
     424    flex-direction: column;
     425    gap: 4px;
     426    min-width: 0;
     427}
     428
     429.nexlifydesk-admin-ticket-list-ui .ticket-link {
     430    text-decoration: none;
     431    color: inherit;
     432    display: flex;
     433    align-items: center;
     434    gap: 8px;
     435    min-width: 0;
     436}
     437
     438.nexlifydesk-admin-ticket-list-ui .ticket-id {
     439    font-size: 12px;
     440    color: #667eea;
     441    font-weight: 600;
     442    flex-shrink: 0;
     443}
     444
     445.nexlifydesk-admin-ticket-list-ui .ticket-title {
     446    font-size: 14px;
     447    font-weight: 600;
     448    color: #1e293b;
     449    overflow: hidden;
     450    text-overflow: ellipsis;
     451    white-space: nowrap;
     452    min-width: 0;
     453}
     454
     455.nexlifydesk-admin-ticket-list-ui .ticket-title.unread-title {
     456    font-weight: 700;
     457    color: #1e40af;
     458}
     459
     460.nexlifydesk-admin-ticket-list-ui .unread-dot {
     461    display: inline-block;
     462    width: 8px;
     463    height: 8px;
     464    background-color: #3b82f6;
     465    border-radius: 50%;
     466    margin-right: 8px;
     467    flex-shrink: 0;
     468    animation: pulse 2s infinite;
     469}
     470
     471@keyframes pulse {
     472    0% {
     473        opacity: 1;
     474    }
     475    50% {
     476        opacity: 0.5;
     477    }
     478    100% {
     479        opacity: 1;
     480    }
     481}
     482
     483.nexlifydesk-admin-ticket-list-ui .ticket-preview {
     484    font-size: 12px;
     485    color: #64748b;
     486    line-height: 1.4;
     487    overflow: hidden;
     488    text-overflow: ellipsis;
     489    white-space: nowrap;
     490}
     491
     492.nexlifydesk-admin-ticket-list-ui .row-customer {
     493    display: flex;
     494    flex-direction: column;
     495    gap: 2px;
     496    min-width: 0;
     497}
     498
     499.nexlifydesk-admin-ticket-list-ui .customer-name {
     500    font-size: 14px;
     501    font-weight: 500;
     502    color: #1e293b;
     503    overflow: hidden;
     504    text-overflow: ellipsis;
     505    white-space: nowrap;
     506}
     507
     508.nexlifydesk-admin-ticket-list-ui .customer-email {
     509    font-size: 12px;
     510    color: #64748b;
     511    overflow: hidden;
     512    text-overflow: ellipsis;
     513    white-space: nowrap;
     514}
     515
     516.nexlifydesk-admin-ticket-list-ui .status-badge {
     517    padding: 4px 8px;
     518    border-radius: 12px;
     519    font-size: 11px;
     520    font-weight: 600;
     521    text-transform: uppercase;
     522    letter-spacing: 0.05em;
     523    display: inline-block;
     524}
     525
     526.nexlifydesk-admin-ticket-list-ui .status-open {
     527    background: #dbeafe;
     528    color: #1e40af;
     529}
     530
     531.nexlifydesk-admin-ticket-list-ui .status-in_progress,
     532.nexlifydesk-admin-ticket-list-ui .status-in-progress {
     533    background: #fef3c7;
     534    color: #92400e;
     535}
     536
     537.nexlifydesk-admin-ticket-list-ui .status-resolved {
     538    background: #d1fae5;
     539    color: #065f46;
     540}
     541
     542.nexlifydesk-admin-ticket-list-ui .status-closed {
     543    background: #f3f4f6;
     544    color: #374151;
     545}
     546
     547.nexlifydesk-admin-ticket-list-ui .priority-badge {
     548    padding: 4px 8px;
     549    border-radius: 12px;
     550    font-size: 11px;
     551    font-weight: 600;
     552    text-transform: uppercase;
     553    letter-spacing: 0.05em;
     554    display: inline-block;
     555}
     556
     557.nexlifydesk-admin-ticket-list-ui .priority-high {
     558    background: #fee2e2;
     559    color: #dc2626;
     560}
     561
     562.nexlifydesk-admin-ticket-list-ui .priority-medium {
     563    background: #fef3c7;
     564    color: #d97706;
     565}
     566
     567.nexlifydesk-admin-ticket-list-ui .priority-low {
     568    background: #f0f9ff;
     569    color: #0369a1;
     570}
     571
     572.nexlifydesk-admin-ticket-list-ui .assignee-info {
     573    display: flex;
     574    align-items: center;
     575    gap: 8px;
     576}
     577
     578.nexlifydesk-admin-ticket-list-ui .assignee-info .avatar {
     579    width: 24px;
     580    height: 24px;
     581    border-radius: 50%;
     582    background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
     583    display: flex;
     584    align-items: center;
     585    justify-content: center;
     586    color: white;
     587    font-size: 10px;
     588    font-weight: 600;
     589}
     590
     591.nexlifydesk-admin-ticket-list-ui .assignee-info span {
     592    font-size: 12px;
     593    color: #1e293b;
     594}
     595
     596.nexlifydesk-admin-ticket-list-ui .unassigned {
     597    font-size: 12px;
     598    color: #94a3b8;
     599    font-style: italic;
     600}
     601
     602.nexlifydesk-admin-ticket-list-ui .row-date {
     603    display: flex;
     604    flex-direction: column;
     605    gap: 2px;
     606}
     607
     608.nexlifydesk-admin-ticket-list-ui .date-time {
     609    font-size: 12px;
     610    color: #1e293b;
     611    font-weight: 500;
     612}
     613
     614.nexlifydesk-admin-ticket-list-ui .time-ago {
     615    font-size: 11px;
     616    color: #64748b;
     617}
     618
     619.nexlifydesk-admin-ticket-list-ui .no-tickets {
    344620    text-align: center;
    345621    padding: 2rem;
     
    348624}
    349625
     626.nexlifydesk-admin-ticket-list-ui .loading-tickets {
     627    text-align: center;
     628    padding: 2rem;
     629    font-size: 16px;
     630    color: #64748b;
     631}
     632
    350633.nexlifydesk-admin-ticket-list-ui .loading-tickets .spinner {
    351634    float: none;
     
    353636}
    354637
    355 .nexlifydesk-admin-ticket-list-ui .ticket-card {
     638/* Real-time updates - smooth transitions */
     639.nexlifydesk-admin-ticket-list-ui .ticket-row {
     640    transition: all 0.3s ease;
     641}
     642
     643.nexlifydesk-admin-ticket-list-ui .ticket-row.updating {
     644    opacity: 0.7;
     645}
     646
     647.nexlifydesk-admin-ticket-list-ui .ticket-row.new-reply {
     648    animation: highlight 3s ease-out;
     649}
     650
     651@keyframes highlight {
     652    0% {
     653        background-color: #dbeafe;
     654        border-left-color: #3b82f6;
     655    }
     656    100% {
     657        background-color: inherit;
     658        border-left-color: inherit;
     659    }
     660}
     661
     662/* Bulk action modals */
     663.bulk-modal {
     664    position: fixed;
     665    top: 0;
     666    left: 0;
     667    width: 100%;
     668    height: 100%;
     669    background: rgba(0, 0, 0, 0.5);
     670    display: flex;
     671    align-items: center;
     672    justify-content: center;
     673    z-index: 10000;
     674}
     675
     676.bulk-modal .modal-content {
    356677    background: white;
    357     border-radius: 12px;
    358     padding: 1.5rem;
    359     box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
     678    padding: 24px;
     679    border-radius: 8px;
     680    min-width: 400px;
     681    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
     682}
     683
     684.bulk-modal h3 {
     685    margin: 0 0 16px 0;
     686    font-size: 18px;
     687    color: #1e293b;
     688}
     689
     690.bulk-modal select {
     691    width: 100%;
     692    padding: 8px 12px;
    360693    border: 1px solid #e2e8f0;
    361     transition: all 0.3s ease;
    362     cursor: pointer;
    363 }
    364 
    365 .nexlifydesk-admin-ticket-list-ui .ticket-card:hover {
    366     transform: translateY(-2px);
    367     box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
    368     border-color: #667eea;
    369 }
    370 
    371 .nexlifydesk-admin-ticket-list-ui .ticket-header {
    372     display: flex;
    373     justify-content: space-between;
    374     align-items: flex-start;
    375     margin-bottom: 1rem;
    376 }
    377 
    378 .nexlifydesk-admin-ticket-list-ui .ticket-id {
    379     font-size: 0.875rem;
    380     color: #667eea;
    381     font-weight: 600;
    382 }
    383 
    384 .nexlifydesk-admin-ticket-list-ui .ticket-title {
    385     font-size: 1.125rem;
    386     font-weight: 600;
    387     color: #1e293b;
    388     margin: 0.5rem 0;
    389     line-height: 1.4;
    390 }
    391 
    392 .nexlifydesk-admin-ticket-list-ui .ticket-description {
    393     color: #64748b;
    394     margin-bottom: 1rem;
    395     line-height: 1.5;
    396 }
    397 
    398 .nexlifydesk-admin-ticket-list-ui .ticket-meta {
    399     display: flex;
    400     flex-wrap: wrap;
    401     gap: 0.75rem;
    402     align-items: center;
    403     margin-bottom: 1rem;
    404 }
    405 
    406 .nexlifydesk-admin-ticket-list-ui .status-badge {
    407     padding: 0.25rem 0.75rem;
    408     border-radius: 20px;
    409     font-size: 0.75rem;
    410     font-weight: 600;
    411     text-transform: uppercase;
    412     letter-spacing: 0.05em;
    413 }
    414 
    415 .nexlifydesk-admin-ticket-list-ui .status-open {
    416     background: #dbeafe;
    417     color: #1e40af;
    418 }
    419 
    420 .nexlifydesk-admin-ticket-list-ui .status-in_progress,
    421 .nexlifydesk-admin-ticket-list-ui .status-in-progress {
    422     background: #fef3c7;
    423     color: #92400e;
    424 }
    425 
    426 .nexlifydesk-admin-ticket-list-ui .status-resolved {
    427     background: #d1fae5;
    428     color: #065f46;
    429 }
    430 
    431 .nexlifydesk-admin-ticket-list-ui .status-closed {
    432     background: #f3f4f6;
    433     color: #374151;
    434 }
    435 
    436 .nexlifydesk-admin-ticket-list-ui .priority-badge {
    437     padding: 0.25rem 0.75rem;
    438     border-radius: 20px;
    439     font-size: 0.75rem;
    440     font-weight: 600;
    441     text-transform: uppercase;
    442     letter-spacing: 0.05em;
    443 }
    444 
    445 .nexlifydesk-admin-ticket-list-ui .priority-high {
    446     background: #fee2e2;
    447     color: #dc2626;
    448 }
    449 
    450 .nexlifydesk-admin-ticket-list-ui .priority-medium {
    451     background: #fef3c7;
    452     color: #d97706;
    453 }
    454 
    455 .nexlifydesk-admin-ticket-list-ui .priority-low {
    456     background: #f0f9ff;
    457     color: #0369a1;
    458 }
    459 
    460 .nexlifydesk-admin-ticket-list-ui .ticket-footer {
    461     display: flex;
    462     justify-content: space-between;
    463     align-items: center;
    464     padding-top: 1rem;
    465     border-top: 1px solid #f1f5f9;
    466     font-size: 0.875rem;
    467     color: #64748b;
    468 }
    469 
    470 .nexlifydesk-admin-ticket-list-ui .assignee {
    471     display: flex;
    472     align-items: center;
    473     gap: 0.5rem;
    474 }
    475 
    476 .nexlifydesk-admin-ticket-list-ui .avatar {
    477     width: 24px;
    478     height: 24px;
    479     border-radius: 50%;
    480     background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
    481     display: flex;
    482     align-items: center;
    483     justify-content: center;
    484     color: white;
    485     font-size: 0.75rem;
    486     font-weight: 600;
     694    border-radius: 6px;
     695    margin-bottom: 16px;
     696}
     697
     698.bulk-modal .modal-actions {
     699    display: flex;
     700    gap: 8px;
     701    justify-content: flex-end;
     702}
     703
     704/* Delete modal specific styles */
     705.bulk-modal .selected-tickets-info {
     706    background-color: #f8f9fa;
     707    padding: 8px 12px;
     708    border-radius: 4px;
     709    font-size: 14px;
     710    color: #555;
     711    margin: 12px 0;
     712}
     713
     714.bulk-modal .delete-option {
     715    color: #d63638;
    487716}
    488717
     
    499728        flex-direction: column;
    500729        align-items: stretch;
     730        gap: 12px;
     731    }
     732
     733    .nexlifydesk-admin-ticket-list-ui .bulk-actions {
     734        order: 2;
    501735    }
    502736
    503737    .nexlifydesk-admin-ticket-list-ui .search-bar {
    504738        min-width: unset;
    505     }
    506 
    507     .nexlifydesk-admin-ticket-list-ui .ticket-grid {
     739        order: 1;
     740    }
     741
     742    .nexlifydesk-admin-ticket-list-ui .ticket-list-header {
     743        display: none;
     744    }
     745
     746    .nexlifydesk-admin-ticket-list-ui .ticket-row {
    508747        grid-template-columns: 1fr;
     748        gap: 8px;
     749        padding: 12px;
     750    }
     751
     752    .nexlifydesk-admin-ticket-list-ui .row-checkbox {
     753        position: absolute;
     754        top: 12px;
     755        right: 12px;
    509756    }
    510757
    511758    .nexlifydesk-admin-ticket-list-ui .stats {
    512759        grid-template-columns: repeat(2, 1fr);
     760    }
     761
     762    .bulk-modal .modal-content {
     763        min-width: 90vw;
     764        margin: 20px;
    513765    }
    514766}
     
    9761228}
    9771229
     1230/* Encryption Key Instructions Styling */
     1231.nexlifydesk-encryption-key-instructions {
     1232    background: #f8f9fa;
     1233    border: 1px solid #e1e5e9;
     1234    border-radius: 5px;
     1235    padding: 20px;
     1236    margin: 20px 0;
     1237}
     1238
     1239.nexlifydesk-encryption-key-instructions h3 {
     1240    margin-top: 0;
     1241    color: #2c3e50;
     1242}
     1243
     1244.nexlifydesk-encryption-key-instructions h4 {
     1245    color: #34495e;
     1246    margin-top: 20px;
     1247    margin-bottom: 10px;
     1248}
     1249
     1250.nexlifydesk-key-generation {
     1251    background: #fff;
     1252    border: 1px solid #ddd;
     1253    border-radius: 3px;
     1254    padding: 15px;
     1255    margin: 15px 0;
     1256}
     1257
     1258.nexlifydesk-key-installation {
     1259    background: #fff;
     1260    border: 1px solid #ddd;
     1261    border-radius: 3px;
     1262    padding: 15px;
     1263    margin: 15px 0;
     1264}
     1265
     1266.nexlifydesk-key-status {
     1267    background: #fff;
     1268    border: 1px solid #ddd;
     1269    border-radius: 3px;
     1270    padding: 15px;
     1271    margin: 15px 0;
     1272}
     1273
     1274.nexlifydesk-key-status p {
     1275    margin: 5px 0;
     1276}
     1277
     1278.nexlifydesk-key-status .dashicons {
     1279    font-size: 16px;
     1280    width: 16px;
     1281    height: 16px;
     1282    margin-right: 5px;
     1283}
     1284
     1285#encryption-key-output {
     1286    background: #f9f9f9;
     1287    border: 1px solid #ccc;
     1288    border-radius: 3px;
     1289    padding: 10px;
     1290    font-family: Consolas, Monaco, monospace;
     1291    font-size: 12px;
     1292    resize: vertical;
     1293}
     1294
    9781295/* Internal Notes */
    9791296.nexlifydesk-admin-single-ticket-ui .message.internal-note {
     
    10181335    margin-bottom: 16px;
    10191336}
     1337
     1338/* IMAP Authentication Spam Protection Styles */
     1339.nexlifydesk-spam-protection {
     1340    border-top: 1px solid #ccd0d4;
     1341    padding-top: 20px;
     1342    margin-top: 20px;
     1343}
     1344
     1345.nexlifydesk-spam-protection h3 {
     1346    color: #23282d;
     1347    margin-bottom: 15px;
     1348}
     1349
     1350.nexlifydesk-spam-protection .form-table th {
     1351    width: 200px;
     1352}
     1353
     1354.nexlifydesk-spam-protection textarea {
     1355    font-family: 'Courier New', monospace;
     1356    font-size: 12px;
     1357}
     1358
     1359.nexlifydesk-spam-protection .description {
     1360    color: #666;
     1361    font-style: italic;
     1362}
  • nexlifydesk/trunk/assets/js/nexlifydesk.js

    r3326104 r3330741  
    7272            }
    7373            $error.text('');
     74           
     75            var totalSize = 0;
    7476            for (var i = 0; i < files.length; i++) {
    7577                var file = files[i];
    7678                var fileExt = file.name.split('.').pop().toLowerCase();
     79                totalSize += file.size;
     80               
    7781                if (file.size > maxSize) {
    7882                    $error.text('File "' + file.name + '" is too large. Maximum size is ' + (maxSize / 1024 / 1024) + 'MB.');
     
    8488                    $fileInput.val('');
    8589                    return;
     90                }
     91            }
     92           
     93            var maxTotalSize = maxSize * 10;
     94            if (totalSize > maxTotalSize * 0.8) {
     95                var totalSizeMB = (totalSize / 1024 / 1024).toFixed(1);
     96                var maxTotalSizeMB = (maxTotalSize / 1024 / 1024).toFixed(1);
     97                if (totalSize > maxTotalSize) {
     98                    $error.text('Total file size (' + totalSizeMB + 'MB) exceeds limit of ' + maxTotalSizeMB + 'MB. Please reduce file sizes.');
     99                    $fileInput.val('');
     100                    return;
     101                } else {
     102                    $error.css('color', 'orange').text('Warning: Large file size (' + totalSizeMB + 'MB). Upload may take longer.');
    86103                }
    87104            }
     
    142159                },
    143160                error: function(xhr, status, error) {
     161                    var errorMsg = nexlifydesk_vars.error_message || 'An error occurred. Please try again.';
     162                   
     163                    if (xhr.status === 413) {
     164                        errorMsg = 'Request too large. Please reduce file sizes or number of files and try again.';
     165                    } else if (xhr.status === 504 || xhr.status === 502) {
     166                        errorMsg = 'Server timeout. Please try again or contact support if the issue persists.';
     167                    } else if (xhr.status === 500) {
     168                        errorMsg = 'Server error. Please try again or contact support if the issue persists.';
     169                    }
     170                   
    144171                    $('#nexlifydesk-message')
    145172                        .removeClass('success')
    146173                        .addClass('error')
    147                         .text(nexlifydesk_vars.error_message || 'An error occurred. Please try again.')
     174                        .text(errorMsg)
    148175                        .show();
    149176                },
     
    569596                    } else {
    570597                        $tableContainer.html('<tr><td colspan="5">' + nexlifydesk_admin_vars.error_loading_tickets_text + ' ' + response.data + '</td></tr>');
    571                         console.error('Error fetching admin tickets:', response.data);
    572598                    }
    573599                },
    574600                error: function(xhr, status, error) {
    575601                    $tableContainer.html('<tr><td colspan="5">' + nexlifydesk_admin_vars.ajax_error_loading_tickets_text + '</td></tr>');
    576                     console.error('AJAX Error fetching admin tickets:', status, error, xhr.responseText);
    577602                }
    578603            });
     
    618643
    619644            var formHtml = '<tr class="edit-position-row"><td colspan="4">';
    620             formHtml += '<form class="nexlifydesk-edit-position-form" method="post" action="' + nexlifydesk_admin_vars.admin_post_url + '">';
     645            formHtml += '<form class="nexlifydesk-edit-position-form" method="post" action="' + nexlifydesk_vars.admin_post_url + '">';
    621646            formHtml += '<input type="hidden" name="action" value="nexlifydesk_save_agent_position">';
    622647            formHtml += '<input type="hidden" name="edit_position" value="1">';
     
    687712    }
    688713
    689     function loadTickets(status, search) {
    690         if (!isPluginValid) return;
    691        
    692         $('#nexlifydesk-tickets-list').html('<p>' + nexlifydesk_admin_vars.loading_tickets_text + '</p>');
    693        
    694         $.ajax({
    695             url: nexlifydesk_admin_vars.ajaxurl,
    696             type: 'POST',
    697             data: {
    698                 action: 'nexlifydesk_admin_get_tickets',
    699                 status: status || 'all',
    700                 search: search || '',
    701                 nonce: nexlifydesk_admin_vars.nonce
    702             },
    703             success: function(response) {
    704                 if (response.success && response.data) {
    705                     displayTickets(response.data);
    706                 } else {
    707                     var errorMsg = response.data || nexlifydesk_vars.no_tickets_found_text;
    708                     $('#nexlifydesk-tickets-list').html('<p>' + errorMsg + '</p>');
    709                 }
    710             },
    711             error: function(xhr, status, error) {
    712                
    713                 var errorMsg = nexlifydesk_admin_vars.ajax_error_loading_tickets_text;
    714                 if (xhr.responseText) {
    715                     try {
    716                         var errorResponse = JSON.parse(xhr.responseText);
    717                         if (errorResponse.data) {
    718                             errorMsg += ' ' + errorResponse.data;
    719                         }
    720                     } catch (e) {
    721                         errorMsg += ' ' + xhr.responseText.substring(0, 100);
    722                     }
    723                 }
    724                
    725                 $('#nexlifydesk-tickets-list').html('<p style="color: red;">' + errorMsg + '</p>');
    726             }
    727         });
    728     }
    729 
    730     function displayTickets(tickets) {
    731         var texts = window.nexlifydesk_tickets_list_texts || {};
    732        
    733         if (!tickets || tickets.length === 0) {
    734             $('#nexlifydesk-tickets-list').html('<p>' + (texts.no_tickets_found || 'No tickets found.') + '</p>');
    735             return;
    736         }
    737 
    738         var html = '<table class="wp-list-table widefat fixed striped">';
    739         html += '<thead><tr>';
    740         html += '<th>' + (texts.ticket_id || 'Ticket ID') + '</th>';
    741         html += '<th>' + (texts.subject || 'Subject') + '</th>';
    742         html += '<th>' + (texts.status || 'Status') + '</th>';
    743         html += '<th>' + (texts.assigned_to || 'Assigned To') + '</th>';
    744         html += '<th>' + (texts.created || 'Created') + '</th>';
    745         html += '<th>' + (texts.actions || 'Actions') + '</th>';
    746         html += '</tr></thead><tbody>';
    747 
    748         $.each(tickets, function(index, ticket) {
    749             html += '<tr>';
    750             html += '<td>' + escapeHtml(ticket.ticket_id) + '</td>';
    751             html += '<td>' + nl2br_js(escapeHtml(ticket.subject)) + '</td>';
    752             html += '<td><span class="status-' + escapeHtml(ticket.status) + '">' + escapeHtml(ticket.status.charAt(0).toUpperCase() + ticket.status.slice(1)) + '</span></td>';
    753             html += '<td>' + escapeHtml(ticket.assigned_to_display_name) + '</td>';
    754             html += '<td>' + escapeHtml(ticket.created_at) + '</td>';
    755             html += '<td><a href="?page=nexlifydesk_tickets&ticket_id=' + ticket.id + '" class="button button-small">' + (texts.view || 'View') + '</a></td>';
    756             html += '</tr>';
    757         });
    758 
    759         html += '</tbody></table>';
    760         $('#nexlifydesk-tickets-list').html(html);
    761     }
    762 
    763     function nl2br_js(str) {
    764         if (!str) return '';
    765         return str.replace(/\n/g, '<br>');
    766     }
    767 
    768     function escapeHtml(text) {
    769         if (!text) return '';
    770         var map = {
    771             '&': '&amp;',
    772             '<': '&lt;',
    773             '>': '&gt;',
    774             '"': '&quot;',
    775             "'": '&#039;'
    776         };
    777         return text.toString().replace(/[&<>"']/g, function(m) { return map[m]; });
    778     }
    779 
    780     $(document).ready(function() {
    781         if ($('#nexlifydesk-tickets-list').length) {
    782             if (typeof loadTickets === 'function') {
    783                 loadTickets();
    784             }
    785            
    786             $('#ticket-filter-btn').on('click', function() {
    787                 var status = $('#ticket-status-filter').val();
    788                 var search = $('#ticket-search').val();
    789                 if (typeof loadTickets === 'function') {
    790                     loadTickets(status, search);
    791                 }
    792             });
    793            
    794             $('#ticket-search').on('keypress', function(e) {
    795                 if (e.which == 13) {
    796                     $('#ticket-filter-btn').click();
    797                 }
    798             });
    799         }
    800     });
    801714})(jQuery);
    802715
     
    11111024    $(function() {
    11121025        if ($('.nexlifydesk-admin-ticket-list-ui').length) {
    1113             loadTickets();
    11141026
    11151027            let searchTimeout;
     
    12651177                    button.prop('disabled', false).text('Purge Data');
    12661178                    resultDiv.html('<span style="color: red;">Error purging data. Please try again.<br>JS: ' + error + '</span>');
    1267                     console.error('[NexlifyDesk] AJAX error:', error, xhr);
    12681179                });
    12691180            }
    12701181        });
     1182    }
     1183});
     1184
     1185jQuery(document).ready(function($) {
     1186    var $clearRateLimitBtn = $('#nexlifydesk_clear_rate_limit');
     1187    var $checkRateLimitBtn = $('#nexlifydesk_check_rate_limit');
     1188   
     1189    if ($clearRateLimitBtn.length) {
     1190        $clearRateLimitBtn.off('click').on('click', function() {
     1191            var button = $(this);
     1192            var resultDiv = $('#nexlifydesk_clear_rate_limit_result');
     1193            var emailInput = $('#nexlifydesk_clear_rate_limit_email');
     1194            var email = emailInput.val().trim();
     1195           
     1196            resultDiv.html('');
     1197           
     1198            if (!email || !isValidEmail(email)) {
     1199                resultDiv.html('<span style="color: red;">Please enter a valid email address.</span>');
     1200                return;
     1201            }
     1202           
     1203            var ajaxurl = (typeof nexlifydesk_vars !== 'undefined' && nexlifydesk_vars.ajaxurl) ? nexlifydesk_vars.ajaxurl : (typeof ajaxurl !== 'undefined' ? ajaxurl : window.ajaxurl);
     1204            var nonce = (typeof nexlifydesk_admin_vars !== 'undefined' && nexlifydesk_admin_vars.nonce) ? nexlifydesk_admin_vars.nonce : '';
     1205           
     1206            button.prop('disabled', true).text('Clearing...');
     1207            $.post(ajaxurl, {
     1208                action: 'nexlifydesk_clear_rate_limit',
     1209                email: email,
     1210                nonce: nonce
     1211            }, function(response) {
     1212                button.prop('disabled', false).text('Clear Rate Limit');
     1213                if (response && response.success) {
     1214                    resultDiv.html('<span style="color: green;">' + response.data + '</span>');
     1215                    emailInput.val('');
     1216                } else {
     1217                    resultDiv.html('<span style="color: red;">' + (response && response.data ? response.data : 'Error clearing rate limit. Please try again.') + '</span>');
     1218                }
     1219            }).fail(function(xhr, status, error) {
     1220                button.prop('disabled', false).text('Clear Rate Limit');
     1221                resultDiv.html('<span style="color: red;">Error clearing rate limit. Please try again.</span>');
     1222            });
     1223        });
     1224    }
     1225   
     1226    if ($checkRateLimitBtn.length) {
     1227        $checkRateLimitBtn.off('click').on('click', function() {
     1228            var button = $(this);
     1229            var resultDiv = $('#nexlifydesk_check_rate_limit_result');
     1230            var emailInput = $('#nexlifydesk_check_rate_limit_email');
     1231            var email = emailInput.val().trim();
     1232           
     1233            resultDiv.html('');
     1234           
     1235            if (!email || !isValidEmail(email)) {
     1236                resultDiv.html('<span style="color: red;">Please enter a valid email address.</span>');
     1237                return;
     1238            }
     1239           
     1240            var ajaxurl = (typeof nexlifydesk_vars !== 'undefined' && nexlifydesk_vars.ajaxurl) ? nexlifydesk_vars.ajaxurl : (typeof ajaxurl !== 'undefined' ? ajaxurl : window.ajaxurl);
     1241            var nonce = (typeof nexlifydesk_admin_vars !== 'undefined' && nexlifydesk_admin_vars.nonce) ? nexlifydesk_admin_vars.nonce : '';
     1242           
     1243            button.prop('disabled', true).text('Checking...');
     1244            $.post(ajaxurl, {
     1245                action: 'nexlifydesk_check_rate_limit',
     1246                email: email,
     1247                nonce: nonce
     1248            }, function(response) {
     1249                button.prop('disabled', false).text('Check Status');
     1250                if (response && response.success) {
     1251                    resultDiv.html('<span style="color: blue;">' + response.data + '</span>');
     1252                } else {
     1253                    resultDiv.html('<span style="color: red;">' + (response && response.data ? response.data : 'Error checking rate limit. Please try again.') + '</span>');
     1254                }
     1255            }).fail(function(xhr, status, error) {
     1256                button.prop('disabled', false).text('Check Status');
     1257                resultDiv.html('<span style="color: red;">Error checking rate limit. Please try again.</span>');
     1258            });
     1259        });
     1260    }
     1261   
     1262    function isValidEmail(email) {
     1263        var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
     1264        return emailRegex.test(email);
    12711265    }
    12721266});
     
    12911285    });
    12921286});
     1287
     1288jQuery(document).ready(function($){
     1289    if (typeof ajaxurl === 'undefined') {
     1290        var ajaxurl = window.nexlifydesk_admin_vars ? window.nexlifydesk_admin_vars.ajaxurl : '';
     1291    }
     1292   
     1293    function toggleProviderSettings() {
     1294        var provider = $('#provider-select').val();
     1295        $('.provider-settings').hide();
     1296        $('#' + provider + '-settings').show();
     1297       
     1298        if (provider === 'aws') {
     1299            var awsIntegrationType = $('#aws_integration_type').val() || 'imap';
     1300            if (awsIntegrationType === 'imap') {
     1301                $('#aws-imap-settings').show();
     1302                $('#aws-lambda-settings').hide();
     1303            } else {
     1304                $('#aws-imap-settings').hide();
     1305                $('#aws-lambda-settings').show();
     1306            }
     1307        } else {
     1308            $('#aws-imap-settings').hide();
     1309            $('#aws-lambda-settings').hide();
     1310        }
     1311       
     1312        var sslEnabled = window.nexlifydesk_ssl_enabled || false;
     1313       
     1314        if (!sslEnabled && provider === 'aws') {
     1315            // alert('Note: SSL is recommended for AWS WorkMail for security purposes.');
     1316        }
     1317    }
     1318   
     1319    if ($('#provider-select').length) {
     1320        toggleProviderSettings();
     1321        $('#provider-select').on('change', toggleProviderSettings);
     1322    }
     1323
     1324    $('#aws_integration_type').on('change', function() {
     1325        const integrationType = $(this).val();
     1326       
     1327        if (integrationType === 'imap') {
     1328            $('#aws-imap-settings').show();
     1329            $('#aws-lambda-settings').hide();
     1330        } else if (integrationType === 'lambda') {
     1331            $('#aws-imap-settings').hide();
     1332            $('#aws-lambda-settings').show();
     1333        }
     1334    });
     1335   
     1336    $('#nexlifydesk-fetch-emails-now').on('click', function(){
     1337        var $btn = $(this);
     1338        var $status = $('#nexlifydesk-fetch-emails-status');
     1339        $btn.prop('disabled', true);
     1340        $status.text('Fetching...');
     1341       
     1342        $.post(ajaxurl, {
     1343            action: 'nexlifydesk_fetch_emails_now',
     1344            _ajax_nonce: window.nexlifydesk_fetch_emails_nonce || ''
     1345        }, function(response){
     1346            $btn.prop('disabled', false);
     1347            if(response.success){
     1348                $status.text('Fetch complete!');
     1349            }else{
     1350                $status.text(response.data || 'Fetch failed.');
     1351            }
     1352        }).fail(function(){
     1353            $btn.prop('disabled', false);
     1354            $status.text('Fetch failed.');
     1355        });
     1356    });
     1357   
     1358    $('#test-aws-connection').on('click', function(){
     1359        var $btn = $(this);
     1360        var $result = $('#aws-connection-result');
     1361       
     1362        $btn.prop('disabled', true).text('Testing...');
     1363        $result.html('<p style="color: #666;">Connecting to AWS WorkMail...</p>');
     1364       
     1365        $.post(ajaxurl, {
     1366            action: 'nexlifydesk_test_aws_connection',
     1367            nonce: window.nexlifydesk_aws_test_nonce || '',
     1368            region: $('#aws_region').val(),
     1369            organization_id: $('#aws_organization_id').val(),
     1370            email: $('#aws_email').val(),
     1371            password: $('#aws_password').val(),
     1372            access_key_id: $('#aws_access_key_id').val(),
     1373            secret_access_key: $('#aws_secret_access_key').val()
     1374        }).done(function(response) {
     1375            $btn.prop('disabled', false).text('Test AWS Connection');
     1376            if (response.success) {
     1377                $result.html('<p style="color: green;">✅ ' + response.data.message + '</p>');
     1378            } else {
     1379                $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>');
     1380            }
     1381        }).fail(function() {
     1382            $btn.prop('disabled', false).text('Test AWS Connection');
     1383            $result.html('<p style="color: red;">❌ Connection test failed.</p>');
     1384        });
     1385    });
     1386
     1387    $('#test-aws-fetch-emails').on('click', function(){
     1388        var $btn = $(this);
     1389        var $result = $('#aws-fetch-result');
     1390       
     1391        $btn.prop('disabled', true).text('Fetching Emails...');
     1392        $result.html('<p style="color: #666;">Attempting to fetch emails from AWS WorkMail...</p>');
     1393       
     1394        $.post(ajaxurl, {
     1395            action: 'nexlifydesk_manual_fetch_emails',
     1396            nonce: window.nexlifydesk_aws_test_nonce || '',
     1397            region: $('#aws_region').val(),
     1398            organization_id: $('#aws_organization_id').val(),
     1399            email: $('#aws_email').val(),
     1400            password: $('#aws_password').val(),
     1401            access_key_id: $('#aws_access_key_id').val(),
     1402            secret_access_key: $('#aws_secret_access_key').val()
     1403        }).done(function(response) {
     1404            $btn.prop('disabled', false).text('Test Email Fetch');
     1405            if (response.success) {
     1406                var message = response.data.message.replace(/\n/g, '<br>');
     1407                $result.html('<div style="color: green; font-family: monospace; white-space: pre-wrap; background: #f0f9ff; padding: 10px; border-left: 4px solid #0087be;">📧 ' + message + '</div>');
     1408            } else {
     1409                $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>');
     1410            }
     1411        }).fail(function() {
     1412            $btn.prop('disabled', false).text('Test Email Fetch');
     1413            $result.html('<p style="color: red;">❌ Email fetch test failed.</p>');
     1414        });
     1415    });
     1416
     1417    $('#test-google-connection').on('click', function(){
     1418        var $btn = $(this);
     1419        var $result = $('#google-connection-result');
     1420       
     1421        $btn.prop('disabled', true).text('Testing...');
     1422        $result.html('<p style="color: #666;">Testing Google connection...</p>');
     1423       
     1424        $.post(ajaxurl, {
     1425            action: 'nexlifydesk_test_google_connection',
     1426            nonce: window.nexlifydesk_google_test_nonce || '',
     1427            client_id: $('#google_client_id').val(),
     1428            client_secret: $('#google_client_secret').val()
     1429        }).done(function(response) {
     1430            $btn.prop('disabled', false).text('Test Google Connection');
     1431            if (response.success) {
     1432                $result.html('<p style="color: green;">✅ ' + response.data.message + '</p>');
     1433            } else {
     1434                $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>');
     1435            }
     1436        }).fail(function() {
     1437            $btn.prop('disabled', false).text('Test Google Connection');
     1438            $result.html('<p style="color: red;">❌ Google connection test failed.</p>');
     1439        });
     1440    });
     1441
     1442    $('#test-google-fetch-emails').on('click', function(){
     1443        var $btn = $(this);
     1444        var $result = $('#google-fetch-result');
     1445       
     1446        $btn.prop('disabled', true).text('Fetching Emails...');
     1447        $result.html('<p style="color: #666;">Attempting to fetch emails from Google...</p>');
     1448       
     1449        $.post(ajaxurl, {
     1450            action: 'nexlifydesk_manual_fetch_google_emails',
     1451            nonce: window.nexlifydesk_google_test_nonce || ''
     1452        }).done(function(response) {
     1453            $btn.prop('disabled', false).text('Test Email Fetch');
     1454            if (response.success) {
     1455                var message = response.data.message.replace(/\n/g, '<br>');
     1456                $result.html('<div style="color: green; font-family: monospace; white-space: pre-wrap; background: #f0f9ff; padding: 10px; border-left: 4px solid #0087be;">📧 ' + message + '</div>');
     1457            } else {
     1458                $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>');
     1459            }
     1460        }).fail(function() {
     1461            $btn.prop('disabled', false).text('Test Email Fetch');
     1462            $result.html('<p style="color: red;">❌ Google email fetch test failed.</p>');
     1463        });
     1464    });
     1465
     1466    $('#test-custom-connection').on('click', function(){
     1467        var $btn = $(this);
     1468        var $result = $('#custom-connection-result');
     1469       
     1470        $btn.prop('disabled', true).text('Testing...');
     1471        $result.html('<p style="color: #666;">Testing custom IMAP/POP3 connection...</p>');
     1472       
     1473        $.post(ajaxurl, {
     1474            action: 'nexlifydesk_test_custom_connection',
     1475            nonce: window.nexlifydesk_custom_test_nonce || '',
     1476            host: $('#host').val(),
     1477            port: $('#port').val(),
     1478            username: $('#username').val(),
     1479            password: $('#password').val(),
     1480            encryption: $('#encryption').val(),
     1481            protocol: $('#protocol').val()
     1482        }).done(function(response) {
     1483            $btn.prop('disabled', false).text('Test Custom Connection');
     1484            if (response.success) {
     1485                $result.html('<p style="color: green;">✅ ' + response.data.message + '</p>');
     1486            } else {
     1487                $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>');
     1488            }
     1489        }).fail(function() {
     1490            $btn.prop('disabled', false).text('Test Custom Connection');
     1491            $result.html('<p style="color: red;">❌ Custom connection test failed.</p>');
     1492        });
     1493    });
     1494
     1495    $('#test-custom-fetch-emails').on('click', function(){
     1496        var $btn = $(this);
     1497        var $result = $('#custom-fetch-result');
     1498       
     1499        $btn.prop('disabled', true).text('Fetching Emails...');
     1500        $result.html('<p style="color: #666;">Attempting to fetch emails from custom server...</p>');
     1501       
     1502        $.post(ajaxurl, {
     1503            action: 'nexlifydesk_manual_fetch_emails',
     1504            nonce: window.nexlifydesk_custom_test_nonce || '',
     1505            provider: 'custom'
     1506        }).done(function(response) {
     1507            $btn.prop('disabled', false).text('Test Email Fetch');
     1508            if (response.success) {
     1509                var message = response.data.message.replace(/\n/g, '<br>');
     1510                $result.html('<div style="color: green; font-family: monospace; white-space: pre-wrap; background: #f0f9ff; padding: 10px; border-left: 4px solid #0087be;">📧 ' + message + '</div>');
     1511            } else {
     1512                $result.html('<p style="color: red;">❌ ' + response.data.message + '</p>');
     1513            }
     1514        }).fail(function() {
     1515            $btn.prop('disabled', false).text('Test Email Fetch');
     1516            $result.html('<p style="color: red;">❌ Custom email fetch test failed.</p>');
     1517        });
     1518    });
     1519});
     1520
     1521jQuery(document).ready(function($){
     1522    $('#edit-custom-password').on('click', function() {
     1523        const passwordField = $('#password');
     1524        const button = $(this);
     1525        const hiddenField = $('#password-preserved-flag');
     1526       
     1527        passwordField.prop('disabled', false)
     1528                    .prop('readonly', false)
     1529                    .css('background-color', '#fff')
     1530                    .css('border', '1px solid #8c8f94')
     1531                    .css('cursor', 'text')
     1532                    .attr('placeholder', 'Enter new password')
     1533                    .val('')
     1534                    .focus();
     1535       
     1536        hiddenField.remove();
     1537       
     1538        button.text('Cancel').off('click').on('click', function() {
     1539            passwordField.prop('disabled', true)
     1540                        .prop('readonly', true)
     1541                        .css('background-color', '#f7f7f7')
     1542                        .css('border', '1px solid #ddd')
     1543                        .css('cursor', 'not-allowed')
     1544                        .attr('placeholder', '')
     1545                        .val('••••••••••••••••');
     1546           
     1547            passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[password_preserved]" value="1" id="password-preserved-flag">');
     1548           
     1549            button.text('Edit Password').off('click').on('click', arguments.callee);
     1550        });
     1551    });
     1552   
     1553    $('#edit-aws-password').on('click', function() {
     1554        const passwordField = $('#aws_password');
     1555        const button = $(this);
     1556        const hiddenField = $('#aws-password-preserved-flag');
     1557       
     1558        passwordField.prop('disabled', false)
     1559                    .prop('readonly', false)
     1560                    .css('background-color', '#fff')
     1561                    .css('border', '1px solid #8c8f94')
     1562                    .css('cursor', 'text')
     1563                    .attr('placeholder', 'Enter new password')
     1564                    .val('')
     1565                    .focus();
     1566       
     1567        hiddenField.remove();
     1568       
     1569        button.text('Cancel').off('click').on('click', function() {
     1570            passwordField.prop('disabled', true)
     1571                        .prop('readonly', true)
     1572                        .css('background-color', '#f7f7f7')
     1573                        .css('border', '1px solid #ddd')
     1574                        .css('cursor', 'not-allowed')
     1575                        .attr('placeholder', '')
     1576                        .val('••••••••••••••••');
     1577           
     1578            passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[aws_password_preserved]" value="1" id="aws-password-preserved-flag">');
     1579           
     1580            button.text('Edit Password').off('click').on('click', arguments.callee);
     1581        });
     1582    });
     1583   
     1584    $('#edit-aws-secret-key').on('click', function() {
     1585        const passwordField = $('#aws_secret_access_key');
     1586        const button = $(this);
     1587        const hiddenField = $('#aws-secret-key-preserved-flag');
     1588       
     1589        passwordField.prop('disabled', false)
     1590                    .prop('readonly', false)
     1591                    .css('background-color', '#fff')
     1592                    .css('border', '1px solid #8c8f94')
     1593                    .css('cursor', 'text')
     1594                    .attr('placeholder', 'Enter new secret key')
     1595                    .val('')
     1596                    .focus();
     1597       
     1598        hiddenField.remove();
     1599       
     1600        button.text('Cancel').off('click').on('click', function() {
     1601            passwordField.prop('disabled', true)
     1602                        .prop('readonly', true)
     1603                        .css('background-color', '#f7f7f7')
     1604                        .css('border', '1px solid #ddd')
     1605                        .css('cursor', 'not-allowed')
     1606                        .attr('placeholder', '')
     1607                        .val('••••••••••••••••');
     1608           
     1609            passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[aws_secret_access_key_preserved]" value="1" id="aws-secret-key-preserved-flag">');
     1610           
     1611            button.text('Edit Secret Key').off('click').on('click', arguments.callee);
     1612        });
     1613    });
     1614   
     1615    $('#edit-google-secret').on('click', function() {
     1616        const passwordField = $('#google_client_secret');
     1617        const button = $(this);
     1618        const hiddenField = $('#google-secret-preserved-flag');
     1619       
     1620        passwordField.prop('disabled', false)
     1621                    .prop('readonly', false)
     1622                    .css('background-color', '#fff')
     1623                    .css('border', '1px solid #8c8f94')
     1624                    .css('cursor', 'text')
     1625                    .attr('placeholder', 'Enter new client secret')
     1626                    .val('')
     1627                    .focus();
     1628       
     1629        hiddenField.remove();
     1630       
     1631        button.text('Cancel').off('click').on('click', function() {
     1632            passwordField.prop('disabled', true)
     1633                        .prop('readonly', true)
     1634                        .css('background-color', '#f7f7f7')
     1635                        .css('border', '1px solid #ddd')
     1636                        .css('cursor', 'not-allowed')
     1637                        .attr('placeholder', '')
     1638                        .val('••••••••••••••••');
     1639           
     1640            passwordField.after('<input type="hidden" name="nexlifydesk_imap_settings[google_client_secret_preserved]" value="1" id="google-secret-preserved-flag">');
     1641           
     1642            button.text('Edit Secret').off('click').on('click', arguments.callee);
     1643        });
     1644    });
     1645   
     1646    $('#aws_integration_type').on('change', function() {
     1647        const integrationType = $(this).val();
     1648       
     1649        if (integrationType === 'lambda') {
     1650            alert('Lambda integration is coming soon! Please use IMAP integration for now.');
     1651            $(this).val('imap');
     1652            $('#aws-imap-settings').show();
     1653            $('#aws-lambda-settings').hide();
     1654        } else {
     1655            $('#aws-imap-settings').show();
     1656            $('#aws-lambda-settings').hide();
     1657        }
     1658    });
     1659   
     1660    $(document).ready(function() {
     1661        $('#aws_integration_type option[value="lambda"]').prop('disabled', true);
     1662    });
     1663   
     1664    $('#regenerate-webhook-secret').on('click', function() {
     1665        const newSecret = generateRandomString(32);
     1666        $('#lambda_webhook_secret').val(newSecret);
     1667    });
     1668   
     1669    $('#copy-webhook-url').on('click', function() {
     1670        const webhookUrl = $(this).prev('input').val();
     1671        navigator.clipboard.writeText(webhookUrl).then(function() {
     1672            alert('Webhook URL copied to clipboard!');
     1673        });
     1674    });
     1675   
     1676    $('#test-lambda-webhook').on('click', function() {
     1677        const button = $(this);
     1678        const resultDiv = $('#lambda-webhook-result');
     1679       
     1680        button.prop('disabled', true).text('Testing...');
     1681        resultDiv.html('<p>Testing webhook connection...</p>');
     1682       
     1683        const testData = {
     1684            from: '[email protected]',
     1685            to: '[email protected]',
     1686            subject: 'Test Email from Lambda',
     1687            body: 'This is a test email to verify the Lambda webhook integration.',
     1688            message_id: 'test-' + Date.now(),
     1689            timestamp: Math.floor(Date.now() / 1000)
     1690        };
     1691       
     1692        $.ajax({
     1693            url: $('#copy-webhook-url').prev('input').val(),
     1694            method: 'POST',
     1695            contentType: 'application/json',
     1696            headers: {
     1697                'X-NexlifyDesk-Secret': $('#lambda_webhook_secret').val()
     1698            },
     1699            data: JSON.stringify(testData),
     1700            success: function(response) {
     1701                resultDiv.html('<div style="color: green;"><strong>✓ Webhook test successful!</strong><br>Response: ' + JSON.stringify(response) + '</div>');
     1702            },
     1703            error: function(xhr, status, error) {
     1704                resultDiv.html('<div style="color: red;"><strong>✗ Webhook test failed!</strong><br>Error: ' + error + '<br>Status: ' + xhr.status + '</div>');
     1705            },
     1706            complete: function() {
     1707                button.prop('disabled', false).text('Test Webhook');
     1708            }
     1709        });
     1710    });
     1711   
     1712    function generateRandomString(length) {
     1713        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
     1714        let result = '';
     1715        for (let i = 0; i < length; i++) {
     1716            result += chars.charAt(Math.floor(Math.random() * chars.length));
     1717        }
     1718        return result;
     1719    }
     1720   
     1721    $('#provider-select').on('change', function() {
     1722        $('input[type="password"]').each(function() {
     1723            const field = $(this);
     1724            const fieldId = field.attr('id');
     1725            const editButton = $('#edit-' + fieldId.replace('_', '-'));
     1726           
     1727            if (editButton.length && field.val() === '••••••••••••••••') {
     1728                field.prop('disabled', true)
     1729                    .prop('readonly', true)
     1730                    .css('background-color', '#f7f7f7')
     1731                    .css('border', '1px solid #ddd')
     1732                    .css('cursor', 'not-allowed');
     1733               
     1734                editButton.text(editButton.text().replace('Cancel', 'Edit'));
     1735            }
     1736        });
     1737    });
     1738});
     1739
     1740jQuery(document).ready(function($) {
     1741    $('#generate-encryption-key').on('click', function() {
     1742        $.ajax({
     1743            url: ajaxurl,
     1744            type: 'POST',
     1745            data: {
     1746                action: 'nexlifydesk_generate_encryption_key',
     1747                nonce: window.nexlifydesk_generate_key_nonce || ''
     1748            },
     1749            success: function(response) {
     1750                if (response.success) {
     1751                    $('#encryption-key-output').val(response.data.key);
     1752                    $('#generated-key-display').show();
     1753                } else {
     1754                    alert('Error generating key: ' + response.data);
     1755                }
     1756            },
     1757            error: function() {
     1758                alert('Error generating encryption key. Please try again.');
     1759            }
     1760        });
     1761    });
     1762   
     1763    $('#copy-encryption-key').on('click', function() {
     1764        const keyField = document.getElementById('encryption-key-output');
     1765        keyField.select();
     1766        keyField.setSelectionRange(0, 99999);
     1767       
     1768        navigator.clipboard.writeText(keyField.value).then(function() {
     1769            alert('Encryption key copied to clipboard!');
     1770        }, function(err) {
     1771            document.execCommand('copy');
     1772            alert('Encryption key copied to clipboard!');
     1773        });
     1774    });
     1775});
  • nexlifydesk/trunk/includes/class-nexlifydesk-admin.php

    r3326104 r3330741  
    1515        add_action('admin_post_nexlifydesk_delete_agent_position', array(__CLASS__, 'handle_delete_agent_position'));
    1616        add_action('admin_post_nexlifydesk_save_agent_assignments', array(__CLASS__, 'handle_save_agent_assignments'));
     17        add_action('admin_post_nexlifydesk_save_imap_settings', array(__CLASS__, 'save_imap_settings'));
     18        add_action('admin_init', ['NexlifyDesk_Admin', 'maybe_encrypt_existing_passwords']);
     19        add_action('wp_ajax_nexlifydesk_refresh_ticket_list', array(__CLASS__, 'ajax_refresh_ticket_list'));
     20        add_action('wp_ajax_nexlifydesk_mark_ticket_read', array(__CLASS__, 'ajax_mark_ticket_read'));
     21        add_action('wp_ajax_nexlifydesk_clear_rate_limit', array(__CLASS__, 'ajax_clear_rate_limit'));
     22        add_action('wp_ajax_nexlifydesk_check_rate_limit', array(__CLASS__, 'ajax_check_rate_limit'));
     23        add_action('admin_post_nexlifydesk_deauth_google', array(__CLASS__, 'handle_deauth_google'));
     24        add_action('admin_post_nexlifydesk_deauth_aws', array(__CLASS__, 'handle_deauth_aws'));
     25        add_action('admin_post_nexlifydesk_deauth_custom', array(__CLASS__, 'handle_deauth_custom'));
     26        add_action('admin_post_nexlifydesk_deauth_all', array(__CLASS__, 'handle_deauth_all'));
     27
     28        register_setting('nexlifydesk_settings', 'nexlifydesk_imap_settings', [
     29            'type' => 'array',
     30            'sanitize_callback' => array(__CLASS__, 'sanitize_imap_settings'),
     31        ]);
     32    }
     33
     34    /**
     35     * Sanitize IMAP settings input
     36     */
     37    public static function sanitize_imap_settings($input) {
     38       
     39        $new_input = get_option('nexlifydesk_imap_settings', []);
     40
     41        $new_input['enabled'] = isset($input['enabled']) ? 1 : 0;
     42        $new_input['provider'] = sanitize_text_field($input['provider'] ?? 'custom');
     43       
     44        $fetch_interval = absint($input['fetch_interval'] ?? 5);
     45        $allowed_intervals = [2, 5, 10, 15];
     46        $new_input['fetch_interval'] = in_array($fetch_interval, $allowed_intervals) ? $fetch_interval : 5;
     47       
     48        $is_ssl_enabled = function_exists('nexlifydesk_check_ssl_enabled') ? nexlifydesk_check_ssl_enabled() : true;
     49       
     50        $provider = $new_input['provider'] ?? 'custom';
     51       
     52        if ($provider === 'aws') {
     53            $is_ssl_enabled = true;
     54        }
     55       
     56        $new_input['protocol'] = sanitize_text_field($input['protocol'] ?? 'imap');
     57        $new_input['host'] = sanitize_text_field($input['host'] ?? '');
     58        $new_input['port'] = absint($input['port'] ?? 993);
     59        $new_input['encryption'] = sanitize_text_field($input['encryption'] ?? 'ssl');
     60        $new_input['username'] = sanitize_text_field($input['username'] ?? '');
     61       
     62        if (!empty($input['password']) && $input['password'] !== '••••••••••••••••') {
     63            $plain = sanitize_text_field($input['password']);
     64            $new_input['password'] = nexlifydesk_encrypt($plain);
     65        } elseif (!empty($input['password_preserved'])) {
     66            $new_input['password'] = $new_input['password'] ?? '';
     67        } else {
     68            $new_input['password'] = '';
     69        }
     70
     71        $new_input['google_client_id'] = sanitize_text_field($input['google_client_id'] ?? '');
     72       
     73        if (!empty($input['google_client_secret']) && $input['google_client_secret'] !== '••••••••••••••••') {
     74            $plain = sanitize_text_field($input['google_client_secret']);
     75            $new_input['google_client_secret'] = nexlifydesk_encrypt($plain);
     76        } elseif (!empty($input['google_client_secret_preserved'])) {
     77            $new_input['google_client_secret'] = $new_input['google_client_secret'] ?? '';
     78        } else {
     79            $new_input['google_client_secret'] = '';
     80        }
     81       
     82        if (isset($input['google_access_token'])) {
     83            $new_input['google_access_token'] = sanitize_text_field($input['google_access_token']);
     84        }
     85        if (isset($input['google_refresh_token'])) {
     86            $new_input['google_refresh_token'] = sanitize_text_field($input['google_refresh_token']);
     87        }
     88        if (isset($input['google_account_email'])) {
     89            $new_input['google_account_email'] = sanitize_email($input['google_account_email']);
     90        }
     91
     92        $new_input['aws_region'] = sanitize_text_field($input['aws_region'] ?? '');
     93        $new_input['aws_organization_id'] = sanitize_text_field($input['aws_organization_id'] ?? '');
     94        $new_input['aws_email'] = sanitize_email($input['aws_email'] ?? '');
     95       
     96        if (!empty($input['aws_password']) && $input['aws_password'] !== '••••••••••••••••') {
     97            $plain = sanitize_text_field($input['aws_password']);
     98            $new_input['aws_password'] = nexlifydesk_encrypt($plain);
     99        } elseif (!empty($input['aws_password_preserved'])) {
     100            $new_input['aws_password'] = $new_input['aws_password'] ?? '';
     101        } else {
     102            $new_input['aws_password'] = '';
     103        }
     104       
     105        $new_input['aws_access_key_id'] = sanitize_text_field($input['aws_access_key_id'] ?? '');
     106       
     107        if (!empty($input['aws_secret_access_key']) && $input['aws_secret_access_key'] !== '••••••••••••••••') {
     108            $plain = sanitize_text_field($input['aws_secret_access_key']);
     109            $new_input['aws_secret_access_key'] = nexlifydesk_encrypt($plain);
     110        } elseif (!empty($input['aws_secret_access_key_preserved'])) {
     111            $new_input['aws_secret_access_key'] = $new_input['aws_secret_access_key'] ?? '';
     112        } else {
     113            $new_input['aws_secret_access_key'] = '';
     114        }
     115
     116        $new_input['delete_emails_after_fetch'] = isset($input['delete_emails_after_fetch']) ? 1 : 0;
     117        $new_input['block_admin_emails'] = isset($input['block_admin_emails']) ? 1 : 0;
     118        $new_input['block_notification_subjects'] = isset($input['block_notification_subjects']) ? 1 : 0;
     119        $new_input['block_marketing_emails'] = isset($input['block_marketing_emails']) ? 1 : 0;
     120        $new_input['blocked_emails'] = sanitize_textarea_field($input['blocked_emails'] ?? '');
     121        $new_input['blocked_domains'] = sanitize_textarea_field($input['blocked_domains'] ?? '');
     122        $new_input['spam_url_filtering'] = isset($input['spam_url_filtering']) ? 1 : 0;
     123        $new_input['max_links_per_email'] = absint($input['max_links_per_email'] ?? 3);
     124        $new_input['blocked_keywords'] = sanitize_textarea_field($input['blocked_keywords'] ?? '');
     125        $new_input['aws_integration_type'] = sanitize_text_field($input['aws_integration_type'] ?? 'imap');
     126        if ($new_input['aws_integration_type'] === 'lambda') {
     127            $new_input['aws_integration_type'] = 'imap';
     128        }
     129        $new_input['lambda_webhook_secret'] = sanitize_text_field($input['lambda_webhook_secret'] ?? '');
     130
     131        return $new_input;
     132    }
     133
     134    /**
     135     * Save IMAP settings from the IMAP Auth settings page
     136     */
     137    public static function save_imap_settings() {
     138        if (!current_user_can('manage_options')) {
     139            wp_die(esc_html__('You do not have permission.', 'nexlifydesk'));
     140        }
     141        check_admin_referer('nexlifydesk_save_imap_settings', 'nexlifydesk_imap_settings_nonce');
     142       
     143        $old_settings = get_option('nexlifydesk_imap_settings', []);
     144       
     145        $settings = array(
     146            'enabled' => isset($_POST['enabled']) ? 1 : 0,
     147            'provider' => sanitize_text_field(isset($_POST['provider']) ? wp_unslash($_POST['provider']) : ''),
     148            'host' => sanitize_text_field(isset($_POST['host']) ? wp_unslash($_POST['host']) : ''),
     149            'port' => absint(isset($_POST['port']) ? wp_unslash($_POST['port']) : 993),
     150            'encryption' => sanitize_text_field(isset($_POST['encryption']) ? wp_unslash($_POST['encryption']) : ''),
     151            'username' => sanitize_text_field(isset($_POST['username']) ? wp_unslash($_POST['username']) : ''),
     152            'protocol' => sanitize_text_field(isset($_POST['protocol']) ? wp_unslash($_POST['protocol']) : 'imap'),
     153        );
     154       
     155        $password_input = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : '';
     156        $password_preserved = isset($_POST['password_preserved']) ? sanitize_text_field(wp_unslash($_POST['password_preserved'])) : '';
     157       
     158        if (!empty($password_input)) {
     159            $settings['password'] = nexlifydesk_encrypt($password_input);
     160        } elseif (!empty($password_preserved)) {
     161            $settings['password'] = $old_settings['password'] ?? '';
     162        } else {
     163            $settings['password'] = '';
     164        }
     165       
     166        $aws_password_input = isset($_POST['aws_password']) ? sanitize_text_field(wp_unslash($_POST['aws_password'])) : '';
     167        $aws_password_preserved = isset($_POST['aws_password_preserved']) ? sanitize_text_field(wp_unslash($_POST['aws_password_preserved'])) : '';
     168       
     169        if (!empty($aws_password_input)) {
     170            $settings['aws_password'] = nexlifydesk_encrypt($aws_password_input);
     171        } elseif (!empty($aws_password_preserved)) {
     172            $settings['aws_password'] = $old_settings['aws_password'] ?? '';
     173        } else {
     174            $settings['aws_password'] = '';
     175        }
     176       
     177        $aws_secret_input = isset($_POST['aws_secret_access_key']) ? sanitize_text_field(wp_unslash($_POST['aws_secret_access_key'])) : '';
     178        $aws_secret_preserved = isset($_POST['aws_secret_access_key_preserved']) ? sanitize_text_field(wp_unslash($_POST['aws_secret_access_key_preserved'])) : '';
     179       
     180        if (!empty($aws_secret_input)) {
     181            $settings['aws_secret_access_key'] = nexlifydesk_encrypt($aws_secret_input);
     182        } elseif (!empty($aws_secret_preserved)) {
     183            $settings['aws_secret_access_key'] = $old_settings['aws_secret_access_key'] ?? '';
     184        } else {
     185            $settings['aws_secret_access_key'] = '';
     186        }
     187       
     188        $google_secret_input = isset($_POST['google_client_secret']) ? sanitize_text_field(wp_unslash($_POST['google_client_secret'])) : '';
     189        $google_secret_preserved = isset($_POST['google_client_secret_preserved']) ? sanitize_text_field(wp_unslash($_POST['google_client_secret_preserved'])) : '';
     190       
     191        if (!empty($google_secret_input)) {
     192            $settings['google_client_secret'] = nexlifydesk_encrypt($google_secret_input);
     193        } elseif (!empty($google_secret_preserved)) {
     194            $settings['google_client_secret'] = $old_settings['google_client_secret'] ?? '';
     195        } else {
     196            $settings['google_client_secret'] = '';
     197        }
     198       
     199        $settings['aws_integration_type'] = sanitize_text_field(isset($_POST['aws_integration_type']) ? wp_unslash($_POST['aws_integration_type']) : 'imap');
     200        if ($settings['aws_integration_type'] === 'lambda') {
     201            $settings['aws_integration_type'] = 'imap';
     202        }
     203        $settings['lambda_webhook_secret'] = sanitize_text_field(isset($_POST['lambda_webhook_secret']) ? wp_unslash($_POST['lambda_webhook_secret']) : '');
     204        $settings = array_merge($old_settings, $settings);
     205       
     206        update_option('nexlifydesk_imap_settings', $settings);
     207        wp_redirect(add_query_arg(array('page' => 'nexlifydesk_imap_auth', 'settings-updated' => 'true'), admin_url('admin.php')));
     208        exit;
     209    }
     210
     211    /**
     212     * Handle Google de-authentication
     213     */
     214    public static function handle_deauth_google() {
     215        if (!current_user_can('manage_options')) {
     216            wp_die(esc_html__('You do not have permission.', 'nexlifydesk'));
     217        }
     218       
     219        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_google')) {
     220            wp_die(esc_html__('Security check failed.', 'nexlifydesk'));
     221        }
     222       
     223        $settings = get_option('nexlifydesk_imap_settings', []);
     224       
     225        unset($settings['google_access_token']);
     226        unset($settings['google_refresh_token']);
     227        unset($settings['google_client_id']);
     228        unset($settings['google_client_secret']);
     229        unset($settings['google_auth_status']);
     230        unset($settings['google_account_email']);
     231        unset($settings['google_token_expires_at']);
     232        unset($settings['google_fetch_start_time']);
     233       
     234        $settings['google_access_token'] = '';
     235        $settings['google_refresh_token'] = '';
     236        $settings['google_client_id'] = '';
     237        $settings['google_client_secret'] = '';
     238        $settings['google_auth_status'] = '';
     239        $settings['google_account_email'] = '';
     240        $settings['google_token_expires_at'] = '';
     241        $settings['google_fetch_start_time'] = '';
     242       
     243        update_option('nexlifydesk_imap_settings', $settings);
     244       
     245        delete_option('nexlifydesk_processed_emails');
     246       
     247        wp_redirect(add_query_arg(array(
     248            'page' => 'nexlifydesk_imap_auth',
     249            'deauth' => 'google_success',
     250            '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth')
     251        ), admin_url('admin.php')));
     252        exit;
     253    }
     254
     255    /**
     256     * Handle AWS de-authentication
     257     */
     258    public static function handle_deauth_aws() {
     259        if (!current_user_can('manage_options')) {
     260            wp_die(esc_html__('You do not have permission.', 'nexlifydesk'));
     261        }
     262       
     263        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_aws')) {
     264            wp_die(esc_html__('Security check failed.', 'nexlifydesk'));
     265        }
     266       
     267        $settings = get_option('nexlifydesk_imap_settings', []);
     268       
     269        unset($settings['aws_region']);
     270        unset($settings['aws_organization_id']);
     271        unset($settings['aws_email']);
     272        unset($settings['aws_password']);
     273        unset($settings['aws_access_key_id']);
     274        unset($settings['aws_secret_access_key']);
     275        unset($settings['aws_fetch_start_time']);
     276       
     277        $settings['aws_region'] = '';
     278        $settings['aws_organization_id'] = '';
     279        $settings['aws_email'] = '';
     280        $settings['aws_password'] = '';
     281        $settings['aws_access_key_id'] = '';
     282        $settings['aws_secret_access_key'] = '';
     283        $settings['aws_fetch_start_time'] = '';
     284       
     285        update_option('nexlifydesk_imap_settings', $settings);
     286       
     287        delete_option('nexlifydesk_aws_processed_emails');
     288       
     289        wp_redirect(add_query_arg(array(
     290            'page' => 'nexlifydesk_imap_auth',
     291            'deauth' => 'aws_success',
     292            '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth')
     293        ), admin_url('admin.php')));
     294        exit;
     295    }
     296
     297    /**
     298     * Handle Custom IMAP/POP3 de-authentication
     299     */
     300    public static function handle_deauth_custom() {
     301        if (!current_user_can('manage_options')) {
     302            wp_die(esc_html__('You do not have permission.', 'nexlifydesk'));
     303        }
     304       
     305        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_custom')) {
     306            wp_die(esc_html__('Security check failed.', 'nexlifydesk'));
     307        }
     308       
     309        $settings = get_option('nexlifydesk_imap_settings', []);
     310       
     311        unset($settings['host']);
     312        unset($settings['port']);
     313        unset($settings['username']);
     314        unset($settings['password']);
     315        unset($settings['encryption']);
     316        unset($settings['protocol']);
     317       
     318        $settings['host'] = '';
     319        $settings['port'] = 993;
     320        $settings['username'] = '';
     321        $settings['password'] = '';
     322        $settings['encryption'] = 'ssl';
     323        $settings['protocol'] = 'imap';
     324       
     325        update_option('nexlifydesk_imap_settings', $settings);
     326       
     327        wp_redirect(add_query_arg(array(
     328            'page' => 'nexlifydesk_imap_auth',
     329            'deauth' => 'custom_success',
     330            '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth')
     331        ), admin_url('admin.php')));
     332        exit;
     333    }
     334
     335    /**
     336     * Handle complete de-authentication (all providers)
     337     */
     338    public static function handle_deauth_all() {
     339        if (!current_user_can('manage_options')) {
     340            wp_die(esc_html__('You do not have permission.', 'nexlifydesk'));
     341        }
     342       
     343        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'nexlifydesk_deauth_all')) {
     344            wp_die(esc_html__('Security check failed.', 'nexlifydesk'));
     345        }
     346       
     347        $current_settings = get_option('nexlifydesk_imap_settings', []);
     348        $credential_keys = [
     349            // Custom IMAP/POP3 credentials
     350            'host', 'port', 'username', 'password', 'encryption', 'protocol',
     351           
     352            // Google credentials
     353            'google_client_id', 'google_client_secret', 'google_access_token',
     354            'google_refresh_token', 'google_auth_status', 'google_account_email',
     355            'google_token_expires_at', 'google_fetch_start_time',
     356           
     357            // AWS credentials
     358            'aws_region', 'aws_organization_id', 'aws_email', 'aws_password',
     359            'aws_access_key_id', 'aws_secret_access_key', 'aws_fetch_start_time'
     360        ];
     361       
     362        foreach ($credential_keys as $key) {
     363            unset($current_settings[$key]);
     364        }
     365       
     366        $default_settings = array(
     367            'enabled' => 0,
     368            'provider' => 'custom',
     369            'fetch_interval' => 5,
     370            'delete_emails_after_fetch' => 1,
     371           
     372            // Custom IMAP/POP3 settings
     373            'host' => '',
     374            'port' => 993,
     375            'username' => '',
     376            'password' => '',
     377            'encryption' => 'ssl',
     378            'protocol' => 'imap',
     379           
     380            // Google settings
     381            'google_client_id' => '',
     382            'google_client_secret' => '',
     383            'google_access_token' => '',
     384            'google_refresh_token' => '',
     385            'google_auth_status' => '',
     386            'google_account_email' => '',
     387            'google_token_expires_at' => '',
     388            'google_fetch_start_time' => '',
     389           
     390            // AWS settings
     391            'aws_region' => '',
     392            'aws_organization_id' => '',
     393            'aws_email' => '',
     394            'aws_password' => '',
     395            'aws_access_key_id' => '',
     396            'aws_secret_access_key' => '',
     397            'aws_fetch_start_time' => '',
     398           
     399            // Spam protection settings (preserve these)
     400            'block_admin_emails' => 1,
     401            'block_notification_subjects' => 1,
     402            'block_marketing_emails' => 1,
     403            'blocked_emails' => '',
     404            'blocked_domains' => '',
     405            'spam_url_filtering' => 1,
     406            'max_links_per_email' => 3,
     407            'blocked_keywords' => '',
     408        );
     409       
     410        $spam_settings = [
     411            'block_admin_emails', 'block_notification_subjects', 'block_marketing_emails',
     412            'blocked_emails', 'blocked_domains', 'spam_url_filtering', 'max_links_per_email', 'blocked_keywords'
     413        ];
     414       
     415        foreach ($spam_settings as $setting) {
     416            if (isset($current_settings[$setting])) {
     417                $default_settings[$setting] = $current_settings[$setting];
     418            }
     419        }
     420       
     421        $final_settings = array_merge($current_settings, $default_settings);
     422       
     423        update_option('nexlifydesk_imap_settings', $final_settings);
     424       
     425        delete_option('nexlifydesk_processed_emails');
     426        delete_option('nexlifydesk_aws_processed_emails');
     427       
     428        wp_redirect(add_query_arg(array(
     429            'page' => 'nexlifydesk_imap_auth',
     430            'deauth' => 'all_success',
     431            '_wpnonce' => wp_create_nonce('nexlifydesk_imap_auth')
     432        ), admin_url('admin.php')));
     433        exit;
     434    }
     435
     436    public static function maybe_encrypt_existing_passwords() {
     437        $settings = get_option('nexlifydesk_imap_settings', []);
     438        $updated = false;
     439
     440        $is_encrypted = function($value) {
     441            if (empty($value)) return false;
     442            return nexlifydesk_is_encrypted($value);
     443        };
     444
     445        // Custom IMAP/POP3 password
     446        if (!empty($settings['password']) && !$is_encrypted($settings['password'])) {
     447            $settings['password'] = nexlifydesk_encrypt($settings['password']);
     448            $updated = true;
     449        }
     450        // AWS password
     451        if (!empty($settings['aws_password']) && !$is_encrypted($settings['aws_password'])) {
     452            $settings['aws_password'] = nexlifydesk_encrypt($settings['aws_password']);
     453            $updated = true;
     454        }
     455        // Google client secret
     456        if (!empty($settings['google_client_secret']) && !$is_encrypted($settings['google_client_secret'])) {
     457            $settings['google_client_secret'] = nexlifydesk_encrypt($settings['google_client_secret']);
     458            $updated = true;
     459        }
     460        // AWS secret access key
     461        if (!empty($settings['aws_secret_access_key']) && !$is_encrypted($settings['aws_secret_access_key'])) {
     462            $settings['aws_secret_access_key'] = nexlifydesk_encrypt($settings['aws_secret_access_key']);
     463            $updated = true;
     464        }
     465        if ($updated) {
     466            update_option('nexlifydesk_imap_settings', $settings);
     467        }
    17468    }
    18469
     
    108559            array('NexlifyDesk_Admin', 'render_support_page')
    109560        );
     561
     562        // Add IMAP Auth submenu
     563        add_submenu_page(
     564            'nexlifydesk_tickets',
     565            __('IMAP Auth', 'nexlifydesk'),
     566            __('IMAP Auth', 'nexlifydesk'),
     567            'manage_options',
     568            'nexlifydesk_imap_auth',
     569            array(__CLASS__, 'render_imap_auth_page')
     570        );
    110571   
    111572    }
    112573
     574    public static function render_imap_auth_page() {
     575        ?>
     576        <div class="wrap">
     577            <h1><?php esc_html_e('Email Piping (IMAP Auth)', 'nexlifydesk'); ?></h1>
     578           
     579            <?php
     580            // Show IMAP extension warning if not available
     581            if (!extension_loaded('imap')) {
     582                echo '<div class="notice notice-error"><p><strong>' . esc_html__('Warning:', 'nexlifydesk') . '</strong> ' .
     583                     esc_html__('IMAP extension is not installed on this server. Email piping will not work for Custom IMAP/POP3 and AWS providers. Please contact your hosting provider to enable the PHP IMAP extension.', 'nexlifydesk') .
     584                     '</p></div>';
     585            }
     586            ?>
     587           
     588            <?php include NEXLIFYDESK_PLUGIN_DIR . 'templates/admin/imap-auth.php'; ?>
     589        </div>
     590        <?php
     591    }
     592
     593    /**
     594     * AJAX handler for refreshing ticket list
     595     */
     596    public static function ajax_refresh_ticket_list() {
     597        if (!current_user_can('nexlifydesk_manage_tickets')) {
     598            wp_die('Unauthorized');
     599        }
     600       
     601        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     602       
     603        $last_refresh = isset($_POST['last_refresh']) ? intval($_POST['last_refresh']) / 1000 : 0;
     604       
     605        if (class_exists('NexlifyDesk_Tickets')) {
     606            $tickets = NexlifyDesk_Tickets::get_tickets_for_refresh($last_refresh);
     607           
     608            wp_send_json_success(array(
     609                'tickets' => $tickets,
     610                'timestamp' => time()
     611            ));
     612        }
     613       
     614        wp_send_json_error('Tickets system not available');
     615    }
     616   
     617    /**
     618     * AJAX handler for marking ticket as read
     619     */
     620    public static function ajax_mark_ticket_read() {
     621        if (!current_user_can('nexlifydesk_manage_tickets')) {
     622            wp_die('Unauthorized');
     623        }
     624       
     625        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     626       
     627        $ticket_id = isset($_POST['ticket_id']) ? intval($_POST['ticket_id']) : 0;
     628       
     629        if ($ticket_id && class_exists('NexlifyDesk_Tickets')) {
     630            $success = NexlifyDesk_Tickets::mark_ticket_as_read($ticket_id, get_current_user_id());
     631           
     632            if ($success) {
     633                wp_send_json_success('Ticket marked as read');
     634            }
     635        }
     636       
     637        wp_send_json_error('Failed to mark ticket as read');
     638    }
     639   
     640    /**
     641     * AJAX handler to clear rate limit for a specific email address
     642     */
     643    public static function ajax_clear_rate_limit() {
     644        // Check permissions
     645        if (!current_user_can('manage_options')) {
     646            wp_send_json_error(__('You do not have permission to perform this action.', 'nexlifydesk'));
     647        }
     648
     649        // Verify nonce
     650        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk-ajax-nonce')) {
     651            wp_send_json_error(__('Nonce verification failed.', 'nexlifydesk'));
     652        }
     653
     654        // Get email address
     655        $email_address = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
     656       
     657        if (empty($email_address) || !is_email($email_address)) {
     658            wp_send_json_error(__('Please enter a valid email address.', 'nexlifydesk'));
     659        }
     660
     661        // Check if user exists
     662        $user = get_user_by('email', $email_address);
     663        $user_id = $user ? $user->ID : 0;
     664
     665        // Get current status before clearing
     666        $status = NexlifyDesk_Rate_Limiter::get_rate_limit_status($user_id, $email_address);
     667       
     668        if (!$status['is_limited']) {
     669            wp_send_json_error(__('This email address is not currently rate limited.', 'nexlifydesk'));
     670        }
     671
     672        // Clear the rate limit
     673        $result = NexlifyDesk_Rate_Limiter::clear_rate_limit($user_id, $email_address);
     674
     675        if ($result) {
     676            $message = sprintf(
     677                /* translators: %s: email address */
     678                __('Rate limit cleared for %s. They can now send emails again.', 'nexlifydesk'),
     679                esc_html($email_address)
     680            );
     681            wp_send_json_success($message);
     682        } else {
     683            wp_send_json_error(__('Failed to clear rate limit. Please try again.', 'nexlifydesk'));
     684        }
     685    }
     686
     687    /**
     688     * AJAX handler to check rate limit status for a specific email address
     689     */
     690    public static function ajax_check_rate_limit() {
     691        // Check permissions
     692        if (!current_user_can('manage_options')) {
     693            wp_send_json_error(__('You do not have permission to perform this action.', 'nexlifydesk'));
     694        }
     695
     696        // Verify nonce
     697        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk-ajax-nonce')) {
     698            wp_send_json_error(__('Nonce verification failed.', 'nexlifydesk'));
     699        }
     700
     701        // Get email address
     702        $email_address = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : '';
     703       
     704        if (empty($email_address) || !is_email($email_address)) {
     705            wp_send_json_error(__('Please enter a valid email address.', 'nexlifydesk'));
     706        }
     707
     708        // Check if user exists
     709        $user = get_user_by('email', $email_address);
     710        $user_id = $user ? $user->ID : 0;
     711
     712        // Get rate limit status
     713        $status = NexlifyDesk_Rate_Limiter::get_rate_limit_status($user_id, $email_address);
     714
     715        if ($status['is_limited']) {
     716            $time_remaining = NexlifyDesk_Rate_Limiter::format_time_remaining($status['time_until_reset']);
     717            $message = sprintf(
     718                /* translators: 1: email address, 2: current count, 3: max allowed, 4: time remaining */
     719                __('Rate limit status for %1$s: %2$d/%3$d emails sent. Rate limit will reset in %4$s.', 'nexlifydesk'),
     720                esc_html($email_address),
     721                $status['current_count'],
     722                $status['max_allowed'],
     723                $time_remaining
     724            );
     725        } else {
     726            $message = sprintf(
     727                /* translators: 1: email address, 2: current count, 3: max allowed */
     728                __('Rate limit status for %1$s: %2$d/%3$d emails sent. No rate limit active.', 'nexlifydesk'),
     729                esc_html($email_address),
     730                $status['current_count'],
     731                $status['max_allowed']
     732            );
     733        }
     734
     735        wp_send_json_success($message);
     736    }
     737   
     738    /**
     739     * Get the ticket page URL from settings
     740     *
     741     * @return string The ticket page URL or empty string if not set
     742     */
     743    public static function get_ticket_page_url() {
     744        $settings = get_option('nexlifydesk_settings', array());
     745        $ticket_page_id = isset($settings['ticket_page_id']) ? (int)$settings['ticket_page_id'] : 0;
     746       
     747        if ($ticket_page_id > 0) {
     748            $ticket_page_url = get_permalink($ticket_page_id);
     749            return $ticket_page_url ? $ticket_page_url : '';
     750        }
     751       
     752        return '';
     753    }
     754   
    113755    public static function render_support_page() {
    114756        if (isset($_POST['submit']) && isset($_POST['nexlify_support_nonce'])) {
     
    159801        $ticket_id = isset($_GET['ticket_id']) ? sanitize_text_field( wp_unslash( $_GET['ticket_id'] ) ) : '';
    160802
    161             if (
    162                 ($page && strpos($page, 'nexlifydesk') === 0) ||
    163                 (strpos($hook, 'nexlifydesk') !== false) ||
    164                 ($page === 'nexlifydesk_tickets' && $ticket_id)
    165             ) {
    166                 wp_enqueue_style(
    167                     'nexlifydesk-admin',
    168                     NEXLIFYDESK_PLUGIN_URL . 'assets/css/nexlifydesk-admin.css',
    169                     array(),
    170                     NEXLIFYDESK_VERSION
    171                 );
     803        if (
     804            ($page && strpos($page, 'nexlifydesk') === 0) ||
     805            (strpos($hook, 'nexlifydesk') !== false)
     806        ) {
     807            wp_enqueue_style(
     808                'nexlifydesk-admin',
     809                NEXLIFYDESK_PLUGIN_URL . 'assets/css/nexlifydesk-admin.css',
     810                array(),
     811                NEXLIFYDESK_VERSION
     812            );
     813            wp_enqueue_script(
     814                'nexlifydesk-admin',
     815                NEXLIFYDESK_PLUGIN_URL . 'assets/js/nexlifydesk.js',
     816                array('jquery'),
     817                NEXLIFYDESK_VERSION,
     818                true
     819            );
     820           
     821            // Enqueue page-specific scripts
     822            if ($page === 'nexlifydesk_tickets') {
    172823                wp_enqueue_script(
    173                     'nexlifydesk-admin',
    174                     NEXLIFYDESK_PLUGIN_URL . 'assets/js/nexlifydesk.js',
    175                     array('jquery'),
     824                    'nexlifydesk-admin-ticket-list',
     825                    NEXLIFYDESK_PLUGIN_URL . 'assets/js/admin-ticket-list.js',
     826                    array('jquery', 'nexlifydesk-admin'),
    176827                    NEXLIFYDESK_VERSION,
    177828                    true
    178829                );
     830            }
    179831
    180832            $available_capabilities = array(
     
    230882            }
    231883           
     884            if ($page === 'nexlifydesk_imap_auth' || $page === 'nexlifydesk_imap_settings') {
     885                $is_ssl_enabled = function_exists('nexlifydesk_check_ssl_enabled') ? nexlifydesk_check_ssl_enabled() : true;
     886               
     887                if (defined('NEXLIFYDESK_FORCE_SSL_ENABLED') && NEXLIFYDESK_FORCE_SSL_ENABLED) {
     888                    $is_ssl_enabled = true;
     889                }
     890               
     891                wp_add_inline_script(
     892                    'nexlifydesk-admin',
     893                    'window.nexlifydesk_ssl_enabled = ' . wp_json_encode($is_ssl_enabled) . ';' .
     894                    'window.nexlifydesk_fetch_emails_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk_fetch_emails_now')) . ';' .
     895                    'window.nexlifydesk_aws_test_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk_aws_test')) . ';' .
     896                    'window.nexlifydesk_google_test_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk-ajax-nonce')) . ';' .
     897                    'window.nexlifydesk_custom_test_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk-ajax-nonce')) . ';' .
     898                    'window.nexlifydesk_generate_key_nonce = ' . wp_json_encode(wp_create_nonce('nexlifydesk_generate_key')) . ';',
     899                    'before'
     900                );
     901            }
     902           
    232903            if ($page === 'nexlifydesk_tickets') {
    233904                wp_add_inline_script(
     
    8211492            $settings = array(
    8221493                'email_notifications' => isset($_POST['email_notifications']) ? 1 : 0,
     1494                'admin_email_notifications' => isset($_POST['admin_email_notifications']) ? 1 : 0,
     1495                'disable_email_notifications_for_email_tickets' => isset($_POST['disable_email_notifications_for_email_tickets']) ? 1 : 0,
    8231496                'default_priority' => isset($_POST['default_priority']) ? sanitize_text_field(wp_unslash($_POST['default_priority'])) : '',
    8241497                'auto_assign' => isset($_POST['auto_assign']) ? 1 : 0,
     
    9551628            'status_changed' => '',
    9561629            'sla_breach' => '',
     1630            'email_auto_response' => '',
    9571631        ));
    9581632
     
    9631637                'status_changed' => isset($_POST['status_changed']) ? wp_kses_post(wp_unslash($_POST['status_changed'])) : '',
    9641638                'sla_breach' => isset($_POST['sla_breach']) ? wp_kses_post(wp_unslash($_POST['sla_breach'])) : '',
     1639                'email_auto_response' => isset($_POST['email_auto_response']) ? wp_kses_post(wp_unslash($_POST['email_auto_response'])) : '',
    9651640            ];
    9661641                update_option('nexlifydesk_email_templates', $templates);
     
    10301705                <div id="preview-status_changed" class="nexlifydesk-email-preview" style="border:1px solid #ddd; margin-top:10px; padding:10px; display:none;"></div>
    10311706               
     1707                <h2><?php esc_html_e('Email Auto-Response (For Email Channel)', 'nexlifydesk'); ?></h2>
     1708                <p class="description"><?php esc_html_e('This is the automatic response sent to customers when they create a ticket via email. Leave empty to use the default message.', 'nexlifydesk'); ?></p>
     1709                <?php
     1710                // Show default template as placeholder if empty
     1711                $auto_response_content = $templates['email_auto_response'];
     1712                if (empty($auto_response_content)) {
     1713                    $auto_response_content = "Hello {customer_name},\n\nThank you for contacting us. We have received your support request and have assigned it ticket ID #{ticket_id}.\n\nSubject: {subject}\n\nOur 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.\n\nBest regards,\n{site_name} Support Team\n{site_url}";
     1714                }
     1715                wp_editor(
     1716                    $auto_response_content,
     1717                    'email_auto_response',
     1718                    array(
     1719                        'textarea_name' => 'email_auto_response',
     1720                        'textarea_rows' => 10,
     1721                        'media_buttons' => false,
     1722                        'tinymce' => false,
     1723                        'quicktags' => true,
     1724                        'default_editor' => 'html',
     1725                    )
     1726                ); ?>
     1727                <button type="button" class="button preview-email-template" data-editor="email_auto_response" style="margin-top:5px;">
     1728                    <?php esc_html_e('Preview', 'nexlifydesk'); ?>
     1729                </button>
     1730                <div id="preview-email_auto_response" class="nexlifydesk-email-preview" style="border:1px solid #ddd; margin-top:10px; padding:10px; display:none;"></div>
     1731               
    10321732                <h2><?php esc_html_e('SLA Breach', 'nexlifydesk'); ?></h2>
    10331733                <?php wp_editor(
     
    10701770                    <li><code>{ticket_admin_url}</code> – <?php esc_html_e('Direct link to the ticket in the admin area (for agents/admins)', 'nexlifydesk'); ?></li>
    10711771                </ul>
     1772               
     1773                <h3><?php esc_html_e('Auto-Response Specific Placeholders', 'nexlifydesk'); ?></h3>
     1774                <p class="description"><?php esc_html_e('These placeholders are specifically for the Email Auto-Response template:', 'nexlifydesk'); ?></p>
     1775                <ul>
     1776                    <li><code>{customer_name}</code> – <?php esc_html_e('Customer Name (extracted from email)', 'nexlifydesk'); ?></li>
     1777                    <li><code>{site_name}</code> – <?php esc_html_e('Your Website Name', 'nexlifydesk'); ?></li>
     1778                    <li><code>{site_url}</code> – <?php esc_html_e('Your Website URL', 'nexlifydesk'); ?></li>
     1779                    <li><code>{admin_email}</code> – <?php esc_html_e('Admin Email Address', 'nexlifydesk'); ?></li>
     1780                </ul>
     1781               
    10721782                <p><?php esc_html_e('Copy and paste these placeholders into your templates. They will be replaced with real ticket data.', 'nexlifydesk'); ?></p>
    10731783            </div>
    10741784        </div>
    10751785        <?php
    1076     }
    1077 
    1078     private static function check_rate_limit($action, $user_id) {
    1079         $key = "nexlifydesk_rate_limit_{$action}_{$user_id}";
    1080         $attempts = get_transient($key);
    1081        
    1082         if ($attempts && $attempts > 5) {
    1083             wp_die(esc_html__('Too many attempts. Please try again later.', 'nexlifydesk'));
    1084         }
    1085        
    1086         set_transient($key, ($attempts + 1), 300); // 5 minutes
    1087     }
    1088 
    1089     public static function get_ticket_page_url() {
    1090         $settings = get_option('nexlifydesk_settings', array());
    1091         $ticket_page_id = isset($settings['ticket_page_id']) ? (int)$settings['ticket_page_id'] : 0;
    1092        
    1093         if ($ticket_page_id > 0) {
    1094             $url = get_permalink($ticket_page_id);
    1095             if ($url && wp_http_validate_url($url)) {
    1096                 return $url;
    1097             }
    1098         }
    1099        
    1100         return home_url();
    11011786    }
    11021787
     
    11181803            <?php
    11191804        }
    1120     }
    1121 
    1122     public static function save_settings() {
    1123         if (!current_user_can('manage_options')) {
    1124             wp_die(esc_html__('You do not have permission to perform this action.', 'nexlifydesk'));
    1125         }
    1126         check_admin_referer('nexlifydesk_save_settings', 'nexlifydesk_settings_nonce');
    1127 
    1128         $settings = get_option('nexlifydesk_settings', array());
    1129 
    1130         $flat_fields = array(
    1131             'email_notifications', 'default_priority', 'auto_assign', 'allowed_file_types', 'max_file_size',
    1132             'default_category', 'sla_response_time', 'ticket_page_id', 'ticket_form_page_id', 'ticket_id_prefix',
    1133             'ticket_id_start', 'keep_data_on_uninstall', 'status_change_notification', 'check_duplicates', 'duplicate_threshold',
    1134             'auto_assign_to_admin'
    1135         );
    1136         foreach ($flat_fields as $field) {
    1137             if (isset($_POST[$field])) {
    1138                 $settings[$field] = is_numeric($_POST[$field]) ? (int)$_POST[$field] : sanitize_text_field(wp_unslash($_POST[$field]));
    1139             } elseif (in_array($field, array('email_notifications', 'auto_assign', 'keep_data_on_uninstall', 'status_change_notification', 'check_duplicates'))) {
    1140                 $settings[$field] = 0;
    1141             }
    1142         }
    1143 
    1144         if (isset($_POST['nexlifydesk_settings']) && is_array($_POST['nexlifydesk_settings'])) {
    1145             // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Input is unslashed and sanitized immediately after
    1146             $nexlifydesk_settings_raw = wp_unslash($_POST['nexlifydesk_settings']);
    1147             $nexlifydesk_settings = filter_var_array($nexlifydesk_settings_raw, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
    1148             $sanitized_settings = array();
    1149            
    1150             foreach ($nexlifydesk_settings as $key => $value) {
    1151                 $sanitized_key = sanitize_key($key);
    1152                 $sanitized_value = is_numeric($value) ? (int)$value : sanitize_text_field($value);
    1153                 $sanitized_settings[$sanitized_key] = $sanitized_value;
    1154                 $settings[$sanitized_key] = $sanitized_value;
    1155             }
    1156         }
    1157 
    1158         update_option('nexlifydesk_settings', $settings);
    1159         add_settings_error('nexlifydesk_settings', 'settings_saved', __('Settings saved successfully!', 'nexlifydesk'), 'success');
    1160         wp_redirect(add_query_arg(array('settings-updated' => 'true'), admin_url('admin.php?page=nexlifydesk_settings')));
    1161         exit;
    1162     }
    1163 
    1164     public static function get_ticket_form_page_url() {
    1165         $settings = get_option('nexlifydesk_settings', array());
    1166         $ticket_form_page_id = isset($settings['ticket_form_page_id']) ? (int)$settings['ticket_form_page_id'] : 0;
    1167        
    1168         if ($ticket_form_page_id > 0) {
    1169             $page = get_post($ticket_form_page_id);
    1170             if ($page && $page->post_status === 'publish') {
    1171                 $url = get_permalink($ticket_form_page_id);
    1172                 if ($url && wp_http_validate_url($url)) {
    1173                     return $url;
    1174                 }
    1175             }
    1176         }
    1177        
    1178         $pages = get_pages(array('post_status' => 'publish'));
    1179         foreach ($pages as $page) {
    1180             if (has_shortcode($page->post_content, 'nexlifydesk_ticket_form')) {
    1181                 $page_title = strtolower($page->post_title);
    1182                 $page_slug = $page->post_name;
    1183                
    1184                 if (strpos($page_title, 'doc') !== false ||
    1185                     strpos($page_title, 'help') !== false ||
    1186                     strpos($page_title, 'guide') !== false ||
    1187                     strpos($page_slug, 'doc') !== false ||
    1188                     strpos($page_slug, 'help') !== false ||
    1189                     strpos($page_slug, 'guide') !== false) {
    1190                     continue;
    1191                 }
    1192                
    1193                 return get_permalink($page->ID);
    1194             }
    1195         }
    1196        
    1197         return '';
    11981805    }
    11991806}
  • nexlifydesk/trunk/includes/class-nexlifydesk-ajax.php

    r3326104 r3330741  
    1717        add_action('wp_ajax_nexlifydesk_delete_category', array(__CLASS__, 'delete_category'));
    1818        add_action('wp_ajax_nexlifydesk_add_category', array(__CLASS__, 'add_category'));
     19        add_action('wp_ajax_nexlifydesk_test_aws_connection', array(__CLASS__, 'test_aws_connection'));
     20        add_action('wp_ajax_nexlifydesk_manual_fetch_emails', array(__CLASS__, 'manual_fetch_emails'));
     21        add_action('wp_ajax_nexlifydesk_bulk_action', array(__CLASS__, 'handle_bulk_action'));
     22        add_action('wp_ajax_nexlifydesk_refresh_ticket_list', array(__CLASS__, 'refresh_ticket_list'));
     23        add_action('wp_ajax_nexlifydesk_mark_ticket_read', array(__CLASS__, 'mark_ticket_read'));
     24        add_action('wp_ajax_nexlifydesk_test_custom_connection', array(__CLASS__, 'test_custom_connection'));
     25        add_action('wp_ajax_nexlifydesk_test_google_connection', array(__CLASS__, 'test_google_connection'));
     26        add_action('wp_ajax_nexlifydesk_manual_fetch_google_emails', array(__CLASS__, 'manual_fetch_google_emails'));
    1927    }
    2028   
     
    2533        if (!is_user_logged_in()) {
    2634            wp_send_json_error(__('You must be logged in to submit a ticket.', 'nexlifydesk'));
     35        }
     36       
     37        if (isset($_FILES['attachments']) && isset($_FILES['attachments']['error']) && is_array($_FILES['attachments']['error'])) {
     38            foreach ($_FILES['attachments']['error'] as $error) {
     39                $error = absint($error);
     40                if ($error === UPLOAD_ERR_FORM_SIZE || $error === UPLOAD_ERR_INI_SIZE) {
     41                    wp_send_json_error(__('One or more files are too large. Please reduce file sizes and try again.', 'nexlifydesk'));
     42                }
     43                if ($error === UPLOAD_ERR_PARTIAL) {
     44                    wp_send_json_error(__('File upload was interrupted. Please try again.', 'nexlifydesk'));
     45                }
     46            }
     47        }
     48       
     49        $max_post_size = self::get_max_post_size();
     50        $content_length = isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
     51       
     52        if ($content_length > $max_post_size) {
     53            wp_send_json_error(sprintf(
     54                /* translators: %s: Maximum allowed file size */
     55                __('Request too large. Maximum allowed size is %s. Please reduce file sizes or number of files.', 'nexlifydesk'),
     56                size_format($max_post_size)
     57            ));
    2758        }
    2859       
     
    3566            'message' => wp_kses_post(wp_unslash($_POST['message'])),
    3667            'category_id' => isset($_POST['category_id']) ? absint(wp_unslash($_POST['category_id'])) : 0,
    37             'priority' => isset($_POST['priority']) ? sanitize_text_field(wp_unslash($_POST['priority'])) : 'medium'
     68            'priority' => isset($_POST['priority']) ? sanitize_text_field(wp_unslash($_POST['priority'])) : 'medium',
     69            'source' => 'web'
    3870        );
    3971       
     
    198230        $data['attachments'] = $attachments;
    199231        $data['user_id'] = $current_user->ID;
     232        $data['source'] = 'web';
    200233
    201234        $reply_id = NexlifyDesk_Tickets::add_reply($data);
     
    395428            wp_send_json_error(
    396429                sprintf(
    397                     /* translators: %d: Maximum file size in megabytes */
     430                    /* translators: %s: Maximum file size in megabytes */
    398431                    __('File size exceeds maximum limit of %dMB.', 'nexlifydesk'),
    399432                    isset($settings['max_file_size']) ? (int)$settings['max_file_size'] : 2
     
    436469       
    437470        try {
     471       
    438472            $current_user = wp_get_current_user();
    439473            $status = isset($_POST['status']) && $_POST['status'] !== 'all' ? sanitize_text_field(wp_unslash($_POST['status'])) : '';
     
    539573           
    540574        } catch (Exception $e) {
    541 
    542575            wp_send_json_error(__('Error loading tickets. Please try again or contact support.', 'nexlifydesk'));
    543576        }
     
    725758        }
    726759    }
     760
     761    /**
     762     * Test AWS WorkMail connection
     763     */
     764    public static function test_aws_connection() {
     765       
     766        if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) {
     767            wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk')));
     768        }
     769
     770        // Check if IMAP extension is available
     771        if (!extension_loaded('imap')) {
     772            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')));
     773        }
     774
     775        if (
     776            !isset($_POST['nonce']) ||
     777            !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk_aws_test')
     778        ) {
     779            wp_send_json_error(array('message' => __('Invalid nonce.', 'nexlifydesk')));
     780        }
     781
     782        $region = sanitize_text_field(wp_unslash($_POST['region'] ?? ''));
     783        $organization_id = sanitize_text_field(wp_unslash($_POST['organization_id'] ?? ''));
     784        $email = sanitize_email(wp_unslash($_POST['email'] ?? ''));
     785        $password = sanitize_text_field(wp_unslash($_POST['password'] ?? ''));
     786        $access_key_id = sanitize_text_field(wp_unslash($_POST['access_key_id'] ?? ''));
     787        $secret_access_key = sanitize_text_field(wp_unslash($_POST['secret_access_key'] ?? ''));
     788
     789        if ($password === '••••••••••••••••') {
     790            $current_settings = get_option('nexlifydesk_imap_settings', []);
     791            if (!empty($current_settings['aws_password'])) {
     792                $password = nexlifydesk_decrypt($current_settings['aws_password']);
     793            }
     794        }
     795
     796        $is_ssl_enabled = (
     797            is_ssl() ||
     798            (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ||
     799            (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
     800            (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on') ||
     801            (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') ||
     802            (wp_parse_url(home_url(), PHP_URL_SCHEME) === 'https')
     803        );
     804       
     805        if (defined('NEXLIFYDESK_FORCE_SSL_ENABLED') && NEXLIFYDESK_FORCE_SSL_ENABLED) {
     806            $is_ssl_enabled = true;
     807        }
     808       
     809        if (!$is_ssl_enabled) {
     810            wp_send_json_error(array('message' => __('SSL certificate is required for AWS WorkMail integration.', 'nexlifydesk')));
     811        }
     812
     813        if (empty($region) || empty($organization_id) || empty($email) || empty($password)) {
     814            wp_send_json_error(array('message' => __('Please fill in all required fields: AWS Region, Organization ID, Email Address, and Email Password.', 'nexlifydesk')));
     815        }
     816
     817        try {
     818            $messages = [];
     819           
     820            $imap_host = "imap.mail.{$region}.awsapps.com";
     821            $imap_port = 993;
     822           
     823            $connection = @imap_open(
     824                "{{$imap_host}:{$imap_port}/imap/ssl}INBOX",
     825                $email,
     826                $password,
     827                OP_READONLY
     828            );
     829
     830            if (!$connection) {
     831                $error = imap_last_error();
     832                /* translators: 1: IMAP error, 2: Organization ID, 3: Email, 4: AWS region */
     833                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)));
     834            }
     835
     836            imap_close($connection);
     837            $messages[] = 'AWS WorkMail IMAP connection successful';
     838           
     839            if (!empty($access_key_id) && !empty($secret_access_key)) {
     840                $original_settings = get_option('nexlifydesk_imap_settings', []);
     841                $test_settings = array_merge($original_settings, [
     842                    'aws_region' => $region,
     843                    'aws_access_key_id' => $access_key_id,
     844                    'aws_secret_access_key' => $secret_access_key
     845                ]);
     846                update_option('nexlifydesk_imap_settings', $test_settings);
     847               
     848                $aws_handler_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/providers/aws-ses/aws-handler.php';
     849                if (file_exists($aws_handler_path)) {
     850                    require_once $aws_handler_path;
     851                   
     852                    $auth = new NexlifyDesk_AWS_Auth();
     853                    $ses_result = $auth->validate_ses_credentials();
     854                   
     855                    if (is_wp_error($ses_result)) {
     856                        $messages[] = 'AWS SES credentials validation failed: ' . $ses_result->get_error_message();
     857                    } else {
     858                        $messages[] = 'AWS SES credentials validated successfully';
     859                    }
     860                }
     861               
     862                update_option('nexlifydesk_imap_settings', $original_settings);
     863            } else {
     864                $messages[] = 'AWS API credentials (Access Key ID, Secret Access Key) not provided - SES features will not be available';
     865            }
     866
     867            $final_message = implode('. ', $messages);
     868            wp_send_json_success(array('message' => $final_message));
     869
     870        } catch (Exception $e) {
     871            /* translators: %s: Error message returned from the connection test */
     872            wp_send_json_error(array('message' => sprintf(__('Connection test failed: %s', 'nexlifydesk'), $e->getMessage())));
     873        }
     874    }
     875
     876    /*
     877    * Test custom IMAP connection
     878    */
     879    public static function test_custom_connection() {
     880        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     881
     882        // Check if IMAP extension is available
     883        if (!extension_loaded('imap')) {
     884            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')));
     885        }
     886
     887        $host = isset($_POST['host']) ? sanitize_text_field(wp_unslash($_POST['host'])) : '';
     888        $port = isset($_POST['port']) ? intval($_POST['port']) : 993;
     889        $username = isset($_POST['username']) ? sanitize_text_field(wp_unslash($_POST['username'])) : '';
     890        $password = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : '';
     891        $encryption = isset($_POST['encryption']) ? sanitize_text_field(wp_unslash($_POST['encryption'])) : 'ssl';
     892        $protocol = isset($_POST['protocol']) ? sanitize_text_field(wp_unslash($_POST['protocol'])) : 'imap';
     893
     894        if ($password === '••••••••••••••••') {
     895            $settings = get_option('nexlifydesk_imap_settings', array());
     896            $password = nexlifydesk_get_safe_password($settings['password'] ?? '');
     897        }
     898
     899        if (empty($host) || empty($port) || empty($username) || empty($password)) {
     900            wp_send_json_error('Missing required connection details.');
     901        }
     902
     903        $mailbox = "{" . $host . ":" . $port . "/" . $protocol . "/" . $encryption . "}INBOX";
     904        $inbox = @imap_open($mailbox, $username, $password, OP_READONLY);
     905
     906        if ($inbox) {
     907            imap_close($inbox);
     908            wp_send_json_success(['message' => 'Connection successful!']);
     909        } else {
     910            $error = imap_last_error();
     911            wp_send_json_error(['message' => 'Connection failed!']);
     912        }
     913    }
     914
     915    /**
     916     * Manual email fetch for testing
     917     */
     918    public static function manual_fetch_emails() {
     919
     920        if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) {
     921            wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk')));
     922        }
     923
     924        $provider = isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : '';
     925        $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
     926
     927        if ($provider === 'custom') {
     928            if (!$nonce || !wp_verify_nonce($nonce, 'nexlifydesk-ajax-nonce')) {
     929                wp_send_json_error(array('message' => __('Invalid nonce.', 'nexlifydesk')));
     930            }
     931            wp_send_json_success(array('message' => 'Custom email fetch test successful!'));
     932        }
     933
     934        if (!$nonce || !wp_verify_nonce($nonce, 'nexlifydesk_aws_test')) {
     935            wp_send_json_error(array('message' => __('Invalid nonce.', 'nexlifydesk')));
     936        }
     937
     938        $region = sanitize_text_field(wp_unslash($_POST['region'] ?? ''));
     939        $organization_id = sanitize_text_field(wp_unslash($_POST['organization_id'] ?? ''));
     940        $email = sanitize_email(wp_unslash($_POST['email'] ?? ''));
     941        $password = sanitize_text_field(wp_unslash($_POST['password'] ?? ''));
     942        $access_key_id = sanitize_text_field(wp_unslash($_POST['access_key_id'] ?? ''));
     943        $secret_access_key = sanitize_text_field(wp_unslash($_POST['secret_access_key'] ?? ''));
     944
     945        if ($password === '••••••••••••••••') {
     946            $current_settings = get_option('nexlifydesk_imap_settings', []);
     947            if (!empty($current_settings['aws_password'])) {
     948                $password = nexlifydesk_decrypt($current_settings['aws_password']);
     949            }
     950        }
     951
     952        if (empty($region) || empty($organization_id) || empty($email) || empty($password)) {
     953            wp_send_json_error(array('message' => __('Please fill in all required AWS fields first.', 'nexlifydesk')));
     954        }
     955
     956        try {
     957            $current_settings = get_option('nexlifydesk_imap_settings', []);
     958            $test_settings = array_merge($current_settings, [
     959                'enabled' => true,
     960                'provider' => 'aws',
     961                'aws_region' => $region,
     962                'aws_organization_id' => $organization_id,
     963                'aws_email' => $email,
     964                'aws_password' => $password
     965            ]);
     966           
     967            if (!empty($access_key_id) && !empty($secret_access_key)) {
     968                $test_settings['aws_access_key_id'] = $access_key_id;
     969                $test_settings['aws_secret_access_key'] = $secret_access_key;
     970            }
     971           
     972            update_option('nexlifydesk_imap_settings', $test_settings);
     973           
     974            $email_pipe_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/nexlifydesk-email-pipe.php';
     975            if (file_exists($email_pipe_path)) {
     976                require_once $email_pipe_path;
     977            }
     978           
     979            $aws_handler_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/providers/aws-ses/aws-handler.php';
     980            if (file_exists($aws_handler_path)) {
     981                require_once $aws_handler_path;
     982            }
     983           
     984            if (function_exists('nexlifydesk_fetch_emails')) {
     985                nexlifydesk_fetch_emails();
     986            } else {
     987                throw new Exception('Email fetch function not found');
     988            }
     989           
     990            update_option('nexlifydesk_imap_settings', $current_settings);
     991           
     992            global $wpdb;
     993            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe and not user input
     994            $table_name = $wpdb->prefix . 'nexlifydesk_tickets';
     995            $recent_tickets_cache_key = 'nexlifydesk_recent_email_tickets_' . md5(gmdate('Y-m-d H:i:s', strtotime('-2 minutes')));
     996            $recent_tickets = wp_cache_get($recent_tickets_cache_key);
     997
     998            if (false === $recent_tickets) {
     999                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and not user input
     1000                $recent_tickets = $wpdb->get_results(
     1001                    $wpdb->prepare(
     1002                        "SELECT ticket_id, subject, source, created_at
     1003                        FROM {$table_name}
     1004                        WHERE created_at >= %s
     1005                        AND source = 'email'
     1006                        ORDER BY created_at DESC
     1007                        LIMIT 10",
     1008                        gmdate('Y-m-d H:i:s', strtotime('-2 minutes'))
     1009                    )
     1010                );
     1011                wp_cache_set($recent_tickets_cache_key, $recent_tickets, '', 120);
     1012            }
     1013           
     1014            $message = "Email fetch completed successfully!\n";
     1015           
     1016            if (!empty($recent_tickets)) {
     1017                $message .= "Found " . count($recent_tickets) . " recent email ticket(s):\n";
     1018                foreach ($recent_tickets as $ticket) {
     1019                    $message .= "- #{$ticket->ticket_id}: {$ticket->subject} ({$ticket->created_at})\n";
     1020                }
     1021            } else {
     1022                $message .= "No new email tickets found. This could mean:\n";
     1023                $message .= "- No new emails in the mailbox\n";
     1024                $message .= "- Emails are being filtered out (spam, duplicates)\n";
     1025                $message .= "- All emails have been processed already\n";
     1026            }
     1027           
     1028            wp_send_json_success(array('message' => $message));
     1029           
     1030        } catch (Exception $e) {
     1031            if (isset($current_settings)) {
     1032                update_option('nexlifydesk_imap_settings', $current_settings);
     1033            }
     1034           
     1035            /* translators: %s: Error message returned from the email fetch */
     1036            wp_send_json_error(array('message' => sprintf(__('Email fetch failed: %s', 'nexlifydesk'), $e->getMessage())));
     1037        }
     1038    }
     1039
     1040    /**
     1041     * Handle bulk actions for tickets
     1042     */
     1043    public static function handle_bulk_action() {
     1044        // Check nonce
     1045        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     1046       
     1047        // Check permissions
     1048        if (!current_user_can('nexlifydesk_manage_tickets')) {
     1049            wp_send_json_error(__('You do not have permission to perform this action.', 'nexlifydesk'));
     1050        }
     1051       
     1052        $bulk_action = sanitize_text_field(wp_unslash($_POST['bulk_action'] ?? ''));
     1053        $ticket_ids = array_map('intval', (array) ($_POST['ticket_ids'] ?? []));
     1054       
     1055        if (empty($bulk_action) || empty($ticket_ids)) {
     1056            wp_send_json_error(__('Invalid bulk action or no tickets selected.', 'nexlifydesk'));
     1057        }
     1058       
     1059        $updated_count = 0;
     1060        $errors = [];
     1061       
     1062        foreach ($ticket_ids as $ticket_id) {
     1063            $ticket = NexlifyDesk_Tickets::get_ticket($ticket_id);
     1064            if (!$ticket) {
     1065                /* translators: %d: Ticket ID */
     1066                $errors[] = sprintf(__('Ticket #%d not found.', 'nexlifydesk'), $ticket_id);
     1067                continue;
     1068            }
     1069           
     1070            try {
     1071                switch ($bulk_action) {
     1072                    case 'assign':
     1073                        $agent_id = intval($_POST['agent_id'] ?? 0);
     1074                        $result = NexlifyDesk_Tickets::assign_ticket($ticket_id, $agent_id);
     1075                        if ($result) {
     1076                            $updated_count++;
     1077                        } else {
     1078                            /* translators: %d: Ticket ID */
     1079                            $errors[] = sprintf(__('Failed to assign ticket #%d.', 'nexlifydesk'), $ticket_id);
     1080                        }
     1081                        break;
     1082                       
     1083                    case 'status':
     1084                        $status = sanitize_text_field(wp_unslash($_POST['status'] ?? ''));
     1085                        $allowed_statuses = ['open', 'in_progress', 'pending', 'resolved', 'closed'];
     1086                        if (!in_array($status, $allowed_statuses)) {
     1087                            /* translators: 1: Status, 2: Ticket ID */
     1088                            $errors[] = sprintf(__('Invalid status "%1$s" for ticket #%2$d.', 'nexlifydesk'), $status, $ticket_id);
     1089                            continue 2;
     1090                        }
     1091                       
     1092                        $result = NexlifyDesk_Tickets::update_ticket_status($ticket_id, $status);
     1093                        if ($result) {
     1094                            $updated_count++;
     1095                        } else {
     1096                            /* translators: %d: Ticket ID */
     1097                            $errors[] = sprintf(__('Failed to update status for ticket #%d.', 'nexlifydesk'), $ticket_id);
     1098                        }
     1099                        break;
     1100                       
     1101                    case 'priority':
     1102                        $priority = sanitize_text_field(wp_unslash($_POST['priority'] ?? ''));
     1103                        $allowed_priorities = ['low', 'medium', 'high', 'urgent'];
     1104                        if (!in_array($priority, $allowed_priorities)) {
     1105                            /* translators: 1: Priority, 2: Ticket ID */
     1106                            $errors[] = sprintf(__('Invalid priority "%1$s" for ticket #%2$d.', 'nexlifydesk'), $priority, $ticket_id);
     1107                            continue 2;
     1108                        }
     1109                       
     1110                        $result = NexlifyDesk_Tickets::update_ticket_priority($ticket_id, $priority);
     1111                        if ($result) {
     1112                            $updated_count++;
     1113                        } else {
     1114                            /* translators: %d: Ticket ID */
     1115                            $errors[] = sprintf(__('Failed to update priority for ticket #%d.', 'nexlifydesk'), $ticket_id);
     1116                        }
     1117                        break;
     1118                       
     1119                    case 'delete':
     1120                        // Only allow admins to delete tickets
     1121                        if (!current_user_can('manage_options')) {
     1122                            /* translators: %d: Ticket ID */
     1123                            $errors[] = sprintf(__('You do not have permission to delete ticket #%d.', 'nexlifydesk'), $ticket_id);
     1124                            continue 2;
     1125                        }
     1126                       
     1127                        $result = NexlifyDesk_Tickets::delete_ticket($ticket_id);
     1128                        if ($result) {
     1129                            $updated_count++;
     1130                        } else {
     1131                            /* translators: %d: Ticket ID */
     1132                            $errors[] = sprintf(__('Failed to delete ticket #%d.', 'nexlifydesk'), $ticket_id);
     1133                        }
     1134                        break;
     1135                       
     1136                    default:
     1137                        /* translators: %s: Bulk action name */
     1138                        $errors[] = sprintf(__('Unknown bulk action "%s".', 'nexlifydesk'), $bulk_action);
     1139                        break;
     1140                }
     1141            } catch (Exception $e) {
     1142                /* translators: 1: Ticket ID, 2: Error message */
     1143                $errors[] = sprintf(__('Error processing ticket #%1$d: %2$s', 'nexlifydesk'), $ticket_id, $e->getMessage());
     1144            }
     1145        }
     1146       
     1147        $messages = [];
     1148        if ($updated_count > 0) {
     1149            if ($bulk_action === 'delete') {
     1150                /* translators: %d: Number of tickets deleted */
     1151                $messages[] = sprintf(_n('%d ticket deleted permanently.', '%d tickets deleted permanently.', $updated_count, 'nexlifydesk'), $updated_count);
     1152            } else {
     1153                /* translators: %d: Number of tickets updated */
     1154                $messages[] = sprintf(_n('%d ticket updated successfully.', '%d tickets updated successfully.', $updated_count, 'nexlifydesk'), $updated_count);
     1155            }
     1156        }
     1157       
     1158        if (!empty($errors)) {
     1159            $messages[] = __('Errors occurred:', 'nexlifydesk') . ' ' . implode(', ', $errors);
     1160        }
     1161       
     1162        if ($updated_count > 0) {
     1163            wp_send_json_success(implode(' ', $messages));
     1164        } else {
     1165            wp_send_json_error(implode(' ', $messages));
     1166        }
     1167    }
     1168
     1169    /**
     1170     * Refresh ticket list - returns updated ticket data
     1171     */
     1172    public static function refresh_ticket_list() {
     1173        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'nexlifydesk-ajax-nonce')) {
     1174            wp_send_json_error(array(
     1175                'message' => __('Session expired. Please reload the page.', 'nexlifydesk'),
     1176                'code' => 'nonce_expired'
     1177            ));
     1178        }
     1179       
     1180        if (!current_user_can('nexlifydesk_manage_tickets')) {
     1181            wp_send_json_error(array('message' => __('You do not have permission to view tickets.', 'nexlifydesk')));
     1182        }
     1183       
     1184        try {
     1185            $last_refresh = isset($_POST['last_refresh']) ? intval($_POST['last_refresh']) : 0;
     1186           
     1187            wp_cache_flush_group('nexlifydesk_tickets_grid');
     1188           
     1189            $tickets = NexlifyDesk_Tickets::get_tickets_for_grid(['per_page' => 100]);
     1190           
     1191            if (!is_array($tickets)) {
     1192                $tickets = array();
     1193            }
     1194           
     1195            $formatted_tickets = array();
     1196            $current_user_id = get_current_user_id();
     1197           
     1198            foreach ($tickets as $ticket) {
     1199                if (!is_object($ticket) || !isset($ticket->id)) {
     1200                    continue;
     1201                }
     1202               
     1203                $user = get_userdata($ticket->user_id);
     1204                $assigned_agent = $ticket->assigned_to ? get_userdata($ticket->assigned_to) : null;
     1205               
     1206                if (!$user) {
     1207                    $customer_details = function_exists('nexlifydesk_extract_customer_details') ?
     1208                        nexlifydesk_extract_customer_details($ticket->message) :
     1209                        ['name' => '', 'email' => '', 'message' => $ticket->message];
     1210                    $customer_name = $customer_details['name'] ?: 'Guest';
     1211                    $customer_email = $customer_details['email'] ?: 'N/A';
     1212                } else {
     1213                    $customer_name = $user->display_name;
     1214                    $customer_email = $user->user_email;
     1215                }
     1216               
     1217                $is_unread = 0;
     1218                if (isset($ticket->is_unread)) {
     1219                    $is_unread = intval($ticket->is_unread);
     1220                } else {
     1221                    global $wpdb;
     1222                    $cache_key = 'nexlifydesk_ticket_read_' . intval($ticket->id) . '_' . intval($current_user_id);
     1223                    $read_record = wp_cache_get($cache_key, '', 60);
     1224                    if (false === $read_record) {
     1225                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1226                        $read_record = $wpdb->get_row(
     1227                            $wpdb->prepare(
     1228                                "SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads
     1229                                 WHERE ticket_id = %d AND user_id = %d",
     1230                                $ticket->id,
     1231                                $current_user_id
     1232                            )
     1233                        );
     1234                        wp_cache_set($cache_key, $read_record, '', 60);
     1235                    }
     1236                   
     1237                    if (!$read_record) {
     1238                        $is_unread = 1;
     1239                    } else {
     1240                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1241                        $newer_replies = $wpdb->get_var(
     1242                            $wpdb->prepare(
     1243                                "SELECT COUNT(*) FROM {$wpdb->prefix}nexlifydesk_replies
     1244                                 WHERE ticket_id = %d AND user_id != %d
     1245                                 AND created_at > %s AND is_internal_note = 0",
     1246                                $ticket->id,
     1247                                $current_user_id,
     1248                                $read_record->read_at
     1249                            )
     1250                        );
     1251                        $is_unread = $newer_replies > 0 ? 1 : 0;
     1252                    }
     1253                }
     1254               
     1255                $formatted_tickets[] = array(
     1256                    'id' => $ticket->id,
     1257                    'ticket_id' => $ticket->ticket_id,
     1258                    'subject' => $ticket->subject,
     1259                    'message' => $ticket->message,
     1260                    'status' => $ticket->status,
     1261                    'priority' => $ticket->priority,
     1262                    'user_id' => $ticket->user_id,
     1263                    'user_name' => $customer_name,
     1264                    'user_email' => $customer_email,
     1265                    'assigned_to' => $ticket->assigned_to,
     1266                    'assigned_to_display_name' => $assigned_agent ? $assigned_agent->display_name : '',
     1267                    'created_at' => $ticket->created_at,
     1268                    'updated_at' => $ticket->updated_at,
     1269                    'last_reply_time' => isset($ticket->last_reply_time) ? $ticket->last_reply_time : $ticket->created_at,
     1270                    'is_unread' => $is_unread
     1271                );
     1272            }
     1273           
     1274            $response_data = array(
     1275                'tickets' => $formatted_tickets,
     1276                'new_nonce' => wp_create_nonce('nexlifydesk-ajax-nonce')
     1277            );
     1278           
     1279            wp_send_json_success($response_data);
     1280           
     1281        } catch (Exception $e) {
     1282            wp_send_json_error(array(
     1283                'message' => __('Error refreshing ticket list.', 'nexlifydesk'),
     1284                'error' => $e->getMessage()
     1285            ));
     1286        } catch (Error $e) {
     1287            wp_send_json_error(array(
     1288                'message' => __('Fatal error refreshing ticket list.', 'nexlifydesk'),
     1289                'error' => $e->getMessage()
     1290            ));
     1291        }
     1292    }
     1293
     1294    /**
     1295     * Mark ticket as read
     1296     */
     1297    public static function mark_ticket_read() {
     1298        // Check nonce
     1299        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     1300       
     1301        // Check permissions
     1302        if (!current_user_can('nexlifydesk_manage_tickets')) {
     1303            wp_send_json_error(array('message' => __('You do not have permission to mark tickets as read.', 'nexlifydesk')));
     1304        }
     1305       
     1306        if (!isset($_POST['ticket_id'])) {
     1307            wp_send_json_error(array('message' => __('Ticket ID is required.', 'nexlifydesk')));
     1308        }
     1309       
     1310        $ticket_id = intval($_POST['ticket_id']);
     1311       
     1312        try {
     1313            $current_user_id = get_current_user_id();
     1314           
     1315            global $wpdb;
     1316            $reads_table = $wpdb->prefix . 'nexlifydesk_ticket_reads';
     1317           
     1318            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct REPLACE query
     1319            $result = $wpdb->replace(
     1320                $reads_table,
     1321                array(
     1322                    'ticket_id' => $ticket_id,
     1323                    'user_id' => $current_user_id,
     1324                    'read_at' => current_time('mysql')
     1325                ),
     1326                array('%d', '%d', '%s')
     1327            );
     1328            // Clear cache for this ticket/user read status
     1329            $cache_key = 'nexlifydesk_ticket_read_' . intval($ticket_id) . '_' . intval($current_user_id);
     1330            wp_cache_delete($cache_key);
     1331           
     1332            if ($result === false) {
     1333                throw new Exception('Failed to update read status in database');
     1334            }
     1335           
     1336            wp_cache_flush_group('nexlifydesk_tickets_grid');
     1337           
     1338            wp_send_json_success(array('message' => __('Ticket marked as read.', 'nexlifydesk')));
     1339           
     1340        } catch (Exception $e) {
     1341            wp_send_json_error(array('message' => __('Error marking ticket as read.', 'nexlifydesk')));
     1342        }
     1343    }
     1344
     1345    /**
     1346     * Test Google connection
     1347     */
     1348    public static function test_google_connection() {
     1349        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     1350
     1351        if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) {
     1352            wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk')));
     1353        }
     1354
     1355        $client_id = isset($_POST['client_id']) ? sanitize_text_field(wp_unslash($_POST['client_id'])) : '';
     1356        $client_secret = isset($_POST['client_secret']) ? sanitize_text_field(wp_unslash($_POST['client_secret'])) : '';
     1357
     1358        if ($client_secret === '••••••••••••••••') {
     1359            $settings = get_option('nexlifydesk_imap_settings', array());
     1360            $client_secret = !empty($settings['google_client_secret']) ? nexlifydesk_decrypt($settings['google_client_secret']) : '';
     1361        }
     1362
     1363        if (empty($client_id) || empty($client_secret)) {
     1364            wp_send_json_error(array('message' => __('Please fill in both Google Client ID and Client Secret.', 'nexlifydesk')));
     1365        }
     1366
     1367        try {
     1368            $email_pipe_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/nexlifydesk-email-pipe.php';
     1369            if (file_exists($email_pipe_path)) {
     1370                require_once $email_pipe_path;
     1371            }
     1372
     1373            if (function_exists('nexlifydesk_get_google_access_token')) {
     1374                $access_token = nexlifydesk_get_google_access_token();
     1375                if ($access_token) {
     1376                    $response = wp_remote_get('https://www.googleapis.com/oauth2/v2/userinfo', [
     1377                        'headers' => ['Authorization' => 'Bearer ' . $access_token]
     1378                    ]);
     1379
     1380                    if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
     1381                        $user_info = json_decode(wp_remote_retrieve_body($response), true);
     1382                        $email = $user_info['email'] ?? 'Unknown';
     1383                        wp_send_json_success(array('message' => "Google connection successful! Authorized as: {$email}"));
     1384                    } else {
     1385                        wp_send_json_error(array('message' => __('Google connection failed. Please re-authorize.', 'nexlifydesk')));
     1386                    }
     1387                } else {
     1388                    wp_send_json_error(array('message' => __('Google not authorized. Please complete the OAuth authorization first.', 'nexlifydesk')));
     1389                }
     1390            } else {
     1391                wp_send_json_error(array('message' => __('Google handler functions not available.', 'nexlifydesk')));
     1392            }
     1393        } catch (Exception $e) {
     1394            /* translators: %s: Error message returned from the Google connection test */
     1395            wp_send_json_error(array('message' => sprintf(__('Google connection test failed: %s', 'nexlifydesk'), $e->getMessage())));
     1396        }
     1397    }
     1398
     1399    /**
     1400     * Manual Google email fetch for testing
     1401     */
     1402    public static function manual_fetch_google_emails() {
     1403        check_ajax_referer('nexlifydesk-ajax-nonce', 'nonce');
     1404
     1405        if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) {
     1406            wp_send_json_error(array('message' => __('You do not have permission.', 'nexlifydesk')));
     1407        }
     1408
     1409        try {
     1410            $email_pipe_path = plugin_dir_path(dirname(__FILE__)) . 'email-source/nexlifydesk-email-pipe.php';
     1411            if (file_exists($email_pipe_path)) {
     1412                require_once $email_pipe_path;
     1413            }
     1414
     1415            if (function_exists('nexlifydesk_fetch_google_emails')) {
     1416                nexlifydesk_fetch_google_emails();
     1417            } else {
     1418                throw new Exception('Google email fetch function not found');
     1419            }
     1420           
     1421            global $wpdb;
     1422            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe and not user input
     1423            $table_name = $wpdb->prefix . 'nexlifydesk_tickets';
     1424            $recent_tickets_cache_key = 'nexlifydesk_recent_google_tickets_' . md5(gmdate('Y-m-d H:i:s', strtotime('-2 minutes')));
     1425            $recent_tickets = wp_cache_get($recent_tickets_cache_key);
     1426
     1427            if (false === $recent_tickets) {
     1428                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and not user input
     1429                $recent_tickets = $wpdb->get_results(
     1430                    $wpdb->prepare(
     1431                        "SELECT ticket_id, subject, source, created_at
     1432                        FROM {$table_name}
     1433                        WHERE created_at >= %s
     1434                        AND source = 'email'
     1435                        ORDER BY created_at DESC
     1436                        LIMIT 10",
     1437                        gmdate('Y-m-d H:i:s', strtotime('-2 minutes'))
     1438                    )
     1439                );
     1440                wp_cache_set($recent_tickets_cache_key, $recent_tickets, '', 120);
     1441            }
     1442           
     1443            $message = "Google email fetch completed successfully!\n";
     1444           
     1445            if (!empty($recent_tickets)) {
     1446                $message .= "Found " . count($recent_tickets) . " recent email ticket(s):\n";
     1447                foreach ($recent_tickets as $ticket) {
     1448                    $message .= "- #{$ticket->ticket_id}: {$ticket->subject} ({$ticket->created_at})\n";
     1449                }
     1450            } else {
     1451                $message .= "No new email tickets found. This could mean:\n";
     1452                $message .= "- No new emails in Gmail\n";
     1453                $message .= "- Emails are being filtered out (spam, duplicates)\n";
     1454                $message .= "- All emails have been processed already\n";
     1455            }
     1456           
     1457            wp_send_json_success(array('message' => $message));
     1458           
     1459        } catch (Exception $e) {
     1460            /* translators: %s: Error message returned from the Google email fetch */
     1461            wp_send_json_error(array('message' => sprintf(__('Google email fetch failed: %s', 'nexlifydesk'), $e->getMessage())));
     1462        }
     1463    }
     1464
     1465    /**
     1466     * Get the maximum allowed POST size from server configuration
     1467     *
     1468     * @return int Maximum POST size in bytes
     1469     */
     1470    private static function get_max_post_size() {
     1471        $post_max_size = ini_get('post_max_size');
     1472        $upload_max_filesize = ini_get('upload_max_filesize');
     1473        $post_max_bytes = self::convert_to_bytes($post_max_size);
     1474        $upload_max_bytes = self::convert_to_bytes($upload_max_filesize);
     1475       
     1476        return min($post_max_bytes, $upload_max_bytes);
     1477    }
     1478   
     1479    /**
     1480     * Convert PHP ini size format to bytes
     1481     *
     1482     * @param string $size Size string like "8M", "1G", "512K"
     1483     * @return int Size in bytes
     1484     */
     1485    private static function convert_to_bytes($size) {
     1486        $size = trim($size);
     1487        $last = strtolower($size[strlen($size) - 1]);
     1488        $size = (int) $size;
     1489       
     1490        switch ($last) {
     1491            case 'g':
     1492                $size *= 1024;
     1493                // Fall through
     1494            case 'm':
     1495                $size *= 1024;
     1496                // Fall through
     1497            case 'k':
     1498                $size *= 1024;
     1499                break;
     1500        }
     1501       
     1502        return $size;
     1503    }
    7271504}
  • nexlifydesk/trunk/includes/class-nexlifydesk-database.php

    r3326104 r3330741  
    1010        global $wpdb;
    1111        self::$tables = array(
    12             'tickets' => $wpdb->prefix . 'nexlifydesk_tickets',
    13             'replies' => $wpdb->prefix . 'nexlifydesk_replies',
    14             'attachments' => $wpdb->prefix . 'nexlifydesk_attachments',
    15             'categories' => $wpdb->prefix . 'nexlifydesk_categories'
     12            'tickets' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'tickets',
     13            'replies' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'replies',
     14            'attachments' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'attachments',
     15            'categories' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'categories',
     16            'ticket_reads' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'ticket_reads'
    1617        );
    1718    }   
     
    2425            'replies' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'replies',
    2526            'attachments' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'attachments',
    26             'categories' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'categories'
     27            'categories' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'categories',
     28            'ticket_reads' => $wpdb->prefix . NEXLIFYDESK_TABLE_PREFIX . 'ticket_reads'
    2729        );
    2830
     
    4244            priority varchar(20) NOT NULL DEFAULT 'medium',
    4345            assigned_to bigint(20) DEFAULT NULL,
     46            source varchar(20) NOT NULL DEFAULT 'web',
    4447            created_at datetime NOT NULL,
    4548            updated_at datetime NOT NULL,
     
    8891        ) $charset_collate;";
    8992       
     93        // Ticket reads table
     94        $sql .= "CREATE TABLE " . self::$tables['ticket_reads'] . " (
     95            id bigint(20) NOT NULL AUTO_INCREMENT,
     96            ticket_id bigint(20) NOT NULL,
     97            user_id bigint(20) NOT NULL,
     98            read_at datetime NOT NULL,
     99            PRIMARY KEY  (id),
     100            UNIQUE KEY ticket_user (ticket_id, user_id),
     101            KEY ticket_id (ticket_id),
     102            KEY user_id (user_id)
     103        ) $charset_collate;";
     104       
    90105        $result = dbDelta($sql);
    91106
    92         // Verifying if the tables were created
    93         $tables_to_check = array('tickets', 'replies', 'attachments', 'categories');
     107        $tables_to_check = array('tickets', 'replies', 'attachments', 'categories', 'ticket_reads');
    94108        foreach ($tables_to_check as $table_key) {
    95109            $table_name = self::$tables[$table_key];
     
    107121        add_option('nexlifydesk_settings', array(
    108122            'email_notifications' => 1,
     123            'admin_email_notifications' => 1,
    109124            'default_priority' => 'medium',
    110125            'auto_assign' => 0,
     
    178193   
    179194    public static function check_and_run_migrations() {
    180         $current_version = get_option('nexlifydesk_db_version', '1.0.0');
     195        $current_version = get_option('nexlifydesk_db_version', '1.0.1');
    181196        $plugin_version = NEXLIFYDESK_VERSION;
    182197       
    183         if (version_compare($current_version, '1.0.0', '<')) {
     198        if (version_compare($current_version, '1.0.1', '<')) {
    184199            self::migrate_to_1_0_1();
    185             update_option('nexlifydesk_db_version', '1.0.0');
     200            update_option('nexlifydesk_db_version', '1.0.1');
     201        }
     202       
     203        if (version_compare($current_version, '1.0.2', '<')) {
     204            self::migrate_to_1_0_2();
     205            update_option('nexlifydesk_db_version', '1.0.2');
    186206        }
    187207       
     
    194214    private static function migrate_to_1_0_1() {
    195215        global $wpdb;
     216       
     217        // Ensure tables array is initialized
     218        if (empty(self::$tables)) {
     219            self::init();
     220        }
    196221
    197222        $table_name = self::$tables['replies'];
     
    229254        }
    230255    }
     256   
     257    private static function migrate_to_1_0_2() {
     258        global $wpdb;
     259       
     260        if (empty(self::$tables)) {
     261            self::init();
     262        }
     263       
     264        $table_name = self::$tables['ticket_reads'];
     265       
     266        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Migration check
     267        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) === $table_name;
     268       
     269        if (!$table_exists) {
     270            $charset_collate = $wpdb->get_charset_collate();
     271           
     272            $sql = "CREATE TABLE {$table_name} (
     273                id bigint(20) NOT NULL AUTO_INCREMENT,
     274                ticket_id bigint(20) NOT NULL,
     275                user_id bigint(20) NOT NULL,
     276                read_at datetime NOT NULL,
     277                PRIMARY KEY  (id),
     278                UNIQUE KEY ticket_user (ticket_id, user_id),
     279                KEY ticket_id (ticket_id),
     280                KEY user_id (user_id)
     281            ) {$charset_collate};";
     282           
     283            require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
     284            dbDelta($sql);
     285        }
     286    }
     287
     288    public static function upgrade_database() {
     289        global $wpdb;
     290       
     291        $current_version = get_option('nexlifydesk_version', '1.0.1');
     292       
     293        if ($current_version === '1.0.1' && !get_option('nexlifydesk_db_version')) {
     294            return;
     295        }
     296       
     297        if (empty(self::$tables)) {
     298            self::init();
     299        }
     300       
     301        $tickets_table = self::$tables['tickets'];
     302       
     303        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table existence check during upgrade, no caching needed
     304        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tickets_table)) === $tickets_table;
     305       
     306        if (!$table_exists) {
     307            return;
     308        }
     309       
     310        if (version_compare($current_version, NEXLIFYDESK_VERSION, '<')) {
     311            $tickets_table_escaped = esc_sql($tickets_table);
     312            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped and safe
     313            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema check during upgrade, no caching needed
     314            $column_exists = $wpdb->get_results(
     315                $wpdb->prepare(
     316                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is escaped and safe
     317                    "SHOW COLUMNS FROM `{$tickets_table_escaped}` LIKE %s",
     318                    'source'
     319                )
     320            );
     321           
     322            if (empty($column_exists)) {
     323                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Schema change during plugin upgrade, table name is safe and not user input, no caching needed
     324                $wpdb->query("ALTER TABLE {$tickets_table} ADD COLUMN source varchar(20) NOT NULL DEFAULT 'web' AFTER assigned_to");
     325            }
     326           
     327            // Update version
     328            update_option('nexlifydesk_version', NEXLIFYDESK_VERSION);
     329        }
     330    }
    231331}
  • nexlifydesk/trunk/includes/class-nexlifydesk-tickets.php

    r3326104 r3330741  
    2525            'priority' => 'medium',
    2626            'status' => 'open',
     27            'source' => 'web',
    2728            'attachments' => array()
    2829        );
     
    3435        }
    3536
    36         // Checking for duplicate tickets
     37        $user_id = $data['user_id'];
     38        $email_address = '';
     39       
     40        if ($user_id > 0) {
     41            $user = get_userdata($user_id);
     42            $email_address = $user ? $user->user_email : '';
     43        } else {
     44            if (isset($data['email'])) {
     45                $email_address = $data['email'];
     46            } else {
     47                if (function_exists('nexlifydesk_extract_customer_details')) {
     48                    $customer_details = nexlifydesk_extract_customer_details($data['message']);
     49                    $email_address = $customer_details['email'] ?: '[email protected]';
     50                } else {
     51                    $email_address = '[email protected]';
     52                }
     53            }
     54        }
     55       
     56        $skip_rate_limit = false;
     57        if ($user_id > 0) {
     58            $user = get_userdata($user_id);
     59            if ($user && (in_array('administrator', $user->roles) || in_array('nexlifydesk_agent', $user->roles))) {
     60                $skip_rate_limit = true;
     61            }
     62        }
     63       
     64        if (!$skip_rate_limit && class_exists('NexlifyDesk_Rate_Limiter')) {
     65            if (NexlifyDesk_Rate_Limiter::is_rate_limited($user_id, $email_address)) {
     66                $error_message = NexlifyDesk_Rate_Limiter::get_rate_limit_error_message($user_id, $email_address);
     67                return new WP_Error('rate_limit_exceeded', $error_message);
     68            }
     69        }
     70
     71        // Add email to data array for duplicate checking
     72        $data['email'] = $email_address;
     73
     74        // For non-registered users, add a small delay to avoid race conditions with concurrent email processing
     75        if (!empty($data['source']) && $data['source'] === 'email' && $user_id == 0) {
     76            // Add a micro-delay to reduce race conditions in email processing
     77            usleep(100000); // 100ms delay
     78        }
     79
    3780        $existing_ticket = self::check_for_duplicate_ticket($data);
    3881        if ($existing_ticket) {
    39             // Add new message as a reply to the existing ticket
    4082            $reply_data = array(
    4183                'ticket_id' => $existing_ticket->id,
     
    4991           
    5092            if (!is_wp_error($reply_id)) {
    51                 // Update the existing ticket's updated_at timestamp
    5293                $current_time = current_time('mysql');
    5394                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     
    60101                );
    61102               
    62                 // Return the existing ticket with a flag indicating it's a duplicate
    63103                $existing_ticket->is_duplicate = true;
    64104                $existing_ticket->new_reply_id = $reply_id;
     
    80120                'priority' => sanitize_text_field($data['priority']),
    81121                'status' => sanitize_text_field($data['status']),
     122                'source' => sanitize_text_field($data['source']),
    82123                'created_at' => $current_time,
    83124                'updated_at' => $current_time
    84125            ),
    85             array('%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s')
     126            array('%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s')
    86127        );
    87128
     
    92133        $new_ticket_id = $wpdb->insert_id;
    93134       
     135        // Store customer email as post meta for both web and email tickets
     136        if (!empty($data['email'])) {
     137            update_post_meta($new_ticket_id, 'customer_email', $data['email']);
     138        }
     139       
    94140        if (!empty($data['attachments'])) {
    95141            self::handle_attachments($data['attachments'], $new_ticket_id, null, $data['user_id']);
     
    98144        $ticket = self::get_ticket($new_ticket_id);
    99145
    100         register_shutdown_function(function() use ($ticket) {
    101             NexlifyDesk_Tickets::send_notification($ticket, 'new_ticket');
     146        if (!$skip_rate_limit && class_exists('NexlifyDesk_Rate_Limiter')) {
     147            NexlifyDesk_Rate_Limiter::record_ticket_creation($user_id, $email_address);
     148        }
     149
     150        register_shutdown_function(function() use ($ticket, $data) {
     151            // Different notification handling for email vs web channels
     152            if (!empty($data['source']) && $data['source'] === 'email') {
     153                // Send simple auto-response for email tickets
     154                NexlifyDesk_Tickets::send_email_channel_notification($ticket, 'new_ticket');
     155            } else {
     156                // Send standard notifications for web tickets
     157                NexlifyDesk_Tickets::send_notification($ticket, 'new_ticket');
     158            }
    102159        });
    103160
     
    125182        }
    126183
     184        wp_cache_flush_group('nexlifydesk_tickets_grid');
     185
    127186        return $ticket;
    128187    }
     
    239298            'message' => '',
    240299            'is_admin_reply' => current_user_can('manage_options'),
    241             'is_internal_note' => false
     300            'is_internal_note' => false,
     301            'source' => 'web'
    242302        );
    243303       
     
    246306        if (empty($data['message'])) {
    247307            return new WP_Error('missing_message', __('Message is required.', 'nexlifydesk'));
     308        }
     309       
     310        if ($data['source'] === 'web' && $data['user_id'] > 0) {
     311            $user = get_userdata($data['user_id']);
     312            if ($user && (in_array('administrator', $user->roles) || in_array('nexlifydesk_agent', $user->roles))) {
     313                // Admin/agent reply from web interface
     314            }
    248315        }
    249316       
     
    283350        $reply_id = $wpdb->insert_id;
    284351       
     352        if (!empty($data['source']) && $data['source'] === 'email' && class_exists('NexlifyDesk_Rate_Limiter')) {
     353            $email_address = '';
     354            if ($data['user_id'] > 0) {
     355                $user = get_userdata($data['user_id']);
     356                $email_address = $user ? $user->user_email : '';
     357            } else {
     358                $ticket = self::get_ticket($data['ticket_id']);
     359                if ($ticket) {
     360                    $email_address = get_post_meta($ticket->id, 'customer_email', true);
     361                }
     362            }
     363           
     364            if (!empty($email_address)) {
     365                NexlifyDesk_Rate_Limiter::record_email_activity($data['user_id'], $email_address);
     366            }
     367        }
     368       
    285369        if (!empty($data['attachments'])) {
    286370            self::handle_attachments($data['attachments'], $data['ticket_id'], $reply_id, $data['user_id']);
     
    289373        $ticket = self::get_ticket($data['ticket_id']);
    290374       
    291         // Send notification email
    292375        if (!$data['is_internal_note']) {
    293             register_shutdown_function(function() use ($ticket, $reply_id) {
    294                 NexlifyDesk_Tickets::send_notification($ticket, 'new_reply', $reply_id);
     376            register_shutdown_function(function() use ($ticket, $reply_id, $data) {
     377                if (!empty($data['source']) && $data['source'] === 'email') {
     378                    NexlifyDesk_Tickets::send_email_channel_notification($ticket, 'new_reply', $reply_id);
     379                } else {
     380                    NexlifyDesk_Tickets::send_notification($ticket, 'new_reply', $reply_id);
     381                }
    295382            });
    296383        }
     384
     385        wp_cache_flush_group('nexlifydesk_tickets_grid');
    297386
    298387        return $reply_id;
     
    354443       
    355444        if (isset($attachments['name']) && is_array($attachments['name'])) {
     445            // Legacy multi-file format from $_FILES
    356446            $file_count = count($attachments['name']);
    357447           
     
    369459                );
    370460               
    371                 self::process_single_attachment($file_data, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types);
     461                $result = self::process_single_attachment($file_data, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types);
    372462            }
    373463        } else {
    374             if (isset($attachments['name']) && !is_array($attachments['name'])) {
    375                 self::process_single_attachment($attachments, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types);
    376             } else {
    377                 foreach ($attachments as $attachment) {
    378                     if (is_array($attachment) && isset($attachment['name'])) {
    379                         self::process_single_attachment($attachment, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types);
    380                     }
     464            // Process each attachment individually (new format from AJAX)
     465            foreach ($attachments as $attachment) {
     466                if (is_array($attachment) && isset($attachment['name'])) {
     467                    $result = self::process_single_attachment($attachment, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types);
    381468                }
    382469            }
     
    385472
    386473    private static function process_single_attachment($attachment, $ticket_id, $reply_id, $user_id, $max_size, $allowed_types) {
     474       
    387475        if ($attachment['size'] > $max_size) {
    388476            return false;
     
    423511
    424512        $upload_result = wp_handle_upload($file_data, array('test_form' => false));
    425 
     513       
    426514        if (!isset($upload_result['error']) && isset($upload_result['url'])) {
    427515            global $wpdb;
    428516           
     517            $table_name = NexlifyDesk_Database::get_table('attachments');
    429518            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    430519            $result = $wpdb->insert(
    431                 NexlifyDesk_Database::get_table('attachments'),
     520                $table_name,
    432521                array(
    433522                    'ticket_id'  => $ticket_id,
     
    465554                $query .= " AND reply_id IS NULL";
    466555            }
     556           
    467557            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled
    468558            $results = $wpdb->get_results(
     
    573663                }
    574664
     665            // Clear ticket grid cache when status is updated
     666            wp_cache_flush_group('nexlifydesk_tickets_grid');
     667
    575668            return true;
    576669        }
     
    585678        $user = get_userdata($ticket->user_id);
    586679        $admin_email = get_option('admin_email');
    587         $headers = array('Content-Type: text/html; charset=UTF-8');
     680       
     681        // Check if admin notifications are enabled
     682        $admin_notifications_enabled = !empty($settings['admin_email_notifications']);
     683       
     684        $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     685        $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
     686        $customer_email = $user ? $user->user_email : ($customer_details['email'] ?: null);
     687       
     688        $headers = array(
     689            'Content-Type: text/html; charset=UTF-8',
     690            'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>',
     691            'Message-ID: <ticket-' . $ticket->ticket_id . '-' . time() . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>',
     692            'In-Reply-To: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>',
     693            'References: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>'
     694        );
    588695
    589696        $ticket_url = add_query_arg(
     
    603710        switch ($type) {
    604711            case 'new_ticket':
    605                 // translators: %s: Ticket ID.
    606                 $subject = sprintf(__('New Support Ticket #%s', 'nexlifydesk'), $ticket->ticket_id);
     712                // translators: 1: Ticket ID, 2: Ticket subject.
     713                        $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
    607714                $message = self::get_email_template('new_ticket', $ticket);
    608715                $message = str_replace(
     
    613720
    614721                // Sending to customer
    615                 if ($user && !in_array($user->user_email, $emailed)) {
    616                     wp_mail($user->user_email, $subject, $message, $headers);
    617                     $emailed[] = $user->user_email;
    618                 }
    619 
    620                 // Sending to admin
    621                 if (!in_array($admin_email, $emailed)) {
    622                     wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers);
    623                     $emailed[] = $admin_email;
    624                 }
    625 
    626                 // Sending to assigned agent if exists and not already emailed
     722                if ($customer_email && !in_array($customer_email, $emailed)) {
     723                    wp_mail($customer_email, $subject, $message, $headers);
     724                    $emailed[] = $customer_email;
     725                }
     726
     727                // Sending to assigned agent if exists
    627728                if (!empty($ticket->assigned_to)) {
    628729                    $agent = get_userdata($ticket->assigned_to);
     
    632733                    }
    633734                }
     735
     736                // Send to admin only if:
     737                // 1. Admin notifications are enabled AND
     738                // 2. Either no agent is assigned OR the ticket is assigned to admin
     739                if ($admin_notifications_enabled && !in_array($admin_email, $emailed)) {
     740                    $should_notify_admin = false;
     741                   
     742                    if (empty($ticket->assigned_to)) {
     743                        // No agent assigned, notify admin
     744                        $should_notify_admin = true;
     745                    } else {
     746                        // Check if assigned to admin
     747                        $assigned_user = get_userdata($ticket->assigned_to);
     748                        if ($assigned_user && in_array('administrator', $assigned_user->roles)) {
     749                            $should_notify_admin = true;
     750                        }
     751                    }
     752                   
     753                    if ($should_notify_admin) {
     754                        wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers);
     755                        $emailed[] = $admin_email;
     756                    }
     757                }
    634758                break;
    635759
     
    644768                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    645769                        $reply = $wpdb->get_row($wpdb->prepare(
    646                             "SELECT is_internal_note FROM `{$wpdb->prefix}nexlifydesk_replies` WHERE id = %d",
     770                            "SELECT user_id, is_internal_note FROM `{$wpdb->prefix}nexlifydesk_replies` WHERE id = %d",
    647771                            $reply_id
    648772                        ));
     
    655779                }
    656780               
    657                 // translators: %s: Ticket ID.
    658                 $subject = sprintf(__('New Reply to Ticket #%s', 'nexlifydesk'), $ticket->ticket_id);
     781                // translators: 1: Ticket ID, 2: Ticket subject.
     782                $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
    659783                $message = self::get_email_template('new_reply', $ticket, $reply_id);
    660784                $message = str_replace(
     
    664788                );
    665789
    666                 if ($ticket->user_id != get_current_user_id()) {
    667                     if ($user && !in_array($user->user_email, $emailed)) {
    668                         wp_mail($user->user_email, $subject, $message, $headers);
    669                         $emailed[] = $user->user_email;
     790                $reply_user_id = $reply ? $reply->user_id : get_current_user_id();
     791                $reply_user = get_userdata($reply_user_id);
     792                $is_agent_reply = $reply_user && (in_array('administrator', $reply_user->roles) || in_array('nexlifydesk_agent', $reply_user->roles));
     793
     794                if ($is_agent_reply) {
     795                    if ($customer_email && !in_array($customer_email, $emailed)) {
     796                        wp_mail($customer_email, $subject, $message, $headers);
     797                        $emailed[] = $customer_email;
    670798                    }
    671799                } else {
     
    673801                        $agent = get_userdata($ticket->assigned_to);
    674802                        if ($agent && !in_array($agent->user_email, $emailed)) {
    675                             wp_mail($agent->user_email, $subject, $message, $headers);
     803                            wp_mail($agent->user_email, '[Agent] ' . $subject, $message, $headers);
    676804                            $emailed[] = $agent->user_email;
    677805                        }
    678                     } else {
     806                    } else if ($admin_notifications_enabled) {
    679807                        if (!in_array($admin_email, $emailed)) {
    680                             wp_mail($admin_email, $subject, $message, $headers);
     808                            wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers);
    681809                            $emailed[] = $admin_email;
    682810                        }
     
    686814
    687815            case 'status_changed':
    688                 // translators: %s: Ticket ID.
    689                 $subject = sprintf(__('Ticket #%s Status Changed', 'nexlifydesk'), $ticket->ticket_id);
     816                // translators: 1: Ticket ID, 2: Ticket subject.
     817                $subject = sprintf(__('[#%1$s] %2$s - Status Changed', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
    690818                $message = self::get_email_template('status_changed', $ticket);
    691819                $message = str_replace(
     
    694822                    $message
    695823                );
    696                 if ($user && !in_array($user->user_email, $emailed)) {
    697                     wp_mail($user->user_email, $subject, $message, $headers);
    698                     $emailed[] = $user->user_email;
     824               
     825                if ($customer_email && !in_array($customer_email, $emailed)) {
     826                    wp_mail($customer_email, $subject, $message, $headers);
     827                    $emailed[] = $customer_email;
     828                }
     829               
     830                if (!empty($ticket->assigned_to)) {
     831                    $agent = get_userdata($ticket->assigned_to);
     832                    if ($agent && !in_array($agent->user_email, $emailed)) {
     833                        wp_mail($agent->user_email, '[Agent] ' . $subject, $message, $headers);
     834                        $emailed[] = $agent->user_email;
     835                    }
     836                }
     837               
     838                if ($admin_notifications_enabled && !in_array($admin_email, $emailed)) {
     839                    $should_notify_admin = false;
     840                   
     841                    if (empty($ticket->assigned_to)) {
     842                        $should_notify_admin = true;
     843                    } else {
     844                        $assigned_user = get_userdata($ticket->assigned_to);
     845                        if ($assigned_user && in_array('administrator', $assigned_user->roles)) {
     846                            $should_notify_admin = true;
     847                        }
     848                    }
     849                   
     850                    if ($should_notify_admin) {
     851                        wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers);
     852                        $emailed[] = $admin_email;
     853                    }
    699854                }
    700855                break;
    701856
    702857            case 'sla_breach':
    703                 // translators: %s: Ticket ID.
    704                                 $subject = sprintf(__('SLA Breach: Ticket #%s', 'nexlifydesk'), $ticket->ticket_id);
     858                // translators: 1: Ticket ID, 2: Ticket subject. 
     859                $subject = sprintf(__('[#%1$s] SLA Breach: %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
    705860                $message = self::get_email_template('sla_breach', $ticket);
    706861                $message = str_replace(
     
    709864                    $message
    710865                );
     866               
    711867                if (!in_array($admin_email, $emailed)) {
    712                     wp_mail($admin_email, $subject, $message, $headers);
     868                    wp_mail($admin_email, '[Admin] ' . $subject, $message, $headers);
    713869                    $emailed[] = $admin_email;
     870                }
     871               
     872                if (!empty($ticket->assigned_to)) {
     873                    $agent = get_userdata($ticket->assigned_to);
     874                    if ($agent && !in_array($agent->user_email, $emailed)) {
     875                        wp_mail($agent->user_email, '[Agent] ' . $subject, $message, $headers);
     876                        $emailed[] = $agent->user_email;
     877                    }
    714878                }
    715879                break;
     
    728892
    729893        $user = get_userdata($ticket->user_id);
     894       
     895        $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     896        $customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
     897        $customer_email = $user ? $user->user_email : ($customer_details['email'] ?: '');
     898        $clean_message = $customer_details['message'] ?: $ticket->message;
     899       
    730900        $placeholders = array(
    731901            '{ticket_id}'   => esc_html($ticket->ticket_id),
    732902            '{subject}'     => esc_html($ticket->subject),
    733             '{message}'     => wp_kses_post($ticket->message),
    734             '{user_name}'   => $user ? esc_html($user->display_name) : '',
    735             '{user_email}'  => $user ? esc_html($user->user_email) : '',
     903            '{message}'     => wpautop(wp_kses_post($clean_message)),
     904            '{user_name}'   => esc_html($customer_name),
     905            '{user_email}'  => esc_html($customer_email),
    736906            '{status}'      => esc_html(ucfirst($ticket->status)),
    737907            '{priority}'    => esc_html(ucfirst($ticket->priority)),
     
    766936            }
    767937            if ($reply) {
    768                 $placeholders['{reply_message}'] = $reply->message;
     938                $reply_customer_details = nexlifydesk_extract_customer_details($reply->message);
     939                $clean_reply_message = $reply_customer_details['message'] ?: $reply->message;
     940               
     941                $clean_reply_message = wp_kses_post($clean_reply_message);
     942                $clean_reply_message = wpautop($clean_reply_message);
     943               
     944                $placeholders['{reply_message}'] = $clean_reply_message;
    769945                $reply_user = get_userdata($reply->user_id);
    770                 $placeholders['{reply_user_name}'] = $reply_user ? $reply_user->display_name : '';
     946                $reply_customer_name = $reply_user ? $reply_user->display_name : ($reply_customer_details['name'] ?: 'Guest');
     947                $placeholders['{reply_user_name}'] = esc_html($reply_customer_name);
    771948            }
    772949        }
     
    775952
    776953        return $content;
     954    }
     955
     956    /**
     957     * Send email notifications for tickets created via email channel
     958     * Simple auto-response for new tickets, raw email replies for ongoing conversation
     959     *
     960     * @param object $ticket The ticket object
     961     * @param string $type The notification type ('new_ticket' or 'new_reply')
     962     * @param int $reply_id Optional reply ID for reply notifications
     963     */
     964    public static function send_email_channel_notification($ticket, $type, $reply_id = null) {
     965        $settings = get_option('nexlifydesk_settings');
     966        if (empty($settings['email_notifications'])) return;
     967
     968        $admin_email = get_option('admin_email');
     969        $admin_notifications_enabled = !empty($settings['admin_email_notifications']);
     970       
     971        $customer_email = get_post_meta($ticket->id, 'customer_email', true);
     972        if (empty($customer_email)) {
     973            $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     974            $customer_email = $customer_details['email'] ?: null;
     975        }
     976       
     977        if (empty($customer_email)) {
     978            return;
     979        }
     980
     981        $headers = array(
     982            'Content-Type: text/html; charset=UTF-8',
     983            'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>',
     984            'Message-ID: <ticket-' . $ticket->ticket_id . '-' . time() . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>',
     985            'In-Reply-To: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>',
     986            'References: <ticket-' . $ticket->ticket_id . '@' . wp_parse_url(home_url(), PHP_URL_HOST) . '>'
     987        );
     988
     989        switch ($type) {
     990            case 'new_ticket':
     991                self::send_auto_response($ticket, $customer_email, $headers);
     992               
     993                self::notify_admin_about_email_ticket($ticket, $admin_email, $headers, $admin_notifications_enabled);
     994                break;
     995
     996            case 'new_reply':
     997                self::send_raw_email_reply($ticket, $reply_id, $customer_email, $headers);
     998                break;
     999        }
     1000    }
     1001
     1002    /**
     1003     * Send auto-response to customer for new email tickets
     1004     */
     1005    private static function send_auto_response($ticket, $customer_email, $headers) {
     1006        $templates = get_option('nexlifydesk_email_templates', array());
     1007       
     1008        $auto_response_message = isset($templates['email_auto_response']) && !empty($templates['email_auto_response'])
     1009            ? $templates['email_auto_response']
     1010            : self::get_default_auto_response();
     1011
     1012        $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     1013        $customer_name = $customer_details['name'] ?: 'Customer';
     1014       
     1015        $placeholders = array(
     1016            '{customer_name}' => esc_html($customer_name),
     1017            '{ticket_id}' => esc_html($ticket->ticket_id),
     1018            '{subject}' => esc_html($ticket->subject),
     1019            '{site_name}' => esc_html(get_bloginfo('name')),
     1020            '{site_url}' => esc_url(home_url()),
     1021            '{admin_email}' => esc_html(get_option('admin_email'))
     1022        );
     1023
     1024        $message = strtr($auto_response_message, $placeholders);
     1025       
     1026        // translators: 1: Ticket ID, 2: Ticket subject.
     1027        $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
     1028
     1029        wp_mail($customer_email, $subject, $message, $headers);
     1030    }
     1031
     1032    /**
     1033     * Send raw email reply to customer (as if agent is replying directly)
     1034     */
     1035    private static function send_raw_email_reply($ticket, $reply_id, $customer_email, $headers) {
     1036        global $wpdb;
     1037       
     1038        if (!$reply_id) return;
     1039
     1040        $reply_cache_key = 'nexlifydesk_reply_' . intval($reply_id);
     1041        $reply = wp_cache_get($reply_cache_key);
     1042        if (false === $reply) {
     1043            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1044            $reply = $wpdb->get_row($wpdb->prepare(
     1045                "SELECT r.*, u.display_name, u.user_email
     1046                 FROM `{$wpdb->prefix}nexlifydesk_replies` r
     1047                 LEFT JOIN {$wpdb->users} u ON r.user_id = u.ID
     1048                 WHERE r.id = %d",
     1049                $reply_id
     1050            ));
     1051            wp_cache_set($reply_cache_key, $reply, '', 300);
     1052        }
     1053
     1054        if (!$reply) return;
     1055
     1056        $reply_user = get_userdata($reply->user_id);
     1057        $is_agent_reply = $reply_user && (
     1058            in_array('administrator', $reply_user->roles) ||
     1059            in_array('nexlifydesk_agent', $reply_user->roles)
     1060        );
     1061
     1062        if (!$is_agent_reply) {
     1063            $settings = get_option('nexlifydesk_settings');
     1064            $admin_notifications_enabled = !empty($settings['admin_email_notifications']);
     1065            self::notify_admin_about_email_reply($ticket, $reply, $headers, $admin_notifications_enabled);
     1066            return;
     1067        }
     1068
     1069        $clean_message = $reply->message;
     1070        $clean_message = preg_replace('/\[Customer Details\].*?\[Message\]/s', '', $clean_message);
     1071        $clean_message = preg_replace('/\[Customer Details\].*?\[Reply\]/s', '', $clean_message);
     1072        $clean_message = trim($clean_message);
     1073       
     1074        if (strpos($clean_message, '<') !== false) {
     1075            $clean_message = wp_strip_all_tags($clean_message);
     1076            $clean_message = html_entity_decode($clean_message, ENT_QUOTES, 'UTF-8');
     1077        }
     1078
     1079        $from_name = $reply_user->display_name ?: get_bloginfo('name');
     1080        $from_email = $reply_user->user_email ?: get_option('admin_email');
     1081        $headers[1] = 'From: ' . $from_name . ' <' . $from_email . '>';
     1082        // translators: 1: Ticket ID, 2: Ticket subject.
     1083        $subject = sprintf(__('[#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
     1084
     1085        wp_mail($customer_email, $subject, $clean_message, $headers);
     1086    }
     1087
     1088    /**
     1089     * Notify admin/agents about new email ticket
     1090     */
     1091    private static function notify_admin_about_email_ticket($ticket, $admin_email, $headers, $admin_notifications_enabled = true) {
     1092        $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     1093        $customer_name = $customer_details['name'] ?: 'Unknown Customer';
     1094        $customer_email = $customer_details['email'] ?: 'Unknown Email';
     1095        $clean_message = $customer_details['message'] ?: $ticket->message;
     1096
     1097        $is_html = false;
     1098        if (is_array($headers)) {
     1099            foreach ($headers as $header) {
     1100                if (stripos($header, 'Content-Type: text/html') !== false) {
     1101                    $is_html = true;
     1102                    break;
     1103                }
     1104            }
     1105        } else {
     1106            $is_html = (stripos($headers, 'Content-Type: text/html') !== false);
     1107        }
     1108
     1109        $formatted_message = $clean_message;
     1110        if ($is_html) {
     1111            $formatted_message = nl2br(esc_html($formatted_message));
     1112        } else {
     1113            $formatted_message = str_replace(array("\r\n", "\r"), "\n", $formatted_message);
     1114        }
     1115
     1116        if ($is_html) {
     1117            $admin_message = sprintf(
     1118                "<strong>New support ticket received via email:</strong><br><br>" .
     1119                "<strong>Ticket ID:</strong> %s<br>" .
     1120                "<strong>From:</strong> %s &lt;%s&gt;<br>" .
     1121                "<strong>Subject:</strong> %s<br>" .
     1122                "<strong>Priority:</strong> %s<br><br>" .
     1123                "<strong>Message:</strong><br>%s<br><br>" .
     1124                "<a href=\"%s\">View ticket</a>",
     1125                esc_html($ticket->ticket_id),
     1126                esc_html($customer_name),
     1127                esc_html($customer_email),
     1128                esc_html($ticket->subject),
     1129                esc_html(ucfirst($ticket->priority)),
     1130                $formatted_message,
     1131                esc_url(add_query_arg(
     1132                    array(
     1133                        'page' => 'nexlifydesk_tickets',
     1134                        'ticket_id' => $ticket->id,
     1135                    ),
     1136                    admin_url('admin.php')
     1137                ))
     1138            );
     1139        } else {
     1140            $admin_message = sprintf(
     1141                "New support ticket received via email:\n\n" .
     1142                "Ticket ID: %s\n" .
     1143                "From: %s <%s>\n" .
     1144                "Subject: %s\n" .
     1145                "Priority: %s\n\n" .
     1146                "Message:\n%s\n\n" .
     1147                "View ticket: %s",
     1148                $ticket->ticket_id,
     1149                $customer_name,
     1150                $customer_email,
     1151                $ticket->subject,
     1152                ucfirst($ticket->priority),
     1153                $formatted_message,
     1154                add_query_arg(
     1155                    array(
     1156                        'page' => 'nexlifydesk_tickets',
     1157                        'ticket_id' => $ticket->id,
     1158                    ),
     1159                    admin_url('admin.php')
     1160                )
     1161            );
     1162        }
     1163
     1164        // translators: 1: Ticket ID, 2: Ticket subject.
     1165        $subject = sprintf(__('[New Email Ticket] [#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
     1166
     1167        // Notify admin only if admin notifications are enabled and conditions are met
     1168        if ($admin_notifications_enabled) {
     1169            $should_notify_admin = false;
     1170           
     1171            if (empty($ticket->assigned_to)) {
     1172                $should_notify_admin = true;
     1173            } else {
     1174                $assigned_user = get_userdata($ticket->assigned_to);
     1175                if ($assigned_user && in_array('administrator', $assigned_user->roles)) {
     1176                    $should_notify_admin = true;
     1177                }
     1178            }
     1179           
     1180            if ($should_notify_admin) {
     1181                wp_mail($admin_email, $subject, $admin_message, $headers);
     1182            }
     1183        }
     1184
     1185        if (!empty($ticket->assigned_to)) {
     1186            $agent = get_userdata($ticket->assigned_to);
     1187            if ($agent && $agent->user_email !== $admin_email) {
     1188                wp_mail($agent->user_email, '[Agent] ' . $subject, $admin_message, $headers);
     1189            }
     1190        }
     1191    }
     1192
     1193    /**
     1194     * Notify admin/agents about customer email reply
     1195     */
     1196    private static function notify_admin_about_email_reply($ticket, $reply, $headers, $admin_notifications_enabled = true) {
     1197        $admin_email = get_option('admin_email');
     1198       
     1199        $customer_details = nexlifydesk_extract_customer_details($reply->message);
     1200        $customer_name = $customer_details['name'] ?: 'Unknown Customer';
     1201        $customer_email = $customer_details['email'] ?: 'Unknown Email';
     1202        $clean_message = $customer_details['message'] ?: $reply->message;
     1203
     1204        $is_html = false;
     1205        if (is_array($headers)) {
     1206            foreach ($headers as $header) {
     1207                if (stripos($header, 'Content-Type: text/html') !== false) {
     1208                    $is_html = true;
     1209                    break;
     1210                }
     1211            }
     1212        } else {
     1213            $is_html = (stripos($headers, 'Content-Type: text/html') !== false);
     1214        }
     1215
     1216        $formatted_message = $clean_message;
     1217        if ($is_html) {
     1218            $formatted_message = nl2br(esc_html($formatted_message));
     1219        } else {
     1220            $formatted_message = str_replace(array("\r\n", "\r"), "\n", $formatted_message);
     1221        }
     1222
     1223        if ($is_html) {
     1224            $admin_message = sprintf(
     1225                "<strong>Customer replied to ticket via email:</strong><br><br>" .
     1226                "<strong>Ticket ID:</strong> %s<br>" .
     1227                "<strong>From:</strong> %s &lt;%s&gt;<br>" .
     1228                "<strong>Subject:</strong> %s<br><br>" .
     1229                "<strong>Reply:</strong><br>%s<br><br>" .
     1230                "<a href=\"%s\">View ticket</a>",
     1231                esc_html($ticket->ticket_id),
     1232                esc_html($customer_name),
     1233                esc_html($customer_email),
     1234                esc_html($ticket->subject),
     1235                $formatted_message,
     1236                esc_url(add_query_arg(
     1237                    array(
     1238                        'page' => 'nexlifydesk_tickets',
     1239                        'ticket_id' => $ticket->id,
     1240                    ),
     1241                    admin_url('admin.php')
     1242                ))
     1243            );
     1244        } else {
     1245            $admin_message = sprintf(
     1246                "Customer replied to ticket via email:\n\n" .
     1247                "Ticket ID: %s\n" .
     1248                "From: %s <%s>\n" .
     1249                "Subject: %s\n\n" .
     1250                "Reply:\n%s\n\n" .
     1251                "View ticket: %s",
     1252                $ticket->ticket_id,
     1253                $customer_name,
     1254                $customer_email,
     1255                $ticket->subject,
     1256                $formatted_message,
     1257                add_query_arg(
     1258                    array(
     1259                        'page' => 'nexlifydesk_tickets',
     1260                        'ticket_id' => $ticket->id,
     1261                    ),
     1262                    admin_url('admin.php')
     1263                )
     1264            );
     1265        }
     1266
     1267        // translators: 1: Ticket ID, 2: Ticket subject.
     1268        $subject = sprintf(__('[Customer Reply] [#%1$s] %2$s', 'nexlifydesk'), $ticket->ticket_id, $ticket->subject);
     1269
     1270        if ($admin_notifications_enabled) {
     1271            $should_notify_admin = false;
     1272           
     1273            if (empty($ticket->assigned_to)) {
     1274                $should_notify_admin = true;
     1275            } else {
     1276                $assigned_user = get_userdata($ticket->assigned_to);
     1277                if ($assigned_user && in_array('administrator', $assigned_user->roles)) {
     1278                    $should_notify_admin = true;
     1279                }
     1280            }
     1281           
     1282            if ($should_notify_admin) {
     1283                wp_mail($admin_email, $subject, $admin_message, $headers);
     1284            }
     1285        }
     1286
     1287        if (!empty($ticket->assigned_to)) {
     1288            $agent = get_userdata($ticket->assigned_to);
     1289            if ($agent && $agent->user_email !== $admin_email) {
     1290                wp_mail($agent->user_email, '[Agent] ' . $subject, $admin_message, $headers);
     1291            }
     1292        }
     1293    }
     1294
     1295    /**
     1296     * Get default auto-response message
     1297     */
     1298    private static function get_default_auto_response() {
     1299        return "Hello {customer_name},\n\n" .
     1300               "Thank you for contacting us. We have received your support request and have assigned it ticket ID #{ticket_id}.\n\n" .
     1301               "Subject: {subject}\n\n" .
     1302               "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.\n\n" .
     1303               "Best regards,\n" .
     1304               "{site_name} Support Team\n" .
     1305               "{site_url}";
    7771306    }
    7781307
     
    10601589   
    10611590    /**
    1062      * Check for duplicate tickets based on subject, order number, or content similarity
     1591     * Check for duplicate tickets based on simple rules:
     1592     * - For non-registered users: Check if they have any active ticket (any status except closed/resolved)
     1593     * - For registered users: Check for subject similarity only
    10631594     *
    10641595     * @param array $data Ticket data to check for duplicates
     
    10681599        global $wpdb;
    10691600       
     1601        // Clear relevant caches to ensure fresh duplicate detection
    10701602        $user_id = isset($data['user_id']) ? absint($data['user_id']) : get_current_user_id();
     1603        $email = isset($data['email']) ? sanitize_email($data['email']) : '';
    10711604        $subject = sanitize_text_field($data['subject']);
    1072         $message = wp_kses_post($data['message']);
    1073        
    1074         // Get settings for duplicate detection
     1605        $source = isset($data['source']) ? sanitize_text_field($data['source']) : '';
     1606       
    10751607        $settings = get_option('nexlifydesk_settings', array());
    10761608        $check_duplicates = isset($settings['check_duplicates']) ? $settings['check_duplicates'] : true;
    1077         $duplicate_threshold = isset($settings['duplicate_threshold']) ? absint($settings['duplicate_threshold']) : 80; // 80% similarity
    10781609       
    10791610        if (!$check_duplicates) {
     
    10811612        }
    10821613       
    1083         // Check for exact subject match from same user in last 30 days
    10841614        $table_name = NexlifyDesk_Database::get_table('tickets');
    1085         // Use cache to avoid repeated direct DB queries
    1086         $cache_key = 'nexlifydesk_duplicate_ticket_' . intval($user_id) . '_' . md5($subject);
    1087         $existing_ticket = wp_cache_get($cache_key);
    1088 
    1089         if (false === $existing_ticket) {
     1615       
     1616        if ($user_id == 0 && !empty($email)) {
     1617           
    10901618            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    10911619            $existing_ticket = $wpdb->get_row($wpdb->prepare(
    1092                 "SELECT * FROM {$table_name}
    1093                  WHERE user_id = %d
    1094                  AND subject = %s
    1095                  AND status NOT IN ('closed', 'resolved')
    1096                  AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
    1097                  ORDER BY created_at DESC
     1620                "SELECT t.* FROM {$table_name} t
     1621                 LEFT JOIN {$wpdb->prefix}postmeta pm ON pm.post_id = t.id AND pm.meta_key = 'customer_email'
     1622                 WHERE t.user_id = 0
     1623                 AND pm.meta_value = %s
     1624                 AND t.status IN ('open', 'pending', 'in_progress')
     1625                 ORDER BY t.created_at DESC
    10981626                 LIMIT 1",
    1099                 $user_id,
    1100                 $subject
     1627                $email
    11011628            ));
    1102             wp_cache_set($cache_key, $existing_ticket, '', 300);
    1103         }
    1104 
    1105         if ($existing_ticket) {
    1106             return $existing_ticket;
    1107         }
    1108        
    1109         $order_patterns = array(
    1110             '/order\s*#?\s*([a-zA-Z0-9\-_]+)/i',
    1111             '/order\s*id\s*:?\s*([a-zA-Z0-9\-_]+)/i',
    1112             '/order\s*number\s*:?\s*([a-zA-Z0-9\-_]+)/i',
    1113             '/#([a-zA-Z0-9\-_]{6,})/i',
    1114             '/invoice\s*#?\s*([a-zA-Z0-9\-_]+)/i'
    1115         );
    1116        
    1117         foreach ($order_patterns as $pattern) {
    1118             if (preg_match($pattern, $subject . ' ' . $message, $matches)) {
    1119                 $order_identifier = sanitize_text_field($matches[1]);
    1120                
     1629           
     1630            if (!$existing_ticket) {
     1631                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1632                $existing_ticket = $wpdb->get_row($wpdb->prepare(
     1633                    "SELECT t.* FROM {$table_name} t
     1634                     WHERE t.user_id = 0
     1635                     AND t.message LIKE %s
     1636                     AND t.status IN ('open', 'pending', 'in_progress')
     1637                     ORDER BY t.created_at DESC
     1638                     LIMIT 1",
     1639                    '%Email: ' . $email . '%'
     1640                ));
     1641            }
     1642           
     1643            if ($existing_ticket) {
     1644                return $existing_ticket;
     1645            }
     1646        }
     1647       
     1648        if ($user_id > 0) {
     1649           
     1650            $cache_key = 'nexlifydesk_registered_user_duplicate_' . intval($user_id) . '_' . md5($subject);
     1651            $existing_ticket = wp_cache_get($cache_key);
     1652           
     1653            if (false === $existing_ticket) {
    11211654                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    11221655                $existing_ticket = $wpdb->get_row($wpdb->prepare(
    11231656                    "SELECT * FROM {$table_name}
    11241657                     WHERE user_id = %d
    1125                      AND (subject LIKE %s OR message LIKE %s)
    1126                      AND status NOT IN ('closed', 'resolved')
    1127                      AND created_at > DATE_SUB(NOW(), INTERVAL 60 DAY)
     1658                     AND subject = %s
     1659                     AND status IN ('open', 'pending', 'in_progress')
     1660                     AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
    11281661                     ORDER BY created_at DESC
    11291662                     LIMIT 1",
    11301663                    $user_id,
    1131                     '%' . $wpdb->esc_like($order_identifier) . '%',
    1132                     '%' . $wpdb->esc_like($order_identifier) . '%'
     1664                    $subject
    11331665                ));
    1134                
    1135                 if ($existing_ticket) {
    1136                     return $existing_ticket;
    1137                 }
    1138             }
    1139         }
    1140        
    1141         $subject_words = array_filter(explode(' ', strtolower($subject)));
    1142         $message_words = array_filter(explode(' ', strtolower($message)));
    1143        
    1144         $stop_words = array('the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'cannot', 'cant', 'wont', 'dont', 'doesnt', 'didnt', 'hasnt', 'havent', 'hadnt', 'wouldnt', 'couldnt', 'shouldnt', 'mightnt', 'mustnt', 'isnt', 'arent', 'wasnt', 'werent', 'a', 'an', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them', 'my', 'your', 'his', 'her', 'its', 'our', 'their');
    1145        
    1146         $subject_keywords = array_diff($subject_words, $stop_words);
    1147         $message_keywords = array_diff($message_words, $stop_words);
    1148         $all_keywords = array_merge($subject_keywords, $message_keywords);
    1149        
    1150         if (count($all_keywords) >= 3) {
     1666                wp_cache_set($cache_key, $existing_ticket, '', 300);
     1667            }
     1668           
     1669            if ($existing_ticket) {
     1670                return $existing_ticket;
     1671            }
     1672        }
     1673       
     1674        return false;
     1675    }
     1676
     1677    /**
     1678     * Get tickets for the admin grid view.
     1679     *
     1680     * @param array $args
     1681     * @return array
     1682     */
     1683    public static function get_tickets_for_grid($args = []) {
     1684        global $wpdb;
     1685
     1686        $defaults = [
     1687            'status' => 'all',
     1688            'priority' => 'all',
     1689            'search' => '',
     1690            'per_page' => 20,
     1691            'offset' => 0,
     1692        ];
     1693        $args = wp_parse_args($args, $defaults);
     1694
     1695        $where_clauses = ['1=1'];
     1696        $query_params = [];
     1697
     1698        if ($args['status'] !== 'all') {
     1699            $where_clauses[] = 't.status = %s';
     1700            $query_params[] = $args['status'];
     1701        }
     1702        if ($args['priority'] !== 'all') {
     1703            $where_clauses[] = 't.priority = %s';
     1704            $query_params[] = $args['priority'];
     1705        }
     1706        if (!empty($args['search'])) {
     1707            $search_term = '%' . $wpdb->esc_like($args['search']) . '%';
     1708            $where_clauses[] = '(t.subject LIKE %s OR t.message LIKE %s OR u.display_name LIKE %s OR u.user_email LIKE %s OR t.ticket_id LIKE %s)';
     1709            $query_params[] = $search_term;
     1710            $query_params[] = $search_term;
     1711            $query_params[] = $search_term;
     1712            $query_params[] = $search_term;
     1713            $query_params[] = $search_term;
     1714        }
     1715
     1716        $where_sql = implode(' AND ', $where_clauses);
     1717
     1718        $query_params[] = (int) $args['per_page'];
     1719        $query_params[] = (int) $args['offset'];
     1720
     1721        $current_user_id = get_current_user_id();
     1722       
     1723        $cache_key = 'nexlifydesk_tickets_grid_v3_' . md5(serialize($args) . $current_user_id);
     1724        $results = wp_cache_get($cache_key, 'nexlifydesk_tickets_grid');
     1725        if ($results === false) {
    11511726            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
    1152             $recent_tickets = $wpdb->get_results($wpdb->prepare(
    1153                 "SELECT * FROM {$table_name}
    1154                  WHERE user_id = %d
    1155                  AND status NOT IN ('closed', 'resolved')
    1156                  AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
    1157                  ORDER BY created_at DESC
    1158                  LIMIT 20",
    1159                 $user_id
    1160             ));
    1161            
    1162             foreach ($recent_tickets as $ticket) {
    1163                 $ticket_content = strtolower($ticket->subject . ' ' . $ticket->message);
    1164                 $matching_keywords = 0;
    1165                
    1166                 foreach ($all_keywords as $keyword) {
    1167                     if (strlen($keyword) > 2 && strpos($ticket_content, $keyword) !== false) {
    1168                         $matching_keywords++;
     1727            $results = $wpdb->get_results(
     1728                $wpdb->prepare(
     1729                    "SELECT t.*,
     1730                           u.display_name as user_name,
     1731                           u.user_email as user_email,
     1732                           a.display_name as assigned_to_display_name,
     1733                           COALESCE(MAX(r.created_at), t.created_at) as last_reply_time,
     1734                           CASE
     1735                               WHEN NOT EXISTS (
     1736                                   SELECT 1 FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr
     1737                                   WHERE tr.ticket_id = t.id AND tr.user_id = %d
     1738                               ) THEN 1
     1739                               WHEN EXISTS (
     1740                                   SELECT 1 FROM {$wpdb->prefix}nexlifydesk_replies r2
     1741                                   WHERE r2.ticket_id = t.id
     1742                                   AND r2.user_id != %d
     1743                                   AND r2.created_at > COALESCE(
     1744                                       (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr2
     1745                                        WHERE tr2.ticket_id = t.id AND tr2.user_id = %d),
     1746                                       t.created_at
     1747                                   )
     1748                                   AND r2.is_internal_note = 0
     1749                               ) THEN 1
     1750                               WHEN t.created_at > COALESCE(
     1751                                   (SELECT read_at FROM {$wpdb->prefix}nexlifydesk_ticket_reads tr3
     1752                                    WHERE tr3.ticket_id = t.id AND tr3.user_id = %d),
     1753                                   '1970-01-01'
     1754                               ) THEN 1
     1755                               ELSE 0
     1756                           END as is_unread
     1757                    FROM {$wpdb->prefix}nexlifydesk_tickets t
     1758                    LEFT JOIN {$wpdb->users} u ON t.user_id = u.ID
     1759                    LEFT JOIN {$wpdb->users} a ON t.assigned_to = a.ID
     1760                    LEFT JOIN {$wpdb->prefix}nexlifydesk_replies r ON t.id = r.ticket_id
     1761                    WHERE {$where_sql}
     1762                    GROUP BY t.id
     1763                    ORDER BY is_unread DESC, last_reply_time DESC
     1764                    LIMIT %d OFFSET %d",
     1765                    $current_user_id,
     1766                    $current_user_id,
     1767                    $current_user_id,
     1768                    $current_user_id,
     1769                    ...$query_params
     1770                )
     1771            );
     1772           
     1773            if (!empty($results)) {
     1774                foreach ($results as $ticket) {
     1775                    if (!$ticket->user_id || empty($ticket->user_name)) {
     1776                        if (function_exists('nexlifydesk_extract_customer_details')) {
     1777                            $customer_details = nexlifydesk_extract_customer_details($ticket->message);
     1778                            $ticket->user_name = $customer_details['name'] ?: 'Guest';
     1779                            $ticket->user_email = $customer_details['email'] ?: 'N/A';
     1780                        } else {
     1781                            $ticket->user_name = 'Guest';
     1782                            $ticket->user_email = 'N/A';
     1783                        }
    11691784                    }
    11701785                }
    1171                
    1172                 $similarity_score = ($matching_keywords / count($all_keywords)) * 100;
    1173                
    1174                 if ($similarity_score >= $duplicate_threshold) {
    1175                     return $ticket;
    1176                 }
    1177             }
     1786            }
     1787           
     1788            wp_cache_set($cache_key, $results, 'nexlifydesk_tickets_grid', 30);
     1789        }
     1790       
     1791        return $results;
     1792    }
     1793
     1794    /**
     1795     * Update ticket priority
     1796     *
     1797     * @param int $ticket_id Ticket ID
     1798     * @param string $priority New priority
     1799     * @return bool Success
     1800     */
     1801    public static function update_ticket_priority($ticket_id, $priority) {
     1802        global $wpdb;
     1803       
     1804        $allowed_priorities = array('low', 'medium', 'high', 'urgent');
     1805        if (!in_array($priority, $allowed_priorities)) {
     1806            return false;
     1807        }
     1808       
     1809        $ticket_before = self::get_ticket($ticket_id);
     1810        if (!$ticket_before) {
     1811            return false;
     1812        }
     1813       
     1814        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled
     1815        $result = $wpdb->update(
     1816            NexlifyDesk_Database::get_table('tickets'),
     1817            array(
     1818                'priority' => $priority,
     1819                'updated_at' => current_time('mysql')
     1820            ),
     1821            array('id' => $ticket_id),
     1822            array('%s', '%s'),
     1823            array('%d')
     1824        );
     1825       
     1826        if ($result !== false) {
     1827            // Clear cache
     1828            wp_cache_delete('nexlifydesk_ticket_' . $ticket_id);
     1829           
     1830            return true;
    11781831        }
    11791832       
    11801833        return false;
    11811834    }
     1835
     1836    /**
     1837     * Assign ticket to an agent
     1838     *
     1839     * @param int $ticket_id Ticket ID
     1840     * @param int $agent_id Agent user ID (0 for unassigned)
     1841     * @return bool Success
     1842     */
     1843    public static function assign_ticket($ticket_id, $agent_id) {
     1844        global $wpdb;
     1845       
     1846        $ticket_before = self::get_ticket($ticket_id);
     1847        if (!$ticket_before) {
     1848            return false;
     1849        }
     1850       
     1851        if ($agent_id > 0) {
     1852            $agent = get_userdata($agent_id);
     1853            if (!$agent) {
     1854                return false;
     1855            }
     1856           
     1857            if (!user_can($agent_id, 'nexlifydesk_manage_tickets') && !user_can($agent_id, 'administrator')) {
     1858                return false;
     1859            }
     1860        }
     1861       
     1862        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Table name is safe and controlled
     1863        $result = $wpdb->update(
     1864            NexlifyDesk_Database::get_table('tickets'),
     1865            array(
     1866                'assigned_to' => $agent_id === 0 ? null : $agent_id,
     1867                'updated_at' => current_time('mysql')
     1868            ),
     1869            array('id' => $ticket_id),
     1870            array('%d', '%s'),
     1871            array('%d')
     1872        );
     1873       
     1874        if ($result !== false) {
     1875            // Clear cache
     1876            wp_cache_delete('nexlifydesk_ticket_' . $ticket_id);
     1877           
     1878            wp_cache_flush_group('nexlifydesk_tickets_grid');
     1879           
     1880            return true;
     1881        }
     1882       
     1883        return false;
     1884    }
     1885   
     1886    /**
     1887     * Get tickets for real-time refresh with unread status
     1888     *
     1889     * @param int $last_refresh_timestamp Unix timestamp of last refresh (unused for now)
     1890     * @return array Array of ticket objects with unread status
     1891     */
     1892    public static function get_tickets_for_refresh($last_refresh_timestamp = 0) {
     1893        return self::get_tickets_for_grid(['per_page' => 50]);
     1894    }
     1895   
     1896    /**
     1897     * Mark ticket as read for a specific user
     1898     *
     1899     * @param int $ticket_id Ticket ID
     1900     * @param int $user_id User ID
     1901     * @return bool Success
     1902     */
     1903    public static function mark_ticket_as_read($ticket_id, $user_id) {
     1904        global $wpdb;
     1905       
     1906        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1907        $result = $wpdb->replace(
     1908            $wpdb->prefix . 'nexlifydesk_ticket_reads',
     1909            array(
     1910                'ticket_id' => $ticket_id,
     1911                'user_id' => $user_id,
     1912                'read_at' => current_time('mysql')
     1913            ),
     1914            array('%d', '%d', '%s')
     1915        );
     1916       
     1917        if ($result) {
     1918            // Clear relevant caches
     1919            wp_cache_delete('nexlifydesk_ticket_' . $ticket_id);
     1920            return true;
     1921        }
     1922       
     1923        return false;
     1924    }
     1925   
     1926    /**
     1927     * Delete a ticket permanently from the database
     1928     */
     1929    public static function delete_ticket($ticket_id) {
     1930        global $wpdb;
     1931       
     1932        $ticket = self::get_ticket($ticket_id);
     1933        if (!$ticket) {
     1934            return false;
     1935        }
     1936       
     1937        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1938        $wpdb->query('START TRANSACTION');
     1939       
     1940        try {
     1941            $attachments = self::get_attachments($ticket_id);
     1942            if (!empty($attachments)) {
     1943                foreach ($attachments as $attachment) {
     1944                    $file_path = wp_upload_dir()['basedir'] . '/nexlifydesk/' . $attachment->file_name;
     1945                    if (file_exists($file_path)) {
     1946                        wp_delete_file($file_path);
     1947                    }
     1948                }
     1949            }
     1950           
     1951            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1952            $attachments_deleted = $wpdb->delete(
     1953                $wpdb->prefix . 'nexlifydesk_attachments',
     1954                array('ticket_id' => $ticket_id),
     1955                array('%d')
     1956            );
     1957           
     1958            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1959            $replies_deleted = $wpdb->delete(
     1960                $wpdb->prefix . 'nexlifydesk_replies',
     1961                array('ticket_id' => $ticket_id),
     1962                array('%d')
     1963            );
     1964           
     1965            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1966            $ticket_deleted = $wpdb->delete(
     1967                $wpdb->prefix . 'nexlifydesk_tickets',
     1968                array('id' => $ticket_id),
     1969                array('%d')
     1970            );
     1971           
     1972            if ($ticket_deleted === false) {
     1973                throw new Exception('Failed to delete ticket from database');
     1974            }
     1975           
     1976            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1977            $wpdb->query('COMMIT');
     1978           
     1979            wp_cache_delete('nexlifydesk_ticket_' . $ticket_id);
     1980            wp_cache_delete('nexlifydesk_ticket_replies_' . $ticket_id);
     1981            wp_cache_delete('nexlifydesk_attachments_' . $ticket_id);
     1982           
     1983            do_action('nexlifydesk_ticket_deleted', $ticket_id, $ticket);
     1984           
     1985            return true;
     1986           
     1987        } catch (Exception $e) {
     1988            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
     1989            $wpdb->query('ROLLBACK');
     1990            return false;
     1991        }
     1992    }
     1993
    11821994}
  • nexlifydesk/trunk/includes/nexlifydesk-functions.php

    r3326104 r3330741  
    33    exit;
    44}
     5
     6/**
     7 * NexlifyDesk Email Filtering & Spam Protection Features
     8 *
     9 * This file contains functions for filtering emails and preventing spam from creating tickets.
     10 *
     11 * Features include:
     12 * - Admin email blocking: Prevents emails from the admin email address (like notification emails)
     13 * - Notification subject filtering: Blocks emails with [Admin] or [Agent] in the subject
     14 * - Blocked email addresses: Custom list of email addresses to ignore
     15 * - Domain blocking: Block entire domains (useful for known spam domains)
     16 * - URL filtering: Blocks emails with too many links or suspicious URLs
     17 * - Keyword filtering: Blocks emails containing specified spam keywords
     18 *
     19 * All settings can be configured in WP Admin > Settings > NexlifyDesk > Email Piping
     20 */
    521
    622/**
     
    1733    return isset($statuses[$status]) ? $statuses[$status] : ucfirst($status);
    1834}
     35
     36/**
     37 * Check if an email should be blocked based on spam protection settings
     38 *
     39 * @param string $email_address The sender's email address
     40 * @param string $subject The email subject
     41 * @param string $message The email message content
     42 * @param array $settings The IMAP settings with spam protection options
     43 * @return bool True if email should be blocked, false otherwise
     44 */
     45function nexlifydesk_should_block_email($email_address, $subject, $message, $settings) {
     46    // Check if admin email blocking is enabled
     47    if (!empty($settings['block_admin_emails']) && $settings['block_admin_emails']) {
     48        $admin_email = get_option('admin_email');
     49        if (strtolower($email_address) === strtolower($admin_email)) {
     50            return true;
     51        }
     52    }
     53
     54    if (!empty($settings['block_notification_subjects']) && $settings['block_notification_subjects']) {
     55        if (preg_match('/\[(Admin|Agent)\]/i', $subject)) {
     56            return true;
     57        }
     58    }
     59
     60    if (!empty($settings['blocked_emails'])) {
     61        $blocked_emails = array_map('trim', array_filter(explode("\n", $settings['blocked_emails'])));
     62        foreach ($blocked_emails as $blocked_email) {
     63            if (strtolower($email_address) === strtolower($blocked_email)) {
     64                return true;
     65            }
     66        }
     67    }
     68
     69    if (!empty($settings['blocked_domains'])) {
     70        $blocked_domains = array_map('trim', array_filter(explode("\n", $settings['blocked_domains'])));
     71        $email_domain = substr(strrchr($email_address, '@'), 1);
     72        foreach ($blocked_domains as $blocked_domain) {
     73            if (strtolower($email_domain) === strtolower($blocked_domain)) {
     74                return true;
     75            }
     76        }
     77    }
     78
     79    if (!empty($settings['blocked_keywords'])) {
     80        $blocked_keywords = array_map('trim', array_filter(explode("\n", $settings['blocked_keywords'])));
     81        $full_content = $subject . ' ' . $message;
     82        foreach ($blocked_keywords as $keyword) {
     83            if (stripos($full_content, $keyword) !== false) {
     84                return true;
     85            }
     86        }
     87    }
     88
     89    if (!empty($settings['spam_url_filtering']) && $settings['spam_url_filtering']) {
     90        $max_links = isset($settings['max_links_per_email']) ? (int)$settings['max_links_per_email'] : 3;
     91       
     92        $url_pattern = '/https?:\/\/[^\s<>"{}|\\^`\[\]]+/i';
     93        preg_match_all($url_pattern, $message, $matches);
     94        $url_count = count($matches[0]);
     95       
     96        if ($url_count > $max_links) {
     97            return true;
     98        }
     99
     100        $suspicious_patterns = [
     101            '/bit\.ly|tinyurl|t\.co|goo\.gl|short\.link/i',
     102            '/\.(tk|ml|ga|cf)(\s|\/|$)/i',
     103            '/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/i',
     104        ];
     105       
     106        foreach ($suspicious_patterns as $pattern) {
     107            if (preg_match($pattern, $message)) {
     108                return true;
     109            }
     110        }
     111    }
     112
     113    if (!empty($settings['block_marketing_emails']) && $settings['block_marketing_emails']) {
     114        if (function_exists('nexlifydesk_is_marketing_email') &&
     115            nexlifydesk_is_marketing_email($email_address, $subject, $message, $settings)) {
     116            return true;
     117        }
     118    }
     119
     120    return false;
     121}
     122
     123/**
     124 * Get the ticket page URL from plugin settings
     125 * @return string The ticket page URL or empty string if not found
     126 */
     127function nexlifydesk_get_ticket_page_url() {
     128    $settings = get_option('nexlifydesk_settings', array());
     129    if (isset($settings['ticket_page_id']) && $settings['ticket_page_id']) {
     130        return get_permalink($settings['ticket_page_id']);
     131    }
     132    return '';
     133}
  • nexlifydesk/trunk/nexlifydesk.php

    r3326104 r3330741  
    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.0
     5 * Version: 1.0.1
    66 * Supported Versions: 6.2+
    77 * Tested up to: 6.2 < 6.8.
     
    1919define('NEXLIFYDESK_PLUGIN_DIR', plugin_dir_path(__FILE__));
    2020define('NEXLIFYDESK_PLUGIN_URL', plugin_dir_url(__FILE__));
    21 define('NEXLIFYDESK_VERSION', '1.0.0');
     21define('NEXLIFYDESK_VERSION', '1.0.1');
    2222define('NEXLIFYDESK_TABLE_PREFIX', 'nexlifydesk_');
    2323define('NEXLIFYDESK_CAP_VIEW_ALL_TICKETS', 'nexlifydesk_view_all_tickets');
     
    3535require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/class-nexlifydesk-reports.php';
    3636require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/class-support.php';
     37require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/class-nexlifydesk-rate-limiter.php';
     38require_once NEXLIFYDESK_PLUGIN_DIR . 'email-source/nexlifydesk-email-pipe.php';
     39require_once NEXLIFYDESK_PLUGIN_DIR . 'includes/helpers.php';
    3740
    3841// Initialize the plugin
     
    4043   
    4144    NexlifyDesk_Database::init();
     45   
     46    // Run database upgrades if needed
     47    NexlifyDesk_Database::upgrade_database();
     48   
    4249    NexlifyDesk_Database::check_and_run_migrations();
    4350    NexlifyDesk_Tickets::init();
     
    232239            'ticket_closed_text' => __('This ticket is closed. Please create a new ticket for further assistance.', 'nexlifydesk'),
    233240            'status_updated_text' => __('Status updated successfully!', 'nexlifydesk'),
    234             'ticket_page_url' => NexlifyDesk_Admin::get_ticket_page_url(),
     241            'ticket_page_url' => nexlifydesk_get_ticket_page_url(),
    235242            ));
    236243    }
     
    424431    wp_send_json_success(implode('<br>', $purged));
    425432});
     433
     434add_action('nexlifydesk_fetch_emails_event', 'nexlifydesk_fetch_emails');
     435
     436// Function to get the current fetch interval setting
     437function nexlifydesk_get_fetch_interval() {
     438    $settings = get_option('nexlifydesk_imap_settings', []);
     439    $interval = isset($settings['fetch_interval']) ? intval($settings['fetch_interval']) : 5;
     440   
     441    // Ensure valid interval
     442    if (!in_array($interval, [2, 5, 10])) {
     443        $interval = 5; // Default to 5 minutes
     444    }
     445   
     446    return $interval;
     447}
     448
     449// Function to reschedule email fetch with new interval
     450function nexlifydesk_reschedule_email_fetch() {
     451    $interval = nexlifydesk_get_fetch_interval();
     452    $schedule_name = "nexlifydesk_{$interval}_minutes";
     453   
     454    // Clear existing schedule
     455    wp_clear_scheduled_hook('nexlifydesk_fetch_emails_event');
     456   
     457    // Schedule with new interval
     458    if (!wp_next_scheduled('nexlifydesk_fetch_emails_event')) {
     459        wp_schedule_event(time(), $schedule_name, 'nexlifydesk_fetch_emails_event');
     460    }
     461}
     462
     463// Schedule email fetch on plugin activation
     464register_activation_hook(__FILE__, function() {
     465    nexlifydesk_reschedule_email_fetch();
     466});
     467
     468// Reschedule when settings are updated
     469add_action('update_option_nexlifydesk_imap_settings', function($old_value, $value, $option) {
     470    $old_interval = isset($old_value['fetch_interval']) ? intval($old_value['fetch_interval']) : 5;
     471    $new_interval = isset($value['fetch_interval']) ? intval($value['fetch_interval']) : 5;
     472   
     473    // If interval changed, reschedule
     474    if ($old_interval !== $new_interval) {
     475        nexlifydesk_reschedule_email_fetch();
     476    }
     477}, 10, 3);
     478
     479// Fallback: schedule on 'init' for multisite/cron edge cases
     480add_action('init', function() {
     481    if (!wp_next_scheduled('nexlifydesk_fetch_emails_event')) {
     482        nexlifydesk_reschedule_email_fetch();
     483    }
     484});
     485
     486// Add custom intervals
     487add_filter('cron_schedules', function($schedules) {
     488    $schedules['nexlifydesk_2_minutes'] = array(
     489        'interval' => 120, // 2 minutes
     490        'display'  => __('Every 2 Minutes (NexlifyDesk)', 'nexlifydesk')
     491    );
     492    $schedules['nexlifydesk_5_minutes'] = array(
     493        'interval' => 300, // 5 minutes
     494        'display'  => __('Every 5 Minutes (NexlifyDesk)', 'nexlifydesk')
     495    );
     496    $schedules['nexlifydesk_10_minutes'] = array(
     497        'interval' => 600, // 10 minutes
     498        'display'  => __('Every 10 Minutes (NexlifyDesk)', 'nexlifydesk')
     499    );
     500    return $schedules;
     501});
     502
     503// AJAX handler for instant email fetch
     504add_action('wp_ajax_nexlifydesk_fetch_emails_now', function() {
     505    check_ajax_referer('nexlifydesk_fetch_emails_now');
     506   
     507    // Use a more permissive permission check
     508    if (!current_user_can('nexlifydesk_manage_tickets') && !current_user_can('manage_options')) {
     509        wp_send_json_error(__('You do not have permission.', 'nexlifydesk'));
     510    }
     511   
     512    if (function_exists('nexlifydesk_fetch_emails')) {
     513        nexlifydesk_fetch_emails();
     514        wp_send_json_success();
     515    } else {
     516        wp_send_json_error(__('Fetch function not found.', 'nexlifydesk'));
     517    }
     518});
     519
     520/**
     521 * Gets tickets for the initial grid view.
     522 *
     523 * @param array $args
     524 * @return array
     525 */
     526function nexlifydesk_get_tickets_for_grid($args = []) {
     527    if (!class_exists('NexlifyDesk_Tickets')) {
     528        return [];
     529    }
     530    return NexlifyDesk_Tickets::get_tickets_for_grid($args);
     531}
     532
     533/**
     534 * Renders a single ticket card.
     535 *
     536 * @param object $ticket
     537 */
     538function nexlifydesk_render_ticket_card($ticket) {
     539    if (!is_object($ticket)) {
     540        return;
     541    }
     542    $assignee_name = !empty($ticket->assigned_to_display_name) ? $ticket->assigned_to_display_name : __('Unassigned', 'nexlifydesk');
     543    $assignee_initials = strtoupper(substr($assignee_name, 0, 2));
     544    $status_class = 'status-' . str_replace('_', '-', $ticket->status);
     545    $priority_class = 'priority-' . $ticket->priority;
     546    $ticket_url = admin_url('admin.php?page=nexlifydesk_tickets&ticket_id=' . $ticket->id);
     547    ?>
     548    <div class="ticket-card" onclick="window.location.href='<?php echo esc_url($ticket_url); ?>';" data-id="<?php echo esc_attr($ticket->id); ?>">
     549        <div class="ticket-header">
     550            <div class="ticket-id">#<?php echo esc_html($ticket->ticket_id); ?></div>
     551        </div>
     552        <div class="ticket-title"><?php echo esc_html($ticket->subject); ?></div>
     553        <div class="ticket-description"><?php echo esc_html(wp_trim_words($ticket->message, 15)); ?></div>
     554        <div class="ticket-meta">
     555            <span class="status-badge <?php echo esc_attr($status_class); ?>"><?php echo esc_html(ucwords(str_replace('_', ' ', $ticket->status))); ?></span>
     556            <span class="priority-badge <?php echo esc_attr($priority_class); ?>"><?php echo esc_html(ucfirst($ticket->priority)); ?></span>
     557        </div>
     558        <div class="ticket-footer">
     559            <div class="assignee">
     560                <div class="avatar"><?php echo esc_html($assignee_initials); ?></div>
     561                <span><?php echo esc_html($assignee_name); ?></span>
     562            </div>
     563            <div><?php echo esc_html(human_time_diff(strtotime($ticket->created_at))) . ' ' . esc_html__('ago', 'nexlifydesk'); ?></div>
     564        </div>
     565    </div>
     566    <?php
     567}
  • nexlifydesk/trunk/readme.txt

    r3326109 r3330741  
    44Requires at least: 6.2
    55Tested up to: 6.8
    6 Stable tag: 1.0.0
     6Stable tag: 1.0.1
    77License: GPLv2 or later
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1515NexlifyDesk is a comprehensive, enterprise-grade support ticketing system for WordPress that transforms your website into a professional customer service hub. Built with scalability and efficiency in mind, it provides everything you need to deliver exceptional support experiences while maintaining complete control over your customer data.
    1616
    17 **Documentation**: [Full Documentation & Setup Guide](https://nexlifylabs.com/nexlifylabs-plugin-documentation/nexlifydesk-documentation/)
     17**Documentation**: [Full Documentation & Setup Guide](https://nexlifylabs.com/nexlifydesk-documentation/getting-started/)
    1818
    1919== Key Features ==
     
    4444
    4545**Intelligent Automation**
    46 *   **Duplicate Detection**: Advanced algorithms detect similar tickets and merge conversations automatically
     46*   **Enhanced Duplicate Detection**: Advanced multi-layer algorithms detect similar tickets and merge conversations automatically with improved accuracy
    4747*   **Auto-Assignment**: Smart distribution of tickets to available agents based on workload
    4848*   **SLA Monitoring**: Automatic tracking and breach notifications to maintain service standards
    4949*   **Auto-Closure**: Resolved tickets automatically close after 48 hours of inactivity with system notifications
     50
     51**Email Piping & Integration**
     52*   **Multi-Provider Support**: Convert emails to tickets with support for Custom IMAP/POP3, AWS WorkMail, and Google Workspace/Gmail
     53*   **Flexible Email Management**: Option to keep or delete emails from inbox after ticket creation
     54*   **Advanced Spam Protection**: Built-in spam filtering, email blocking, and rate limiting to prevent abuse
     55*   **Intelligent Email Processing**: Automatic sender detection, thread management, and duplicate prevention
     56
     57**Enhanced Admin Experience**
     58*   **Real-time Ticket List**: Live-updating ticket interface with read/unread status indicators
     59*   **Smart Sorting**: Automatic prioritization with unread tickets displayed first
     60*   **Instant Notifications**: Real-time updates without page refresh
     61*   **Bulk Operations**: Enhanced bulk actions for efficient ticket management
    5062
    5163**Categories & Organization**
     
    117129    *   Link these pages in NexlifyDesk > Settings for proper navigation
    118130
    119 5.  **Agent Setup** (Optional): Configure your support team:
     1315.  **Email Piping Setup** (Optional): Configure email-to-ticket conversion:
     132    *   Choose your email provider (Custom IMAP/POP3, AWS WorkMail, or Google Workspace)
     133    *   Configure connection settings and authentication
     134    *   Set up spam protection and email filtering rules
     135    *   Choose whether to keep or delete emails from inbox after processing
     136
     1376.  **Agent Setup** (Optional): Configure your support team:
    120138    *   Create user accounts for support agents
    121139    *   Assign the "NexlifyDesk Agent" role
     
    179197- **Order-based duplicate detection**: Links tickets mentioning order numbers
    180198- **Customer context**: View order information when handling support requests
     199
     200= How do I set up email piping? =
     201
     202Email piping converts incoming emails into support tickets automatically. Setup varies by provider:
     203
     204**Custom IMAP/POP3:**
     2051. Go to NexlifyDesk > Settings > Email Piping
     2062. Select "Custom IMAP/POP3" as your provider
     2073. Enter your mail server details (host, port, username, password)
     2084. Configure spam protection and email filtering settings
     2095. Choose whether to delete emails from inbox after processing
     210
     211**AWS WorkMail:**
     2121. Ensure your site has SSL enabled (required for AWS)
     2132. Select "AWS WorkMail" as your provider 
     2143. Enter your AWS region, organization ID, and email credentials
     2154. Optionally configure AWS SES for enhanced email sending
     216
     217**Google Workspace/Gmail:**
     2181. Set up Google OAuth credentials in your Google Cloud Console
     2192. Select "Google Workspace" as your provider
     2203. Complete the OAuth authentication process
     2214. Configure email processing preferences
    181222
    182223= How does automatic ticket assignment work? =
     
    351392== Upgrade Notice ==
    352393
     394= 1.0.1 =
     395Major update with email piping support! Convert emails to tickets automatically with support for Custom IMAP/POP3, AWS WorkMail, and Google Workspace. Enhanced duplicate detection, real-time ticket list with read/unread status, and advanced spam protection. Recommended for all users.
     396
    353397= 1.0.0 =
    354398Initial release of NexlifyDesk. No upgrade steps necessary for new installations.
     399
     400== Changelog ==
     401
     402= 1.0.1 =
     403**Major Features:**
     404* Added Email Piping support with multiple providers (Custom IMAP/POP3, AWS WorkMail, Google Workspace/Gmail)
     405* Enhanced duplicate ticket detection with improved accuracy and performance
     406* New real-time ticket list interface with read/unread status indicators
     407* Advanced spam protection with rate limiting and email blocking capabilities
     408
     409**Email Piping Features:**
     410* Support for Custom IMAP/POP3 servers with SSL/TLS encryption
     411* AWS WorkMail integration with SSL requirement and SES support
     412* Google Workspace/Gmail integration with OAuth authentication
     413* Configurable option to keep or delete emails from inbox after processing
     414* Intelligent email thread detection and customer detail extraction
     415* Comprehensive spam filtering with customizable rules
     416
     417**Enhanced Admin Experience:**
     418* Real-time ticket list updates without page refresh
     419* Smart sorting with unread tickets prioritized first
     420* Improved bulk operations for efficient ticket management
     421* Enhanced ticket grid with performance optimizations
     422
     423**Security & Performance:**
     424* Built-in rate limiting to prevent spam and abuse
     425* Advanced email validation and sanitization
     426* Optimized database queries with proper caching
     427* Enhanced SSL detection for secure email processing
     428
     429**Bug Fixes:**
     430* Improved WordPress coding standards compliance
     431* Fixed database query optimizations
     432* Enhanced error handling and logging
     433* Better mobile responsive design
     434
     435= 1.0.0 =
     436* Initial release
     437* Core ticketing system with agent management
     438* Internal notes and professional communication tools
     439* WooCommerce integration
     440* SLA monitoring and reporting
     441* Duplicate detection system
     442* Email notification system
     443* Agent roles and permissions
     444* Categories and priority management
     445* File attachment support
     446* Shortcode system for frontend integration
    355447
    356448== Support & Documentation ==
  • nexlifydesk/trunk/templates/admin/settings.php

    r3326104 r3330741  
    66    $default_settings = array(
    77        'email_notifications' => 1,
     8        'admin_email_notifications' => 1,
    89        'default_priority' => 'medium',
    910        'auto_assign' => 0,
     
    6566            </tr>
    6667            <tr>
     68                <th><label for="admin_email_notifications"><?php esc_html_e('Admin Email Notifications', 'nexlifydesk'); ?></label></th>
     69                <td>
     70                    <input type="checkbox" name="admin_email_notifications" id="admin_email_notifications" value="1" <?php checked($settings['admin_email_notifications'], 1); ?>>
     71                    <p class="description"><?php esc_html_e('Send email notifications to admin. When disabled, only assigned agents will receive notifications (except for SLA breach notifications).', 'nexlifydesk'); ?></p>
     72                </td>
     73            </tr>
     74            <tr>
    6775                <th><label for="default_priority"><?php esc_html_e('Default Priority', 'nexlifydesk'); ?></label></th>
    6876                <td>
     
    238246        </table>
    239247
     248        <h2><?php esc_html_e('Spam Protection', 'nexlifydesk'); ?></h2>
     249        <table class="form-table">
     250            <tr>
     251                <th><label for=""><?php esc_html_e('Rate Limiting', 'nexlifydesk'); ?></label></th>
     252                <td>
     253                    <p class="description"><?php esc_html_e('Prevents spam by limiting ticket creation to 5 tickets per 30 minutes per user/email address.', 'nexlifydesk'); ?></p>
     254                    <p class="description"><?php esc_html_e('This applies to both web submissions and email piping. Admin and agent users are exempt from rate limiting.', 'nexlifydesk'); ?></p>
     255                </td>
     256            </tr>
     257            <tr>
     258                <th><label for=""><?php esc_html_e('Clear Rate Limits', 'nexlifydesk'); ?></label></th>
     259                <td>
     260                    <p class="description"><?php esc_html_e('Remove rate limiting restrictions for a specific user or email address.', 'nexlifydesk'); ?></p>
     261                    <div style="margin-top: 10px;">
     262                        <input type="email" id="nexlifydesk_clear_rate_limit_email" placeholder="<?php esc_attr_e('Enter email address', 'nexlifydesk'); ?>" style="width: 300px;">
     263                        <button type="button" class="button" id="nexlifydesk_clear_rate_limit"><?php esc_html_e('Clear Rate Limit', 'nexlifydesk'); ?></button>
     264                    </div>
     265                    <div id="nexlifydesk_clear_rate_limit_result" style="margin-top: 10px;"></div>
     266                </td>
     267            </tr>
     268            <tr>
     269                <th><label for=""><?php esc_html_e('Rate Limit Status', 'nexlifydesk'); ?></label></th>
     270                <td>
     271                    <p class="description"><?php esc_html_e('Check the current rate limit status for a specific user or email address.', 'nexlifydesk'); ?></p>
     272                    <div style="margin-top: 10px;">
     273                        <input type="email" id="nexlifydesk_check_rate_limit_email" placeholder="<?php esc_attr_e('Enter email address', 'nexlifydesk'); ?>" style="width: 300px;">
     274                        <button type="button" class="button" id="nexlifydesk_check_rate_limit"><?php esc_html_e('Check Status', 'nexlifydesk'); ?></button>
     275                    </div>
     276                    <div id="nexlifydesk_check_rate_limit_result" style="margin-top: 10px;"></div>
     277                </td>
     278            </tr>
     279        </table>
     280
    240281        <h2><?php esc_html_e('Data Management', 'nexlifydesk'); ?></h2>
    241282        <table class="form-table">
  • nexlifydesk/trunk/templates/admin/ticket-single.php

    r3326104 r3330741  
    99}
    1010
    11 // --- Preparing variables ---
    1211$user = get_userdata($ticket->user_id);
    1312$assigned_agent = $ticket->assigned_to ? get_userdata($ticket->assigned_to) : null;
     
    1514$initial_attachments = NexlifyDesk_Tickets::get_attachments($ticket->id);
    1615$user_avatar_url = $user ? get_avatar_url($user->ID) : get_avatar_url(0);
     16
     17$customer_details = function_exists('nexlifydesk_extract_customer_details') ?
     18    nexlifydesk_extract_customer_details($ticket->message) :
     19    ['name' => '', 'email' => '', 'message' => $ticket->message];
     20
     21$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
     22$customer_email = $user ? $user->user_email : ($customer_details['email'] ?: 'N/A');
     23$display_message = $customer_details['message'];
    1724?>
    1825
     
    6976                    <p><?php
    7077                        /* translators: 1: requester name, 2: date and time */
    71                         printf(esc_html__('Requested by %1$s on %2$s', 'nexlifydesk'), '<strong>' . esc_html($user ? $user->display_name : 'Guest') . '</strong>', esc_html(date_i18n(get_option('date_format') . ' \a\t ' . get_option('time_format'), strtotime($ticket->created_at))));
     78                        printf(esc_html__('Requested by %1$s on %2$s', 'nexlifydesk'), '<strong>' . esc_html($customer_name) . '</strong>', esc_html(date_i18n(get_option('date_format') . ' \a\t ' . get_option('time_format'), strtotime($ticket->created_at))));
    7279                    ?></p>
     80                    <?php if (!$user && $customer_email !== 'N/A') : ?>
     81                        <p class="customer-email"><?php echo esc_html($customer_email); ?></p>
     82                    <?php endif; ?>
    7383                </div>
    7484            </header>
     
    118128                    <div class="message-content">
    119129                        <div class="message-header">
    120                             <strong><?php echo esc_html($user ? $user->display_name : 'Guest'); ?></strong>
     130                            <strong><?php echo esc_html($customer_name); ?></strong>
    121131                            <span class="timestamp"><?php echo esc_html(date_i18n(get_option('date_format') . ' - ' . get_option('time_format'), strtotime($ticket->created_at))); ?></span>
    122132                        </div>
    123                         <p><?php echo nl2br(esc_html($ticket->message)); ?></p>
     133                        <p><?php echo nl2br(esc_html($display_message)); ?></p>
    124134                        <?php if (!empty($initial_attachments)) : ?>
    125135                            <div class="attachments">
     
    139149                    $is_internal_note = isset($reply->is_internal_note) && $reply->is_internal_note;
    140150                   
     151                    if (!$reply_user) {
     152                        $reply_customer_details = function_exists('nexlifydesk_extract_customer_details') ?
     153                            nexlifydesk_extract_customer_details($reply->message) :
     154                            ['name' => '', 'email' => '', 'message' => $reply->message];
     155                        $reply_customer_name = $reply_customer_details['name'] ?: 'Guest';
     156                        $reply_display_message = $reply_customer_details['message'];
     157                    } else {
     158                        $reply_customer_name = $reply_user->display_name;
     159                        $reply_display_message = $reply->message;
     160                    }
     161                   
    141162                    if ($is_internal_note) {
    142163                        $message_class = 'internal-note';
     
    149170                    <div class="message-content">
    150171                        <div class="message-header">
    151                             <strong><?php echo esc_html($reply_user ? $reply_user->display_name : 'User'); ?></strong>
     172                            <strong><?php echo esc_html($reply_customer_name); ?></strong>
    152173                            <?php if ($is_internal_note) : ?>
    153174                                <span class="internal-note-label"><?php esc_html_e('Internal Note', 'nexlifydesk'); ?></span>
     
    155176                            <span class="timestamp"><?php echo esc_html(date_i18n(get_option('date_format') . ' - ' . get_option('time_format'), strtotime($reply->created_at))); ?></span>
    156177                        </div>
    157                         <p><?php echo nl2br(esc_html($reply->message)); ?></p>
     178                        <p><?php echo nl2br(esc_html($reply_display_message)); ?></p>
    158179                        <?php if (!empty($reply->attachments)) : ?>
    159180                            <div class="attachments">
     
    174195            <div class="sidebar-block">
    175196                <h3><?php esc_html_e('About Customer', 'nexlifydesk'); ?></h3>
    176                 <p><strong><?php echo esc_html($user ? $user->display_name : 'Guest'); ?></strong></p>
    177                 <p><?php echo esc_html($user ? $user->user_email : 'N/A'); ?></p>
     197                <p><strong><?php echo esc_html($customer_name); ?></strong></p>
     198                <p><?php echo esc_html($customer_email); ?></p>
    178199                <?php if ($user): ?>
    179200                    <a href="<?php echo esc_url(get_edit_user_link($user->ID)); ?>" target="_blank"><?php esc_html_e('View Profile', 'nexlifydesk'); ?></a>
     201                <?php elseif ($customer_email !== 'N/A'): ?>
     202                    <p class="non-registered-notice"><?php esc_html_e('Non-registered customer', 'nexlifydesk'); ?></p>
    180203                <?php endif; ?>
    181204            </div>
     
    198221                <ul class="attachments-list">
    199222                    <?php
    200                     $all_attachments = array_merge($initial_attachments, ...array_column($replies, 'attachments'));
     223                    $all_attachments = array();
     224                   
     225                    if (!empty($initial_attachments) && is_array($initial_attachments)) {
     226                        $all_attachments = array_merge($all_attachments, $initial_attachments);
     227                    }
     228                   
     229                    if (!empty($replies) && is_array($replies)) {
     230                        foreach ($replies as $reply) {
     231                            if (isset($reply->attachments) && is_array($reply->attachments) && !empty($reply->attachments)) {
     232                                $all_attachments = array_merge($all_attachments, $reply->attachments);
     233                            }
     234                        }
     235                    }
     236                   
    201237                    if (!empty($all_attachments)) {
    202238                        foreach ($all_attachments as $attachment) {
    203                             echo '<li><a href="' . esc_url($attachment->file_path) . '" target="_blank" rel="noopener">' . esc_html($attachment->file_name) . '</a></li>';
     239                            if (isset($attachment->file_path) && isset($attachment->file_name)) {
     240                                echo '<li><a href="' . esc_url($attachment->file_path) . '" target="_blank" rel="noopener">' . esc_html($attachment->file_name) . '</a></li>';
     241                            }
    204242                        }
    205243                    } else {
  • nexlifydesk/trunk/templates/admin/tickets-list.php

    r3326104 r3330741  
    1919        <h1><?php esc_html_e('Support Tickets', 'nexlifydesk'); ?></h1>
    2020        <p><?php esc_html_e('Manage and track customer support requests', 'nexlifydesk'); ?></p>
     21        <button id="manual-refresh-btn" class="button" style="margin-left: 10px;"><?php esc_html_e('Refresh Now', 'nexlifydesk'); ?></button>
    2122    </div>
    2223
     
    4849
    4950    <div class="controls">
     51        <div class="bulk-actions">
     52            <input type="checkbox" id="select-all-tickets" class="select-all-checkbox">
     53            <select id="bulk-action-select" class="bulk-action-dropdown" disabled>
     54                <option value=""><?php esc_html_e('Bulk Actions', 'nexlifydesk'); ?></option>
     55                <option value="assign"><?php esc_html_e('Assign to Agent', 'nexlifydesk'); ?></option>
     56                <option value="status"><?php esc_html_e('Change Status', 'nexlifydesk'); ?></option>
     57                <option value="priority"><?php esc_html_e('Change Priority', 'nexlifydesk'); ?></option>
     58                <?php if (current_user_can('manage_options')): ?>
     59                <option value="delete" class="delete-option" style="color: #d63638;"><?php esc_html_e('Delete Tickets (Admin Only)', 'nexlifydesk'); ?></option>
     60                <?php endif; ?>
     61            </select>
     62            <button id="apply-bulk-action" class="button button-primary" disabled><?php esc_html_e('Apply', 'nexlifydesk'); ?></button>
     63        </div>
     64       
    5065        <div class="search-bar">
    5166            <span class="search-icon dashicons dashicons-search"></span>
     
    6782            <option value="low"><?php esc_html_e('Low', 'nexlifydesk'); ?></option>
    6883        </select>
    69        
    70         <button id="reassign-orphaned-tickets-btn" class="button button-secondary">
    71             <?php esc_html_e('Reassign Orphaned Tickets', 'nexlifydesk'); ?>
    72         </button>
    73     </div>
    74 
    75     <div id="nexlifydesk-ticket-grid" class="ticket-grid">
    76         <div class="loading-tickets">
    77             <span class="spinner is-active"></span>
    78             <?php esc_html_e('Loading tickets...', 'nexlifydesk'); ?>
    79         </div>
    80     </div>
     84    </div>
     85
     86    <div id="nexlifydesk-ticket-list" class="ticket-list">
     87        <div class="ticket-list-header">
     88            <div class="header-checkbox">
     89                <input type="checkbox" id="header-select-all" class="select-all-checkbox">
     90            </div>
     91            <div class="header-subject"><?php esc_html_e('Subject', 'nexlifydesk'); ?></div>
     92            <div class="header-customer"><?php esc_html_e('Customer', 'nexlifydesk'); ?></div>
     93            <div class="header-status"><?php esc_html_e('Status', 'nexlifydesk'); ?></div>
     94            <div class="header-priority"><?php esc_html_e('Priority', 'nexlifydesk'); ?></div>
     95            <div class="header-assignee"><?php esc_html_e('Assignee', 'nexlifydesk'); ?></div>
     96            <div class="header-date"><?php esc_html_e('Date', 'nexlifydesk'); ?></div>
     97        </div>
     98       
     99        <div class="ticket-list-body">
     100            <?php
     101            if (class_exists('NexlifyDesk_Tickets')) {
     102                $initial_tickets = NexlifyDesk_Tickets::get_tickets_for_grid();
     103                if (!empty($initial_tickets)) {
     104                    foreach ($initial_tickets as $ticket) {
     105                        $user = get_userdata($ticket->user_id);
     106                        $assigned_agent = $ticket->assigned_to ? get_userdata($ticket->assigned_to) : null;
     107                       
     108                        if (!$user) {
     109                            $customer_details = function_exists('nexlifydesk_extract_customer_details') ?
     110                                nexlifydesk_extract_customer_details($ticket->message) :
     111                                ['name' => '', 'email' => '', 'message' => $ticket->message];
     112                            $customer_name = $customer_details['name'] ?: 'Guest';
     113                            $customer_email = $customer_details['email'] ?: 'N/A';
     114                        } else {
     115                            $customer_name = $user->display_name;
     116                            $customer_email = $user->user_email;
     117                        }
     118                        ?>
     119                        <?php
     120                        $is_unread = isset($ticket->is_unread) ? $ticket->is_unread : false;
     121                        $last_reply_time = isset($ticket->last_reply_time) ? $ticket->last_reply_time : $ticket->created_at;
     122                        ?>
     123                        <div class="ticket-row <?php echo $is_unread ? 'unread' : ''; ?>"
     124                            data-ticket-id="<?php echo esc_attr($ticket->id); ?>"
     125                            data-last-reply="<?php echo esc_attr(strtotime($last_reply_time)); ?>"
     126                            data-is-unread="<?php echo $is_unread ? '1' : '0'; ?>"
     127                            data-status="<?php echo esc_attr($ticket->status); ?>"
     128                            data-priority="<?php echo esc_attr($ticket->priority); ?>">
     129                            <div class="row-checkbox">
     130                                <input type="checkbox" class="ticket-checkbox" value="<?php echo esc_attr($ticket->id); ?>">
     131                            </div>
     132                            <div class="row-subject">
     133                                <a href="<?php echo esc_url(admin_url('admin.php?page=nexlifydesk_tickets&ticket_id=' . $ticket->id)); ?>" class="ticket-link">
     134                                    <?php if ($is_unread): ?><span class="unread-dot"></span><?php endif; ?>
     135                                    <span class="ticket-id">#<?php echo esc_html($ticket->ticket_id); ?></span>
     136                                    <span class="ticket-title <?php echo $is_unread ? 'unread-title' : ''; ?>"><?php echo esc_html($ticket->subject); ?></span>
     137                                </a>
     138                                <div class="ticket-preview"><?php echo esc_html(wp_trim_words($ticket->message, 15)); ?></div>
     139                            </div>
     140                            <div class="row-customer">
     141                                <span class="customer-name"><?php echo esc_html($customer_name); ?></span>
     142                                <span class="customer-email"><?php echo esc_html($customer_email); ?></span>
     143                            </div>
     144                            <div class="row-status">
     145                                <span class="status-badge status-<?php echo esc_attr($ticket->status); ?>">
     146                                    <?php echo esc_html(ucfirst(str_replace('_', ' ', $ticket->status))); ?>
     147                                </span>
     148                            </div>
     149                            <div class="row-priority">
     150                                <span class="priority-badge priority-<?php echo esc_attr($ticket->priority); ?>">
     151                                    <?php echo esc_html(ucfirst($ticket->priority)); ?>
     152                                </span>
     153                            </div>
     154                            <div class="row-assignee">
     155                                <?php if ($assigned_agent): ?>
     156                                    <div class="assignee-info">
     157                                        <div class="avatar"><?php echo esc_html(substr($assigned_agent->display_name, 0, 1)); ?></div>
     158                                        <span><?php echo esc_html($assigned_agent->display_name); ?></span>
     159                                    </div>
     160                                <?php else: ?>
     161                                    <span class="unassigned"><?php esc_html_e('Unassigned', 'nexlifydesk'); ?></span>
     162                                <?php endif; ?>
     163                            </div>
     164                            <div class="row-date">
     165                                <span class="date-time"><?php echo esc_html(date_i18n('M j, Y', strtotime($last_reply_time))); ?></span>
     166                                <span class="time-ago"><?php echo esc_html(human_time_diff(strtotime($last_reply_time), current_time('timestamp')) . ' ago'); ?></span>
     167                            </div>
     168                        </div>
     169                        <?php
     170                    }
     171                } else {
     172                    echo '<div class="no-tickets">' . esc_html__('No tickets found.', 'nexlifydesk') . '</div>';
     173                }
     174            } else {
     175                echo '<div class="no-tickets">' . esc_html__('Tickets system not available.', 'nexlifydesk') . '</div>';
     176            }
     177            ?>
     178        </div>
     179    </div>
     180
     181    <input type="hidden" id="nexlifydesk-nonce" value="<?php echo esc_attr(wp_create_nonce('nexlifydesk-ajax-nonce')); ?>">
     182
     183    <div id="bulk-assign-modal" class="bulk-modal" style="display: none;">
     184        <div class="modal-content">
     185            <h3><?php esc_html_e('Assign Tickets to Agent', 'nexlifydesk'); ?></h3>
     186            <select id="bulk-assign-agent">
     187                <option value=""><?php esc_html_e('Select Agent', 'nexlifydesk'); ?></option>
     188                <?php
     189                $agents = get_users(array('capability' => 'nexlifydesk_manage_tickets'));
     190                foreach ($agents as $agent) {
     191                    echo '<option value="' . esc_attr($agent->ID) . '">' . esc_html($agent->display_name) . '</option>';
     192                }
     193                ?>
     194            </select>
     195            <div class="modal-actions">
     196                <button id="cancel-bulk-assign" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button>
     197                <button id="confirm-bulk-assign" class="button button-primary"><?php esc_html_e('Assign', 'nexlifydesk'); ?></button>
     198            </div>
     199        </div>
     200    </div>
     201
     202    <div id="bulk-status-modal" class="bulk-modal" style="display: none;">
     203        <div class="modal-content">
     204            <h3><?php esc_html_e('Change Ticket Status', 'nexlifydesk'); ?></h3>
     205            <select id="bulk-status-select">
     206                <option value="open"><?php esc_html_e('Open', 'nexlifydesk'); ?></option>
     207                <option value="in_progress"><?php esc_html_e('In Progress', 'nexlifydesk'); ?></option>
     208                <option value="resolved"><?php esc_html_e('Resolved', 'nexlifydesk'); ?></option>
     209                <option value="closed"><?php esc_html_e('Closed', 'nexlifydesk'); ?></option>
     210            </select>
     211            <div class="modal-actions">
     212                <button id="cancel-bulk-status" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button>
     213                <button id="confirm-bulk-status" class="button button-primary"><?php esc_html_e('Update', 'nexlifydesk'); ?></button>
     214            </div>
     215        </div>
     216    </div>
     217
     218    <div id="bulk-priority-modal" class="bulk-modal" style="display: none;">
     219        <div class="modal-content">
     220            <h3><?php esc_html_e('Change Ticket Priority', 'nexlifydesk'); ?></h3>
     221            <select id="bulk-priority-select">
     222                <option value="low"><?php esc_html_e('Low', 'nexlifydesk'); ?></option>
     223                <option value="medium"><?php esc_html_e('Medium', 'nexlifydesk'); ?></option>
     224                <option value="high"><?php esc_html_e('High', 'nexlifydesk'); ?></option>
     225            </select>
     226            <div class="modal-actions">
     227                <button id="cancel-bulk-priority" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button>
     228                <button id="confirm-bulk-priority" class="button button-primary"><?php esc_html_e('Update', 'nexlifydesk'); ?></button>
     229            </div>
     230        </div>
     231    </div>
     232
     233    <?php if (current_user_can('manage_options')): ?>
     234    <div id="bulk-delete-modal" class="bulk-modal" style="display: none;">
     235        <div class="modal-content">
     236            <h3 style="color: #d63638;"><?php esc_html_e('Delete Tickets', 'nexlifydesk'); ?></h3>
     237            <p><?php esc_html_e('Are you sure you want to permanently delete the selected tickets?', 'nexlifydesk'); ?></p>
     238            <div class="selected-tickets-info">
     239                <?php esc_html_e('This will delete', 'nexlifydesk'); ?> <strong><span id="selected-tickets-count">0</span></strong> <?php esc_html_e('tickets permanently.', 'nexlifydesk'); ?>
     240            </div>
     241            <p style="color: #d63638; font-weight: 600;"><?php esc_html_e('This action cannot be undone!', 'nexlifydesk'); ?></p>
     242            <div class="modal-actions">
     243                <button id="cancel-bulk-delete" class="button"><?php esc_html_e('Cancel', 'nexlifydesk'); ?></button>
     244                <button id="confirm-bulk-delete" class="button button-primary" style="background-color: #d63638; border-color: #d63638;"><?php esc_html_e('Delete Permanently', 'nexlifydesk'); ?></button>
     245            </div>
     246        </div>
     247    </div>
     248    <?php endif; ?>
    81249</div>
  • nexlifydesk/trunk/templates/frontend/partials/single-reply.php

    r3326104 r3330741  
    1111}
    1212
     13$reply_message = isset($reply->message) ? $reply->message : '';
     14$reply_customer_details = nexlifydesk_extract_customer_details($reply_message);
     15$reply_customer_name = $reply_user ? $reply_user->display_name : ($reply_customer_details['name'] ?: __('Unknown', 'nexlifydesk'));
     16$clean_reply_message = $reply_customer_details['message'] ?: $reply_message;
     17
    1318if (!$reply_user || !is_object($reply_user) || !isset($reply_user->roles)) {
    1419    $reply_user = (object) array(
    15         'display_name' => __('Unknown', 'nexlifydesk'),
     20        'display_name' => $reply_customer_name,
    1621        'ID' => 0,
    1722        'roles' => array()
     
    3136    <div class="message-content">
    3237        <div class="message-header">
    33             <strong><?php echo esc_html($reply_user ? $reply_user->display_name : __('Unknown', 'nexlifydesk')); ?></strong>
     38            <strong><?php echo esc_html($reply_customer_name); ?></strong>
    3439            <span class="timestamp"><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($reply->created_at))); ?></span>
    3540        </div>
    3641        <div class="message-body">
    37             <?php echo nl2br(esc_html($reply->message)); ?>
     42            <?php echo nl2br(esc_html($clean_reply_message)); ?>
    3843           
    3944            <?php if (!empty($reply->attachments)) : ?>
     
    4146                    <?php foreach ($reply->attachments as $attachment) : ?>
    4247                        <div class="attachment">
    43                             <a href="<?php echo esc_url($attachment->file_url); ?>" target="_blank">
     48                            <a href="<?php echo esc_url($attachment->file_path); ?>" target="_blank">
    4449                                <span class="attachment-icon">📎</span>
    4550                                <?php echo esc_html($attachment->file_name); ?>
  • nexlifydesk/trunk/templates/frontend/ticket-form.php

    r3326104 r3330741  
    103103                        <p class="file-info">
    104104                            <?php
     105                            // Get server limits for display
     106                            $server_max_size = min(
     107                                wp_max_upload_size(),
     108                                wp_convert_hr_to_bytes(ini_get('post_max_size'))
     109                            );
     110                            $server_max_mb = round($server_max_size / 1024 / 1024, 1);
     111                            $plugin_max_mb = $max_file_size;
     112                           
     113                            // Use the smaller of plugin setting or server limit
     114                            $effective_max_mb = min($plugin_max_mb, $server_max_mb);
     115                           
    105116                            printf(
    106117                                /* translators: 1: Maximum file size in MB, 2: Allowed file types (comma separated) */
    107                                 esc_html__('Max size: %1$sMB. Allowed types: %2$s', 'nexlifydesk'),
    108                                 esc_html($max_file_size),
     118                                esc_html__('Max size: %1$sMB per file. Allowed types: %2$s', 'nexlifydesk'),
     119                                esc_html($effective_max_mb),
    109120                                esc_html($allowed_types)
    110                             ); ?>
     121                            );
     122                           
     123                            // Show server limit warning if it's lower than plugin setting
     124                            if ($server_max_mb < $plugin_max_mb) {
     125                                echo '<br><small style="color: #d63638;">';
     126                                printf(
     127                                    /* translators: %s: Server maximum file size in MB */
     128                                    esc_html__('Server limit: %sMB', 'nexlifydesk'),
     129                                    esc_html($server_max_mb)
     130                                );
     131                                echo '</small>';
     132                            }
     133                            ?>
    111134                        </p>
    112135                    </div>
  • nexlifydesk/trunk/templates/frontend/ticket-single.php

    r3326104 r3330741  
    6666$replies = NexlifyDesk_Tickets::get_replies($ticket->id);
    6767$initial_attachments = NexlifyDesk_Tickets::get_attachments($ticket->id);
     68
     69// Extract customer details from ticket content for non-registered users
     70$customer_details = nexlifydesk_extract_customer_details($ticket->message);
     71$customer_name = $user ? $user->display_name : ($customer_details['name'] ?: 'Guest');
     72$customer_email = $user ? $user->user_email : ($customer_details['email'] ?: 'N/A');
     73$clean_initial_message = $customer_details['message'] ?: $ticket->message;
    6874$user_initials = $user ? strtoupper(substr($user->first_name, 0, 1) . substr($user->last_name, 0, 1)) : 'G';
    6975if (empty(trim($user_initials))) {
    70     $user_initials = strtoupper(substr($user->display_name, 0, 2));
     76    $user_initials = $user ? strtoupper(substr($user->display_name, 0, 2)) : strtoupper(substr($customer_name, 0, 2));
    7177}
    7278
     
    95101                </div>
    96102            </div>
    97             <div class="user-avatar" title="<?php echo esc_attr($user ? $user->display_name : 'Guest'); ?>"><?php echo esc_html($user_initials); ?></div>
     103            <div class="user-avatar" title="<?php echo esc_attr($customer_name); ?>"><?php echo esc_html($user_initials); ?></div>
    98104        </div>
    99105    </div>
     
    103109            <div class="section-title"><?php esc_html_e('Description', 'nexlifydesk'); ?></div>
    104110            <div class="description">
    105                 <?php echo nl2br(esc_html($ticket->message)); ?>
     111                <?php echo nl2br(esc_html($clean_initial_message)); ?>
    106112            </div>
    107113
Note: See TracChangeset for help on using the changeset viewer.