mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 19:29:34 +01:00
[AI][Feat]: Display Cryptographic keys in extra section
Thanks @aurorasmiles for that wonderful idea <3
This commit is contained in:
parent
72c22f9815
commit
fd993eb272
6 changed files with 643 additions and 2 deletions
411
application/crypto-detector.js
Normal file
411
application/crypto-detector.js
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
const debug = require('debug')('48hr-email:crypto-detector')
|
||||
|
||||
/**
|
||||
* Detects cryptographic keys and signatures in email attachments
|
||||
*/
|
||||
class CryptoDetector {
|
||||
constructor() {
|
||||
// Common cryptographic file extensions
|
||||
this.cryptoExtensions = [
|
||||
'.pgp', '.gpg', '.asc', '.pub', '.key', '.pem',
|
||||
'.crt', '.cer', '.sig', '.sign', '.p7s', '.p7m',
|
||||
'.pkcs7', '.pkcs12', '.pfx', '.p12'
|
||||
]
|
||||
|
||||
// Patterns to detect key blocks in content
|
||||
this.keyPatterns = [
|
||||
// PGP/GPG keys
|
||||
/-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*?-----END PGP PUBLIC KEY BLOCK-----/g,
|
||||
/-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----/g,
|
||||
/-----BEGIN PGP MESSAGE-----[\s\S]*?-----END PGP MESSAGE-----/g,
|
||||
/-----BEGIN PGP SIGNATURE-----[\s\S]*?-----END PGP SIGNATURE-----/g,
|
||||
|
||||
// SSH keys
|
||||
/-----BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----[\s\S]*?-----END (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----/g,
|
||||
/ssh-(rsa|dss|ed25519|ecdsa) [A-Za-z0-9+/=]+/g,
|
||||
|
||||
// SSL/TLS certificates and keys
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g,
|
||||
/-----BEGIN (RSA|EC) PRIVATE KEY-----[\s\S]*?-----END (RSA|EC) PRIVATE KEY-----/g,
|
||||
/-----BEGIN ENCRYPTED PRIVATE KEY-----[\s\S]*?-----END ENCRYPTED PRIVATE KEY-----/g,
|
||||
/-----BEGIN PUBLIC KEY-----[\s\S]*?-----END PUBLIC KEY-----/g,
|
||||
|
||||
// PKCS7/CMS signatures
|
||||
/-----BEGIN PKCS7-----[\s\S]*?-----END PKCS7-----/g,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a filename suggests a cryptographic file
|
||||
* @param {string} filename
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isCryptoFilename(filename) {
|
||||
if (!filename) return false
|
||||
const lowerName = filename.toLowerCase()
|
||||
return this.cryptoExtensions.some(ext => lowerName.endsWith(ext)) ||
|
||||
lowerName.includes('signature') ||
|
||||
lowerName.includes('publickey') ||
|
||||
lowerName.includes('privatekey') ||
|
||||
lowerName.includes('certificate')
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the type of cryptographic content
|
||||
* @param {string} content
|
||||
* @param {string} filename
|
||||
* @returns {string|null} The detected key type or null
|
||||
*/
|
||||
detectKeyType(content, filename) {
|
||||
if (!content) return null
|
||||
|
||||
const contentStr = content.toString('utf8', 0, Math.min(content.length, 10000)) // Check first 10KB
|
||||
|
||||
if (contentStr.includes('BEGIN PGP PUBLIC KEY')) return 'PGP Public Key'
|
||||
if (contentStr.includes('BEGIN PGP PRIVATE KEY')) return 'PGP Private Key'
|
||||
if (contentStr.includes('BEGIN PGP MESSAGE')) return 'PGP Encrypted Message'
|
||||
if (contentStr.includes('BEGIN PGP SIGNATURE')) return 'PGP Signature'
|
||||
if (contentStr.match(/ssh-(rsa|dss|ed25519|ecdsa)/)) return 'SSH Public Key'
|
||||
if (contentStr.includes('BEGIN RSA PRIVATE KEY')) return 'RSA Private Key'
|
||||
if (contentStr.includes('BEGIN EC PRIVATE KEY')) return 'EC Private Key'
|
||||
if (contentStr.includes('BEGIN OPENSSH PRIVATE KEY')) return 'OpenSSH Private Key'
|
||||
if (contentStr.includes('BEGIN CERTIFICATE')) return 'X.509 Certificate'
|
||||
if (contentStr.includes('BEGIN PUBLIC KEY')) return 'Public Key'
|
||||
if (contentStr.includes('BEGIN ENCRYPTED PRIVATE KEY')) return 'Encrypted Private Key'
|
||||
if (contentStr.includes('BEGIN PKCS7')) return 'PKCS#7 Signature'
|
||||
|
||||
// Check by filename if content detection fails
|
||||
if (filename) {
|
||||
const lower = filename.toLowerCase()
|
||||
if (lower.endsWith('.pub')) return 'Public Key'
|
||||
if (lower.endsWith('.sig') || lower.endsWith('.sign')) return 'Detached Signature'
|
||||
if (lower.endsWith('.asc')) return 'ASCII Armored Key/Signature'
|
||||
if (lower.endsWith('.pgp') || lower.endsWith('.gpg')) return 'PGP Key/Message'
|
||||
if (lower.endsWith('.pem')) return 'PEM Encoded Key/Certificate'
|
||||
if (lower.endsWith('.crt') || lower.endsWith('.cer')) return 'Certificate'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts cryptographic keys from content
|
||||
* @param {string|Buffer} content
|
||||
* @returns {Array<{type: string, content: string}>}
|
||||
*/
|
||||
extractKeys(content) {
|
||||
if (!content) return []
|
||||
|
||||
const contentStr = content.toString('utf8')
|
||||
const keys = []
|
||||
|
||||
this.keyPatterns.forEach(pattern => {
|
||||
const matches = contentStr.match(pattern)
|
||||
if (matches) {
|
||||
matches.forEach(match => {
|
||||
// Determine key type from the match
|
||||
let type = 'Cryptographic Key'
|
||||
if (match.includes('PGP PUBLIC KEY')) type = 'PGP Public Key'
|
||||
else if (match.includes('PGP PRIVATE KEY')) type = 'PGP Private Key'
|
||||
else if (match.includes('PGP MESSAGE')) type = 'PGP Message'
|
||||
else if (match.includes('PGP SIGNATURE')) type = 'PGP Signature'
|
||||
else if (match.includes('ssh-')) type = 'SSH Public Key'
|
||||
else if (match.includes('CERTIFICATE')) type = 'Certificate'
|
||||
else if (match.includes('PUBLIC KEY')) type = 'Public Key'
|
||||
else if (match.includes('PRIVATE KEY')) type = 'Private Key'
|
||||
|
||||
keys.push({
|
||||
type,
|
||||
content: match
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes email attachments to detect and extract cryptographic content
|
||||
* @param {Array} attachments - Array of email attachments
|
||||
* @returns {Array<{filename: string, type: string, content: string, preview: string}>}
|
||||
*/
|
||||
detectCryptoAttachments(attachments) {
|
||||
if (!attachments || !Array.isArray(attachments)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const cryptoFiles = []
|
||||
|
||||
attachments.forEach(attachment => {
|
||||
// Check if it's a potential crypto file
|
||||
if (this.isCryptoFilename(attachment.filename)) {
|
||||
const keyType = this.detectKeyType(attachment.content, attachment.filename)
|
||||
|
||||
if (keyType) {
|
||||
// Extract actual keys from content
|
||||
const extractedKeys = this.extractKeys(attachment.content)
|
||||
|
||||
if (extractedKeys.length > 0) {
|
||||
extractedKeys.forEach(key => {
|
||||
cryptoFiles.push({
|
||||
filename: attachment.filename,
|
||||
type: key.type,
|
||||
content: key.content,
|
||||
preview: this._generatePreview(key.content, key.type),
|
||||
info: this._extractKeyInfo(key.content, key.type)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// File has crypto extension/name but no extractable key blocks
|
||||
// Still show it as it might be binary encoded
|
||||
const contentStr = attachment.content.toString('utf8', 0, Math.min(attachment.content.length, 500))
|
||||
cryptoFiles.push({
|
||||
filename: attachment.filename,
|
||||
type: keyType,
|
||||
content: contentStr + (attachment.content.length > 500 ? '\n...[truncated]' : ''),
|
||||
preview: this._generatePreview(contentStr, keyType),
|
||||
info: this._extractKeyInfo(contentStr, keyType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
debug(`Detected ${cryptoFiles.length} cryptographic files in attachments`)
|
||||
return cryptoFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract specific information from the key content
|
||||
* @param {string} content
|
||||
* @param {string} type
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_extractKeyInfo(content, type) {
|
||||
if (!content) return ''
|
||||
|
||||
// For SSH keys, extract the key comment/user
|
||||
if (type.includes('SSH')) {
|
||||
const sshMatch = content.match(/ssh-\S+\s+\S+\s+(.+?)[\r\n]/)
|
||||
if (sshMatch && sshMatch[1] && sshMatch[1].trim()) {
|
||||
return sshMatch[1].trim()
|
||||
}
|
||||
// Show algorithm if available
|
||||
const algoMatch = content.match(/ssh-(rsa|dss|ed25519|ecdsa-sha2-nistp(\d+))/)
|
||||
if (algoMatch) {
|
||||
return `${algoMatch[1].toUpperCase()}`
|
||||
}
|
||||
}
|
||||
|
||||
// For PGP keys and signatures, extract user info
|
||||
if (type.includes('PGP')) {
|
||||
// For signatures, try to extract key ID from the signature packet
|
||||
if (type.includes('Signature')) {
|
||||
try {
|
||||
// Extract base64 content
|
||||
const lines = content.split('\n')
|
||||
let base64Content = ''
|
||||
let inSig = false
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('BEGIN PGP')) {
|
||||
inSig = true
|
||||
continue
|
||||
}
|
||||
if (line.includes('END PGP')) {
|
||||
break
|
||||
}
|
||||
if (inSig && line.trim() && !line.startsWith('=')) {
|
||||
base64Content += line.trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (base64Content) {
|
||||
const decoded = Buffer.from(base64Content, 'base64')
|
||||
|
||||
// Try to find key ID in signature packet
|
||||
// OpenPGP signature packets typically have key ID at specific offsets
|
||||
// Look for 8-byte key ID patterns
|
||||
for (let i = 0; i < decoded.length - 8; i++) {
|
||||
// Check if this looks like a key ID section
|
||||
// Key IDs are often preceded by specific packet headers
|
||||
if (decoded[i] === 0x00 && i + 8 < decoded.length) {
|
||||
const keyIdBytes = decoded.slice(i + 1, i + 9)
|
||||
const keyId = keyIdBytes.toString('hex').toUpperCase()
|
||||
|
||||
// Validate it looks like a reasonable key ID (not all zeros, not all FFs)
|
||||
if (keyId.match(/^[0-9A-F]{16}$/) &&
|
||||
keyId !== '0000000000000000' &&
|
||||
keyId !== 'FFFFFFFFFFFFFFFF') {
|
||||
return `Key ID: ${keyId.slice(-16)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alternative: look for the issuer key ID in a more reliable way
|
||||
// The key ID is usually in the last 8 bytes before certain markers
|
||||
if (decoded.length > 20) {
|
||||
// Try to extract from common positions
|
||||
const possibleKeyId = decoded.slice(decoded.length - 20, decoded.length - 12).toString('hex').toUpperCase()
|
||||
if (possibleKeyId.match(/^[0-9A-F]{16}$/)) {
|
||||
return `Key ID: ${possibleKeyId}`
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
debug(`Error extracting signature key ID: ${err.message}`)
|
||||
}
|
||||
return 'PGP detached signature'
|
||||
}
|
||||
|
||||
// For keys, extract user info
|
||||
try {
|
||||
// Extract base64 content between BEGIN and END lines
|
||||
const lines = content.split('\n')
|
||||
let base64Content = ''
|
||||
let inKey = false
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('BEGIN PGP')) {
|
||||
inKey = true
|
||||
continue
|
||||
}
|
||||
if (line.includes('END PGP')) {
|
||||
break
|
||||
}
|
||||
if (inKey && line.trim() && !line.startsWith('=')) {
|
||||
base64Content += line.trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (base64Content) {
|
||||
// Decode base64 to binary buffer
|
||||
const decoded = Buffer.from(base64Content, 'base64')
|
||||
|
||||
// Extract printable ASCII strings from the buffer
|
||||
let printableStr = ''
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
const byte = decoded[i]
|
||||
// Keep printable ASCII characters
|
||||
if (byte >= 0x20 && byte <= 0x7E) {
|
||||
printableStr += String.fromCharCode(byte)
|
||||
} else {
|
||||
// Add separator for non-printable bytes
|
||||
if (printableStr.length > 0 && !printableStr.endsWith('|')) {
|
||||
printableStr += '|'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Extracted printable from PGP: ${printableStr.substring(0, 200)}`)
|
||||
|
||||
// Look for email with optional name before it
|
||||
const emailPattern = /([A-Za-z][A-Za-z\s]{0,40}?)<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/
|
||||
const emailMatch = printableStr.match(emailPattern)
|
||||
if (emailMatch) {
|
||||
const name = emailMatch[1].replace(/\|/g, '').trim()
|
||||
const email = emailMatch[2]
|
||||
debug(`Found PGP user: ${name} <${email}>`)
|
||||
if (name.length > 0) {
|
||||
return `${name} <${email}>`
|
||||
}
|
||||
return email
|
||||
}
|
||||
|
||||
// Just look for bare email
|
||||
const bareEmailMatch = printableStr.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/)
|
||||
if (bareEmailMatch) {
|
||||
debug(`Found PGP email: ${bareEmailMatch[1]}`)
|
||||
return bareEmailMatch[1]
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
debug(`Error extracting PGP info: ${err.message}`)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// For detached signatures, show signature type
|
||||
if (type.includes('Signature')) {
|
||||
if (type.includes('PKCS')) {
|
||||
return 'PKCS#7/CMS signature'
|
||||
}
|
||||
return 'Detached signature'
|
||||
}
|
||||
|
||||
// For certificates, extract subject Common Name or issuer
|
||||
if (type.includes('Certificate')) {
|
||||
const cnPatterns = [
|
||||
/CN\s*=\s*([^,\n/]+)/,
|
||||
/commonName\s*=\s*([^,\n/]+)/i,
|
||||
/Subject:.*?CN\s*=\s*([^,\n/]+)/
|
||||
]
|
||||
for (const pattern of cnPatterns) {
|
||||
const match = content.match(pattern)
|
||||
if (match && match[1]) {
|
||||
return match[1].trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For private keys, check encryption
|
||||
if (type.includes('Private')) {
|
||||
if (content.includes('ENCRYPTED') || content.includes('Proc-Type: 4,ENCRYPTED')) {
|
||||
return 'Encrypted'
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a preview/fingerprint for the key
|
||||
* @param {string} content
|
||||
* @param {string} type
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_generatePreview(content, type) {
|
||||
if (!content) return ''
|
||||
|
||||
// For SSH keys, extract the key comment if available
|
||||
if (type.includes('SSH')) {
|
||||
const sshMatch = content.match(/ssh-\S+\s+\S+\s+(.+)/)
|
||||
if (sshMatch && sshMatch[1]) {
|
||||
return `Comment: ${sshMatch[1].trim()}`
|
||||
}
|
||||
}
|
||||
|
||||
// For PGP keys, try to extract key ID or user info
|
||||
if (type.includes('PGP')) {
|
||||
// This is a simplified preview - proper parsing would require OpenPGP library
|
||||
const lines = content.split('\n')
|
||||
const infoLines = lines.filter(line =>
|
||||
line.includes('User-ID') ||
|
||||
line.includes('Key-ID') ||
|
||||
line.includes('Fingerprint')
|
||||
)
|
||||
if (infoLines.length > 0) {
|
||||
return infoLines.slice(0, 2).join(', ')
|
||||
}
|
||||
}
|
||||
|
||||
// For certificates, try to extract subject/issuer
|
||||
if (type.includes('Certificate')) {
|
||||
const subjectMatch = content.match(/Subject:.*?CN=([^,\n]+)/)
|
||||
const issuerMatch = content.match(/Issuer:.*?CN=([^,\n]+)/)
|
||||
const parts = []
|
||||
if (subjectMatch) parts.push(`Subject: ${subjectMatch[1]}`)
|
||||
if (issuerMatch) parts.push(`Issuer: ${issuerMatch[1]}`)
|
||||
if (parts.length > 0) return parts.join(', ')
|
||||
}
|
||||
|
||||
// Generic preview: show first and last few characters
|
||||
const preview = content.replace(/[\r\n]+/g, ' ').slice(0, 100)
|
||||
return preview.length < content.length ? preview + '...' : preview
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CryptoDetector
|
||||
|
|
@ -347,6 +347,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
}
|
||||
|
||||
function initCryptoKeysToggle() {
|
||||
const cryptoHeader = document.getElementById('cryptoHeader');
|
||||
const cryptoContent = document.getElementById('cryptoContent');
|
||||
const cryptoToggle = cryptoHeader ? cryptoHeader.querySelector('.crypto-toggle') : null;
|
||||
|
||||
if (cryptoHeader && cryptoContent && cryptoToggle) {
|
||||
cryptoHeader.addEventListener('click', () => {
|
||||
const isCurrentlyHidden = cryptoContent.style.display === 'none';
|
||||
cryptoContent.style.display = isCurrentlyHidden ? 'block' : 'none';
|
||||
cryptoToggle.setAttribute('aria-expanded', isCurrentlyHidden ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initRefreshCountdown(refreshInterval) {
|
||||
const refreshTimer = document.getElementById('refreshTimer');
|
||||
if (!refreshTimer || !refreshInterval) return;
|
||||
|
|
@ -363,7 +377,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
// Expose utilities and run them
|
||||
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu, initThemeToggle, initRefreshCountdown };
|
||||
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu, initThemeToggle, initRefreshCountdown, initCryptoKeysToggle };
|
||||
formatEmailDates();
|
||||
formatMailDate();
|
||||
initLockModals();
|
||||
|
|
|
|||
|
|
@ -701,6 +701,176 @@ label {
|
|||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
|
||||
/* Cryptographic Keys Section */
|
||||
|
||||
.mail-crypto-keys {
|
||||
border: 2px solid var(--overlay-purple-30);
|
||||
border-radius: 15px;
|
||||
background: var(--overlay-white-03);
|
||||
padding: 0;
|
||||
margin-bottom: 30px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.crypto-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 25px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.crypto-header:hover {
|
||||
background: var(--overlay-purple-05);
|
||||
}
|
||||
|
||||
.crypto-header h4 {
|
||||
color: var(--color-accent-purple);
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.crypto-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.crypto-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.crypto-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
stroke: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.crypto-icon-collapsed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.crypto-icon-expanded {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crypto-toggle[aria-expanded="true"] .crypto-icon-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crypto-toggle[aria-expanded="true"] .crypto-icon-expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.crypto-content {
|
||||
padding: 0 25px 25px 25px;
|
||||
}
|
||||
|
||||
.crypto-item {
|
||||
background: var(--overlay-white-02);
|
||||
border: 1px solid var(--overlay-purple-20);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.crypto-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.crypto-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.crypto-type {
|
||||
display: inline-block;
|
||||
background: var(--color-accent-purple);
|
||||
color: var(--color-background);
|
||||
padding: 5px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.crypto-filename {
|
||||
color: var(--color-text-dim);
|
||||
font-size: 0.95rem;
|
||||
font-family: monospace;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.crypto-preview {
|
||||
color: var(--color-text-light);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--overlay-white-03);
|
||||
border-left: 3px solid var(--color-accent-purple-light);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.crypto-preview:hover {
|
||||
background: var(--overlay-purple-10);
|
||||
border-left-color: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.crypto-key-content {
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--overlay-white-10);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-light);
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.crypto-key-content::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.crypto-key-content::-webkit-scrollbar-track {
|
||||
background: var(--overlay-white-05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.crypto-key-content::-webkit-scrollbar-thumb {
|
||||
background: var(--overlay-purple-30);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.crypto-key-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-accent-purple);
|
||||
}
|
||||
|
||||
.mail-attachments h4 {
|
||||
color: var(--color-accent-purple);
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.attachments-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ const debug = require('debug')('48hr-email:routes')
|
|||
|
||||
const config = require('../../../application/config')
|
||||
const Helper = require('../../../application/helper')
|
||||
const CryptoDetector = require('../../../application/crypto-detector')
|
||||
const helper = new(Helper)
|
||||
const cryptoDetector = new CryptoDetector()
|
||||
const { checkLockAccess } = require('../middleware/lock')
|
||||
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
|
|
@ -112,6 +114,10 @@ router.get(
|
|||
// Emails are immutable, cache if found
|
||||
res.set('Cache-Control', 'private, max-age=600')
|
||||
|
||||
// Detect cryptographic keys in attachments
|
||||
const cryptoAttachments = cryptoDetector.detectCryptoAttachments(mail.attachments)
|
||||
debug(`Found ${cryptoAttachments.length} cryptographic attachments`)
|
||||
|
||||
const inboxLock = req.app.get('inboxLock')
|
||||
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
|
||||
const hasAccess = req.session && req.session.lockedInbox === req.params.address
|
||||
|
|
@ -124,6 +130,7 @@ router.get(
|
|||
count: count,
|
||||
totalcount: totalcount,
|
||||
mail,
|
||||
cryptoAttachments: cryptoAttachments,
|
||||
uid: req.params.uid,
|
||||
branding: config.http.branding,
|
||||
lockEnabled: config.lock.enabled,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,33 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if cryptoAttachments and cryptoAttachments|length > 0 %}
|
||||
<div class="mail-crypto-keys">
|
||||
<div class="crypto-header" id="cryptoHeader">
|
||||
<h4>Cryptographic Keys & Signatures ({{ cryptoAttachments|length }})</h4>
|
||||
<button class="crypto-toggle" aria-label="Toggle cryptographic keys visibility" aria-expanded="false">
|
||||
<svg class="crypto-icon crypto-icon-collapsed" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
<svg class="crypto-icon crypto-icon-expanded" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="18 15 12 9 6 15"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="crypto-content" id="cryptoContent" style="display: none;">
|
||||
{% for crypto in cryptoAttachments %}
|
||||
<div class="crypto-item">
|
||||
<div class="crypto-item-header">
|
||||
<span class="crypto-type">{{ crypto.type }}</span>
|
||||
<span class="crypto-filename">{{ crypto.filename }}{% if crypto.info %} · {{ crypto.info }}{% endif %}</span>
|
||||
</div>
|
||||
<pre class="crypto-key-content">{{ crypto.content }}</pre>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if mail.attachments %}
|
||||
<div class="mail-attachments">
|
||||
<h4>Attachments</h4>
|
||||
|
|
@ -62,3 +89,15 @@
|
|||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{{ parent() }}
|
||||
<script>
|
||||
// Initialize crypto keys toggle
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (window.utils && typeof window.utils.initCryptoKeysToggle === 'function') {
|
||||
window.utils.initCryptoKeysToggle();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "48hr.email",
|
||||
"version": "1.7.6",
|
||||
"version": "1.8.0",
|
||||
"private": false,
|
||||
"description": "48hr.email is your favorite open-source tempmail client.",
|
||||
"keywords": [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue