This commit is contained in:
ghostersk
2025-07-16 15:39:28 +01:00
commit cb13605d85
17 changed files with 4014 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/go.mod
/go.sum
/ImageMagick
wordlist.txt

149
README.md Normal file
View File

@@ -0,0 +1,149 @@
# Email Header Analyzer - Build & Usage Instructions
## Building a Single-File Executable (Windows & Linux)
This app uses Go's `embed` package to bundle all static files (web UI, icons, CSS, etc.) into a single executable. You do **not** need to distribute the `web/` folder separately.
### Prerequisites
- [Go 1.16+](https://golang.org/dl/) (required for `embed`)
- (Optional) [Python 3](https://www.python.org/) with Pillow for icon conversion
### 1. Prepare Icons
- Place your tray icon and favicon in `web/icon.png` and `web/favicon.png`.
- For Windows tray icon, you **must** use a `.ico` file. Use the provided script:
```sh
# Convert PNG to ICO (requires Python Pillow)
python web/convert_icon.py web/icon.png web/icon.ico
# using ImageMagick convertor
magick envelope.jpg -define icon:auto-resize=16,32,48,64 favicon.ico
ImageMagick\magick.exe web\icon.png -define icon:auto-resize=16,32,48,64 web\favicon.ico
ImageMagick\magick.exe identify web\favicon.ico
# App Icon:
ImageMagick\magick.exe web\icon.png -resize 256x256 -define icon:format=bmp icon.ico
go install github.com/akavel/rsrc@latest
rsrc -arch amd64 -ico icon.ico -o icon.syso
go clean -cache
```
- For Linux, PNG is fine for the tray icon.
### 2. Build for Your Platform
#### Windows (from Windows PowerShell or Command Prompt):
```powershell
# PowerShell (set environment variables before the command)
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
```
```cmd
REM Command Prompt (set environment variables before the command)
set GOOS=windows
set GOARCH=amd64
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
```
#### Linux/macOS (from Bash):
```sh
# Build 64-bit Linux executable
GOOS=linux GOARCH=amd64 go build -o headeranalyzer main.go
```
- The resulting executable contains all static files and icons.
### 2.1. Add an Icon to the Windows Executable
By default, Go does not embed an icon in the .exe. To add your tray/web icon as the Windows executable icon:
1. Install the `rsrc` tool (one-time):
```powershell
go install github.com/akavel/rsrc@latest
```
2. Generate a Windows resource file with your icon:
```powershell
rsrc -ico web/icon.ico -o icon.syso
```
This creates `icon.syso` in your project root. Go will automatically include it when building for Windows.
3. Build your app for Windows (see below for PowerShell/CMD syntax).
### 2.2. Build for Your Platform
#### Windows (from Windows PowerShell or Command Prompt):
```powershell
# PowerShell (set environment variables before the command)
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
```
```cmd
REM Command Prompt (set environment variables before the command)
set GOOS=windows
set GOARCH=amd64
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
```
- The `-ldflags "-H=windowsgui"` flag prevents a console window from opening when you run the app.
- The `icon.syso` file ensures your executable uses the same icon as the tray and web UI.
#### Linux/macOS (from Bash):
```sh
# Build 64-bit Linux executable
GOOS=linux GOARCH=amd64 go build -o headeranalyzer main.go
```
- The resulting executable contains all static files and icons.
### 3. Run the App
- Double-click or run from terminal:
- On Windows: `headeranalyzer.exe`
- On Linux: `./headeranalyzer`
- The app will start a web server (default: http://localhost:8080) and show a system tray icon.
- Use the tray menu to open the web UI or quit the app.
### 4. Usage Notes
- All static files (HTML, CSS, JS, icons) are embedded. No need to copy the `web/` folder.
- For custom icons, always convert to `.ico` for Windows tray compatibility.
- The favicon is served from the embedded files.
### 5. Troubleshooting
- If icons do not appear in the tray, ensure you used a `.ico` file for Windows.
- If you update static files, rebuild the executable to embed the latest changes.
### 5.1. If the Executable Still Has No Icon
If you followed all steps and the icon still does not appear:
- **Check icon.syso location:** It must be in the same folder as `main.go` when you run `go build`.
- **Check for multiple .syso files:** Only one `.syso` file should be present in your project root. Delete any others.
- **Try a different icon:** Some ICO files may be malformed or missing required sizes. Use a simple 32x32 or 64x64 PNG, convert again, and re-run `rsrc`.
- **Clear Windows icon cache:** Sometimes Windows caches icons. Move the `.exe` to a new folder, or restart Explorer.
- **Verify with Resource Hacker:** Download [Resource Hacker](http://www.angusj.com/resourcehacker/) and open your `.exe` to see if the icon is embedded. If not, the build did not pick up `icon.syso`.
- **Try building without cross-compiling:** If you are cross-compiling, try building directly on Windows.
- **Try go build without -ldflags:** Rarely, the `-ldflags` flag can interfere. Try building with and without it:
```powershell
go build -o headeranalyzer.exe main.go
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
```
- **Try go generate:** If you use `go generate`, ensure it does not overwrite or remove `icon.syso`.
If none of these work, please report your Go version, OS, and the exact steps you used.
---
## Favicon and Tray Icon Support
- The app uses both `favicon.png` and `icon.ico` for browser and tray compatibility.
- The browser will use `icon.ico` if available (for Windows/Edge/PWA), and `favicon.png` for other platforms.
- The tray icon is loaded from the embedded `icon.ico`.
- If you update icons, place the new files in `web/` and rebuild.
### How to Add or Update Icons
1. Place your PNG icon (32x32 or 64x64 recommended) in `web/favicon.png`.
2. Convert it to ICO for Windows tray support:
```sh
python web/convert_icon.py web/favicon.png web/icon.ico
```
3. Both files must be present in `web/` before building.
## License
MIT

33
build-resource.bat Normal file
View File

@@ -0,0 +1,33 @@
@echo off
echo Compiling Windows resource file...
REM Check if windres is available (part of MinGW/MSYS2)
where windres >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo Error: windres not found. Please install MinGW-w64 or MSYS2.
echo Download from: https://www.msys2.org/
echo After installation, run: pacman -S mingw-w64-x86_64-toolchain
pause
exit /b 1
)
REM Check if favicon.ico exists
if not exist "web\favicon.ico" (
echo Error: web\favicon.ico not found!
echo Please ensure your favicon.ico file is in the web folder.
pause
exit /b 1
)
REM Compile resource file to .syso
windres -i resource.rc -o resource.syso -O coff
if %ERRORLEVEL% NEQ 0 (
echo Error: Failed to compile resource file
pause
exit /b 1
)
echo Resource file compiled successfully: resource.syso
echo Now you can build the executable with: go build -ldflags="-H=windowsgui" -o headeranalyzer.exe
pause

32
build-resource.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
echo "Compiling Windows resource file..."
# Check if x86_64-w64-mingw32-windres is available (for cross-compilation)
if command -v x86_64-w64-mingw32-windres >/dev/null 2>&1; then
WINDRES="x86_64-w64-mingw32-windres"
elif command -v windres >/dev/null 2>&1; then
WINDRES="windres"
else
echo "Error: windres not found. Please install MinGW-w64."
echo "On Ubuntu/Debian: sudo apt-get install mingw-w64"
echo "On macOS: brew install mingw-w64"
exit 1
fi
# Check if favicon.ico exists
if [ ! -f "web/favicon.ico" ]; then
echo "Error: web/favicon.ico not found!"
echo "Please ensure your favicon.ico file is in the web folder."
exit 1
fi
# Compile resource file to .syso
$WINDRES -i resource.rc -o resource.syso -O coff
if [ $? -ne 0 ]; then
echo "Error: Failed to compile resource file"
exit 1
fi
echo "Resource file compiled successfully: resource.syso"
echo "Now you can build the executable with:"
echo "GOOS=windows GOARCH=amd64 go build -ldflags=\"-H=windowsgui\" -o headeranalyzer.exe"

39
build-windows.bat Normal file
View File

@@ -0,0 +1,39 @@
@echo off
echo Building HeaderAnalyzer for Windows...
REM Clean previous builds
if exist "headeranalyzer.exe" del "headeranalyzer.exe"
REM Check if resource.syso exists
if not exist "resource.syso" (
echo Warning: resource.syso not found. Building resource file first...
call build-resource.bat
if %ERRORLEVEL% NEQ 0 (
echo Failed to build resource file. Continuing without icon...
)
)
REM Set environment variables for Windows build
set GOOS=windows
set GOARCH=amd64
REM Build the executable with Windows GUI subsystem (no console window)
echo Building executable...
go build -ldflags="-H=windowsgui -s -w" -o headeranalyzer.exe
if %ERRORLEVEL% NEQ 0 (
echo Error: Build failed
pause
exit /b 1
)
echo Build completed successfully: headeranalyzer.exe
echo The executable includes:
echo - Embedded web assets
echo - Application icon (if resource.syso exists)
echo - No console window
echo - System tray support
REM Check file size
for %%A in (headeranalyzer.exe) do echo File size: %%~zA bytes
pause

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

547
main.go Normal file
View File

@@ -0,0 +1,547 @@
package main
import (
"context"
"embed"
"encoding/json"
"flag"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
"headeranalyzer/parser"
"headeranalyzer/passwordgenerator"
"github.com/getlantern/systray"
"github.com/miekg/dns"
)
var (
addr = flag.String("host", "127.0.0.1", "IP to bind")
port = flag.Int("port", 5555, "Port to run on")
)
//go:embed web/*
var embeddedFiles embed.FS
func onReady(addrPort string, shutdownCh chan struct{}) {
var iconPath string
if runtime.GOOS == "windows" {
iconPath = "web/favicon.ico"
} else {
iconPath = "web/favicon.png"
}
iconData, err := fs.ReadFile(embeddedFiles, iconPath)
if err != nil {
log.Printf("Failed to load system tray icon (%s): %v", iconPath, err)
return
}
if len(iconData) == 0 {
log.Printf("System tray icon (%s) is empty", iconPath)
return
}
log.Printf("Loaded system tray icon (%s): %d bytes", iconPath, len(iconData))
// SetIcon does not return an error, so call it directly
systray.SetIcon(iconData)
systray.SetTitle("HeaderAnalyzer")
systray.SetTooltip("Email Header Analyzer")
mOpen := systray.AddMenuItem("Open Web UI", "Open the web interface")
mQuit := systray.AddMenuItem("Quit", "Quit the app")
go func() {
for {
select {
case <-mOpen.ClickedCh:
url := "http://" + addrPort
openBrowser(url)
case <-mQuit.ClickedCh:
systray.Quit()
close(shutdownCh)
return
}
}
}()
}
func openBrowser(url string) {
switch runtime.GOOS {
case "windows":
exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
exec.Command("open", url).Start()
default:
exec.Command("xdg-open", url).Start()
}
}
func main() {
flag.Parse()
// Initialize password generator word list
passwordgenerator.InitWordList()
tmpl := template.Must(template.New("index.html").Funcs(template.FuncMap{
"splitString": func(s, delimiter string) []string {
return strings.Split(s, delimiter)
},
"contains": func(s, substr string) bool {
return strings.Contains(s, substr)
},
}).ParseFS(embeddedFiles, "web/index.html"))
// Use embedded static files for web assets
staticFS, err := fs.Sub(embeddedFiles, "web")
if err != nil {
panic(err)
}
// Serve static files from embedded FS
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
// Serve favicon and tray icon from embedded FS
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
data, err := fs.ReadFile(staticFS, "favicon.ico")
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "image/x-icon")
w.Write(data)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
headers := r.FormValue("headers")
report := parser.Analyze(headers)
tmpl.Execute(w, report)
return
}
tmpl.Execute(w, nil)
})
http.HandleFunc("/dns", func(w http.ResponseWriter, r *http.Request) {
dnsTmpl := template.Must(template.ParseFS(embeddedFiles, "web/dns.html"))
dnsTmpl.Execute(w, nil)
})
http.HandleFunc("/api/dns", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("query")
typeq := r.URL.Query().Get("type")
if query == "" || typeq == "" {
w.WriteHeader(400)
w.Write([]byte("Missing query or type"))
return
}
dnsServer := r.URL.Query().Get("server")
var result string
switch typeq {
case "WHOIS":
resp, err := http.Get("https://rdap.org/domain/" + query)
if err != nil {
result = "WHOIS lookup failed: " + err.Error()
} else {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// Try to parse JSON and extract key info
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err == nil {
var lines []string
if v, ok := data["ldhName"]; ok {
lines = append(lines, fmt.Sprintf("Domain: %v", v))
}
if v, ok := data["status"]; ok {
if arr, ok := v.([]interface{}); ok {
lines = append(lines, fmt.Sprintf("Status: %v", strings.Join(func() []string {
s := make([]string, len(arr))
for i, x := range arr {
s[i] = fmt.Sprintf("%v", x)
}
return s
}(), ", ")))
} else {
lines = append(lines, fmt.Sprintf("Status: %v", v))
}
}
var registrar, registrarIANA string
var registrant, registrantEmail, registrantPhone, registrantOrg, registrantCountry string
if v, ok := data["entities"]; ok {
if ents, ok := v.([]interface{}); ok {
for _, ent := range ents {
if entmap, ok := ent.(map[string]interface{}); ok {
var rolestr string
if roles, ok := entmap["roles"]; ok {
if rlist, ok := roles.([]interface{}); ok {
for _, r := range rlist {
rolestr = fmt.Sprintf("%v", r)
if rolestr == "registrar" {
if v, ok := entmap["vcardArray"]; ok {
if vcard, ok := v.([]interface{}); ok && len(vcard) > 1 {
if props, ok := vcard[1].([]interface{}); ok {
for _, prop := range props {
if arr, ok := prop.([]interface{}); ok && len(arr) > 3 {
if arr[0] == "fn" {
registrar = fmt.Sprintf("%v", arr[3])
}
if arr[0] == "org" {
registrar = fmt.Sprintf("%v", arr[3])
}
if arr[0] == "ianaRegistrarId" {
registrarIANA = fmt.Sprintf("%v", arr[3])
}
}
}
}
}
}
}
}
}
}
if rolestr == "registrant" {
if v, ok := entmap["vcardArray"]; ok {
if vcard, ok := v.([]interface{}); ok && len(vcard) > 1 {
if props, ok := vcard[1].([]interface{}); ok {
for _, prop := range props {
if arr, ok := prop.([]interface{}); ok && len(arr) > 3 {
if arr[0] == "fn" {
registrant = fmt.Sprintf("%v", arr[3])
}
if arr[0] == "email" {
registrantEmail = fmt.Sprintf("%v", arr[3])
}
if arr[0] == "tel" {
registrantPhone = fmt.Sprintf("%v", arr[3])
}
if arr[0] == "org" {
registrantOrg = fmt.Sprintf("%v", arr[3])
}
if arr[0] == "adr" {
if adr, ok := arr[3].([]interface{}); ok && len(adr) > 6 {
registrantCountry = fmt.Sprintf("%v", adr[6])
}
}
}
}
}
}
}
}
}
}
}
}
if registrar != "" {
lines = append(lines, "Registrar: "+registrar)
}
if registrarIANA != "" {
lines = append(lines, "Registrar IANA ID: "+registrarIANA)
}
if registrant != "" {
lines = append(lines, "Registrant: "+registrant)
}
if registrantOrg != "" {
lines = append(lines, "Registrant Org: "+registrantOrg)
}
if registrantEmail != "" {
lines = append(lines, "Registrant Email: "+registrantEmail)
}
if registrantPhone != "" {
lines = append(lines, "Registrant Phone: "+registrantPhone)
}
if registrantCountry != "" {
lines = append(lines, "Registrant Country: "+registrantCountry)
}
if v, ok := data["nameservers"]; ok {
if nsarr, ok := v.([]interface{}); ok && len(nsarr) > 0 {
nslist := make([]string, 0, len(nsarr))
for _, ns := range nsarr {
if nsmap, ok := ns.(map[string]interface{}); ok {
if ldh, ok := nsmap["ldhName"]; ok {
nslist = append(nslist, fmt.Sprintf("%v", ldh))
}
}
}
if len(nslist) > 0 {
lines = append(lines, "Nameservers: "+strings.Join(nslist, ", "))
}
}
}
if v, ok := data["secureDNS"]; ok {
if sec, ok := v.(map[string]interface{}); ok {
if ds, ok := sec["delegationSigned"]; ok {
lines = append(lines, fmt.Sprintf("DNSSEC: %v", ds))
}
}
}
if v, ok := data["events"]; ok {
if evs, ok := v.([]interface{}); ok {
for _, ev := range evs {
if evm, ok := ev.(map[string]interface{}); ok {
if action, ok := evm["eventAction"]; ok {
if date, ok := evm["eventDate"]; ok {
lines = append(lines, fmt.Sprintf("%v: %v", action, date))
}
}
}
}
}
}
if v, ok := data["remarks"]; ok {
if rems, ok := v.([]interface{}); ok {
for _, rem := range rems {
if remm, ok := rem.(map[string]interface{}); ok {
if desc, ok := remm["description"]; ok {
if descarr, ok := desc.([]interface{}); ok && len(descarr) > 0 {
lines = append(lines, fmt.Sprintf("Remark: %v", descarr[0]))
}
}
}
}
}
}
result = strings.Join(lines, "\n")
} else {
result = string(body)
}
}
default:
// Special handling for SPF, DKIM, DMARC
var answers []string
switch strings.ToUpper(typeq) {
case "SPF":
// Query TXT records and filter for SPF
target := dns.Fqdn(query)
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
if dnsServer != "" {
if !strings.Contains(dnsServer, ":") {
dnsServer = dnsServer + ":53"
}
resolvers = []string{dnsServer}
}
for _, resolverAddr := range resolvers {
m := new(dns.Msg)
m.SetQuestion(target, dns.TypeTXT)
resp, err := dns.Exchange(m, resolverAddr)
if err == nil && resp != nil && len(resp.Answer) > 0 {
for _, ans := range resp.Answer {
if t, ok := ans.(*dns.TXT); ok {
for _, txt := range t.Txt {
if strings.HasPrefix(txt, "v=spf1") {
answers = append(answers, txt)
}
}
}
}
break
}
}
if len(answers) == 0 {
result = "No SPF record found."
} else {
result = "SPF record(s):\n" + strings.Join(answers, "\n")
}
case "DMARC":
// Query _dmarc.<domain> TXT
dmarc := "_dmarc." + query
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(dmarc), dns.TypeTXT)
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
if dnsServer != "" {
if !strings.Contains(dnsServer, ":") {
dnsServer = dnsServer + ":53"
}
resolvers = []string{dnsServer}
}
for _, resolverAddr := range resolvers {
resp, err := dns.Exchange(m, resolverAddr)
if err == nil && resp != nil && len(resp.Answer) > 0 {
for _, ans := range resp.Answer {
if t, ok := ans.(*dns.TXT); ok {
answers = append(answers, strings.Join(t.Txt, ""))
}
}
break
}
}
if len(answers) == 0 {
result = "No DMARC record found."
} else {
result = "DMARC record(s):\n" + strings.Join(answers, "\n")
}
case "DKIM":
// Query <selector>._domainkey.<domain> TXT, if not found, check CNAME and then TXT at CNAME target
domain := query
selector := r.URL.Query().Get("selector")
if strings.Contains(query, ":") {
parts := strings.SplitN(query, ":", 2)
domain = parts[0]
selector = parts[1]
}
if selector == "" {
selector = "default"
}
if selector == "default" && !strings.Contains(query, ":") {
answers = append(answers, "Tip: For DKIM, specify selector as domain:selector (e.g. example.com:selector1)")
}
dkim := selector + "._domainkey." + domain
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
if dnsServer != "" {
if !strings.Contains(dnsServer, ":") {
dnsServer = dnsServer + ":53"
}
resolvers = []string{dnsServer}
}
foundTXT := false
var cnameTarget string
for _, resolverAddr := range resolvers {
// 1. Query TXT at DKIM name
mTXT := new(dns.Msg)
mTXT.SetQuestion(dns.Fqdn(dkim), dns.TypeTXT)
txtResp, txtErr := dns.Exchange(mTXT, resolverAddr)
if txtErr == nil && txtResp != nil && len(txtResp.Answer) > 0 {
for _, ans := range txtResp.Answer {
if t, ok := ans.(*dns.TXT); ok {
answers = append(answers, strings.Join(t.Txt, ""))
foundTXT = true
}
}
}
if foundTXT {
break
}
// 2. If no TXT, query CNAME at DKIM name
mCNAME := new(dns.Msg)
mCNAME.SetQuestion(dns.Fqdn(dkim), dns.TypeCNAME)
cnameResp, cnameErr := dns.Exchange(mCNAME, resolverAddr)
if cnameErr == nil && cnameResp != nil && len(cnameResp.Answer) > 0 {
for _, ans := range cnameResp.Answer {
if c, ok := ans.(*dns.CNAME); ok {
cnameTarget = c.Target
}
}
}
if cnameTarget != "" {
// 3. Query TXT at CNAME target
m2 := new(dns.Msg)
m2.SetQuestion(cnameTarget, dns.TypeTXT)
resp2, err2 := dns.Exchange(m2, resolverAddr)
if err2 == nil && resp2 != nil && len(resp2.Answer) > 0 {
for _, ans2 := range resp2.Answer {
if t2, ok := ans2.(*dns.TXT); ok {
answers = append(answers, strings.Join(t2.Txt, ""))
foundTXT = true
}
}
}
if foundTXT {
answers = append(answers, "(via CNAME: "+cnameTarget+")")
break
}
}
if foundTXT {
break
}
}
if len(answers) == 0 || (len(answers) == 1 && strings.HasPrefix(answers[0], "Tip:")) {
result = "No DKIM record found for selector '" + selector + "'."
if len(answers) > 0 {
result += "\n" + answers[0]
}
} else {
result = "DKIM record(s) for selector '" + selector + "':\n" + strings.Join(answers, "\n")
}
default:
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(query), dns.StringToType[typeq])
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
if dnsServer != "" {
if !strings.Contains(dnsServer, ":") {
dnsServer = dnsServer + ":53"
}
resolvers = []string{dnsServer}
}
for _, resolverAddr := range resolvers {
resp, err := dns.Exchange(m, resolverAddr)
if err == nil && resp != nil && len(resp.Answer) > 0 {
for _, ans := range resp.Answer {
result += ans.String() + "\n"
}
break
}
}
if result == "" {
result = "No result found."
}
}
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(result))
})
http.HandleFunc("/password", func(w http.ResponseWriter, r *http.Request) {
passwordTmpl := template.Must(template.ParseFS(embeddedFiles, "web/password.html"))
passwordTmpl.Execute(w, nil)
})
http.HandleFunc("/api/password", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var config passwordgenerator.Config
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid JSON"))
return
}
password, err := passwordgenerator.GeneratePassword(config)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(password))
})
http.HandleFunc("/api/password/info", func(w http.ResponseWriter, r *http.Request) {
count, source, lastUpdate := passwordgenerator.GetWordListInfo()
info := map[string]interface{}{
"wordCount": count,
"source": source,
"lastUpdate": lastUpdate.Format("2006-01-02 15:04:05"),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(info)
})
addrPort := fmt.Sprintf("%s:%d", *addr, *port)
fmt.Printf("Listening on http://%s\n", addrPort)
srv := &http.Server{Addr: addrPort}
shutdownCh := make(chan struct{})
go func() {
log.Fatal(srv.ListenAndServe())
}()
systray.Run(func() { onReady(addrPort, shutdownCh) }, func() {})
<-shutdownCh
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
srv.Shutdown(ctx)
}

1132
parser/analyzer.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,518 @@
package passwordgenerator
import (
"bufio"
"crypto/rand"
"fmt"
"math/big"
"net/http"
"os"
"strings"
"sync"
"time"
)
type Config struct {
Length int `json:"length"`
IncludeUpper bool `json:"includeUpper"`
IncludeLower bool `json:"includeLower"`
NumberCount int `json:"numberCount"`
SpecialChars string `json:"specialChars"`
NoConsecutive bool `json:"noConsecutive"`
UsePassphrase bool `json:"usePassphrase"`
WordCount int `json:"wordCount"`
NumberPosition string `json:"numberPosition"` // "start", "end", "each"
PassphraseUseNumbers bool `json:"passphraseUseNumbers"`
PassphraseUseSpecial bool `json:"passphraseUseSpecial"`
}
// Fallback word list in case online fetch fails
var fallbackWordList = []string{
"apple", "brave", "chair", "dance", "eagle", "flame", "grape", "happy", "island", "jungle",
"kite", "lemon", "magic", "night", "ocean", "piano", "quiet", "river", "storm", "tiger",
"under", "voice", "water", "young", "zebra", "beach", "cloud", "dream", "fresh", "glass",
"heart", "light", "money", "paper", "quick", "royal", "smile", "table", "violet", "world",
"bright", "castle", "gentle", "honest", "knight", "marble", "orange", "purple", "silver", "yellow",
"ancient", "crystal", "distant", "emerald", "fantasy", "harmony", "journey", "mystery", "outdoor", "perfect",
"rainbow", "serenity", "thunder", "universe", "victory", "whisper", "amazing", "balance", "courage", "dignity",
"elegant", "freedom", "golden", "holiday", "inspire", "justice", "kindness", "library", "mountain", "natural",
"peaceful", "quality", "respect", "sunshine", "triumph", "unique", "wonderful", "adventure", "beautiful", "creative",
"delicate", "exciting", "friendly", "generous", "hilarious", "incredible", "joyful", "lovely", "magnificent", "optimistic",
}
// Global word list cache
var (
wordList []string
wordListMux sync.RWMutex
lastFetch time.Time
fetchTimeout = 10 * time.Second
cacheExpiry = 24 * time.Hour // Cache for 24 hours
wordListFile = "wordlist.txt" // Local file path
)
// InitWordList initializes the word list cache. Call this at app startup.
func InitWordList() {
go func() {
// Fetch word list in background to warm up cache
getWordList()
}()
}
// GetWordListInfo returns information about the current word list
func GetWordListInfo() (count int, source string, lastUpdate time.Time) {
wordListMux.RLock()
defer wordListMux.RUnlock()
count = len(wordList)
if count == 0 {
count = len(fallbackWordList)
source = "fallback"
return
}
// Check if we have a local file
if _, err := os.Stat(wordListFile); err == nil {
fileTime := getFileModTime(wordListFile)
if time.Since(fileTime) > cacheExpiry {
source = "local file (expired)"
} else if len(wordList) > len(fallbackWordList) {
source = "local file (from MIT)"
} else {
source = "local file"
}
return count, source, fileTime
}
if time.Since(lastFetch) > cacheExpiry {
source = "cached (expired)"
} else if len(wordList) > len(fallbackWordList) {
source = "MIT online"
} else {
source = "fallback"
}
return count, source, lastFetch
}
// getWordList returns the current word list, fetching from online source if needed
func getWordList() []string {
wordListMux.RLock()
// Check if we have a cached word list that's still valid
if len(wordList) > 0 && time.Since(lastFetch) < cacheExpiry {
defer wordListMux.RUnlock()
return wordList
}
wordListMux.RUnlock()
// Need to fetch or refresh
wordListMux.Lock()
defer wordListMux.Unlock()
// Double-check in case another goroutine already fetched
if len(wordList) > 0 && time.Since(lastFetch) < cacheExpiry {
return wordList
}
// First try to load from local file
if localWords, err := loadWordListFromFile(); err == nil && len(localWords) >= 100 {
wordList = localWords
lastFetch = getFileModTime(wordListFile)
return wordList
}
// If local file doesn't exist or is invalid, try to fetch from MIT
newWordList, err := fetchWordListFromMIT()
if err != nil || len(newWordList) < 100 { // Sanity check
// Use fallback if fetch failed or list is too small
if len(wordList) == 0 {
wordList = make([]string, len(fallbackWordList))
copy(wordList, fallbackWordList)
}
return wordList
}
// Save downloaded word list to local file
saveWordListToFile(newWordList)
wordList = newWordList
lastFetch = time.Now()
return wordList
}
// loadWordListFromFile loads the word list from local file
func loadWordListFromFile() ([]string, error) {
file, err := os.Open(wordListFile)
if err != nil {
return nil, err
}
defer file.Close()
var words []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
word := strings.TrimSpace(scanner.Text())
if len(word) > 0 && len(word) <= 15 {
words = append(words, word)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return words, nil
}
// saveWordListToFile saves the word list to local file
func saveWordListToFile(words []string) error {
file, err := os.Create(wordListFile)
if err != nil {
return err
}
defer file.Close()
for _, word := range words {
if _, err := fmt.Fprintln(file, word); err != nil {
return err
}
}
return nil
}
// getFileModTime returns the modification time of a file
func getFileModTime(filename string) time.Time {
if info, err := os.Stat(filename); err == nil {
return info.ModTime()
}
return time.Time{}
}
// fetchWordListFromMIT fetches the word list from MIT's server
// fetchWordListFromMIT fetches the word list from MIT's server
func fetchWordListFromMIT() ([]string, error) {
client := &http.Client{
Timeout: fetchTimeout,
}
resp, err := client.Get("https://www.mit.edu/~ecprice/wordlist.10000")
if err != nil {
return nil, fmt.Errorf("failed to fetch word list: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var words []string
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
word := strings.TrimSpace(scanner.Text())
if len(word) > 0 && len(word) <= 15 { // Filter out empty lines and very long words
words = append(words, word)
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading word list: %w", err)
}
if len(words) < 100 {
return nil, fmt.Errorf("word list too small: got %d words", len(words))
}
return words, nil
}
func DefaultConfig() Config {
return Config{
Length: 12,
IncludeUpper: true,
IncludeLower: true,
NumberCount: 1,
SpecialChars: "!@#$%^&*-_=+",
NoConsecutive: false,
UsePassphrase: true, // Default to passphrase
WordCount: 3,
NumberPosition: "end",
PassphraseUseNumbers: true,
PassphraseUseSpecial: true,
}
}
func GeneratePassword(config Config) (string, error) {
if config.UsePassphrase {
return generatePassphrase(config)
}
return generateRandomPassword(config)
}
func generateRandomPassword(config Config) (string, error) {
if config.Length < 1 {
return "", fmt.Errorf("password length must be at least 1")
}
var charset string
var required []string
// Build character set
if config.IncludeLower {
charset += "abcdefghijklmnopqrstuvwxyz"
required = append(required, "abcdefghijklmnopqrstuvwxyz")
}
if config.IncludeUpper {
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
required = append(required, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
}
if config.NumberCount > 0 {
charset += "0123456789"
required = append(required, "0123456789")
}
if len(config.SpecialChars) > 0 {
charset += config.SpecialChars
required = append(required, config.SpecialChars)
}
if len(charset) == 0 {
return "", fmt.Errorf("no character types selected")
}
password := make([]byte, config.Length)
// Ensure at least one character from each required set
usedPositions := make(map[int]bool)
// First, place required numbers
numbersPlaced := 0
for numbersPlaced < config.NumberCount && numbersPlaced < config.Length {
pos, err := rand.Int(rand.Reader, big.NewInt(int64(config.Length)))
if err != nil {
return "", err
}
posInt := int(pos.Int64())
if !usedPositions[posInt] {
char, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", err
}
password[posInt] = byte('0' + char.Int64())
usedPositions[posInt] = true
numbersPlaced++
}
}
// Then place at least one from each other required character set
for _, reqSet := range required {
if reqSet == "0123456789" {
continue // Already handled numbers
}
placed := false
attempts := 0
for !placed && attempts < config.Length*2 {
pos, err := rand.Int(rand.Reader, big.NewInt(int64(config.Length)))
if err != nil {
return "", err
}
posInt := int(pos.Int64())
if !usedPositions[posInt] {
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(reqSet))))
if err != nil {
return "", err
}
password[posInt] = reqSet[char.Int64()]
usedPositions[posInt] = true
placed = true
}
attempts++
}
}
// Fill remaining positions
for i := 0; i < config.Length; i++ {
if !usedPositions[i] {
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
password[i] = charset[char.Int64()]
}
}
// Handle no consecutive characters requirement
if config.NoConsecutive {
return ensureNoConsecutive(string(password), charset)
}
return string(password), nil
}
func generatePassphrase(config Config) (string, error) {
if config.WordCount < 1 {
return "", fmt.Errorf("word count must be at least 1")
}
// Get current word list (online or fallback)
currentWordList := getWordList()
words := make([]string, config.WordCount)
for i := 0; i < config.WordCount; i++ {
wordIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(currentWordList))))
if err != nil {
return "", err
}
word := currentWordList[wordIndex.Int64()]
// Capitalize first letter if upper case is enabled
if config.IncludeUpper {
word = strings.Title(word)
}
words[i] = word
}
// Generate numbers if enabled and needed
var numbers []string
if config.PassphraseUseNumbers && config.NumberCount > 0 {
for i := 0; i < config.NumberCount; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", err
}
numbers = append(numbers, fmt.Sprintf("%d", num.Int64()))
}
}
// Helper function to get random separator
getSeparator := func() string {
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
sepIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(config.SpecialChars))))
if err != nil {
return "-" // fallback
}
return string(config.SpecialChars[sepIndex.Int64()])
}
return "-"
}
// Default separator for non-random cases
defaultSeparator := "-"
// Combine based on number position
var result string
if len(numbers) == 0 {
// No numbers, join words with random separators
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
var parts []string
for i, word := range words {
parts = append(parts, word)
if i < len(words)-1 { // Don't add separator after last word
parts = append(parts, getSeparator())
}
}
result = strings.Join(parts, "")
} else {
result = strings.Join(words, defaultSeparator)
}
} else {
switch config.NumberPosition {
case "start":
numberStr := strings.Join(numbers, "")
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
var parts []string
parts = append(parts, numberStr)
for i, word := range words {
parts = append(parts, getSeparator(), word)
if i < len(words)-1 {
parts = append(parts, getSeparator())
}
}
result = strings.Join(parts, "")
} else {
result = numberStr + defaultSeparator + strings.Join(words, defaultSeparator)
}
case "end":
numberStr := strings.Join(numbers, "")
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
var parts []string
for i, word := range words {
parts = append(parts, word)
if i < len(words)-1 {
parts = append(parts, getSeparator())
}
}
parts = append(parts, getSeparator(), numberStr)
result = strings.Join(parts, "")
} else {
result = strings.Join(words, defaultSeparator) + defaultSeparator + numberStr
}
case "each":
var parts []string
for i, word := range words {
parts = append(parts, word)
// Add the specified number of digits after each word
for j := 0; j < config.NumberCount; j++ {
num, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", err
}
parts = append(parts, fmt.Sprintf("%d", num.Int64()))
}
// Add separator after each word+numbers (except last)
if i < len(words)-1 {
parts = append(parts, getSeparator())
}
}
result = strings.Join(parts, "")
default:
numberStr := strings.Join(numbers, "")
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
var parts []string
for i, word := range words {
parts = append(parts, word)
if i < len(words)-1 {
parts = append(parts, getSeparator())
}
}
parts = append(parts, getSeparator(), numberStr)
result = strings.Join(parts, "")
} else {
result = strings.Join(words, defaultSeparator) + defaultSeparator + numberStr
}
}
}
return result, nil
}
func ensureNoConsecutive(password, charset string) (string, error) {
passwordRunes := []rune(password)
maxAttempts := 1000
for attempt := 0; attempt < maxAttempts; attempt++ {
hasConsecutive := false
for i := 0; i < len(passwordRunes)-1; i++ {
if passwordRunes[i] == passwordRunes[i+1] {
// Replace the second character
newChar, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
passwordRunes[i+1] = rune(charset[newChar.Int64()])
hasConsecutive = true
}
}
if !hasConsecutive {
break
}
}
return string(passwordRunes), nil
}

88
resolver/dnscheck.go Normal file
View File

@@ -0,0 +1,88 @@
package resolver
import (
"fmt"
"strings"
"github.com/miekg/dns"
)
var resolvers = []string{"1.1.1.1:53", "8.8.8.8:53"}
func lookupTXT(domain string) ([]string, error) {
for _, server := range resolvers {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
resp, err := dns.Exchange(m, server)
if err != nil || resp == nil {
continue
}
var results []string
for _, ans := range resp.Answer {
if txt, ok := ans.(*dns.TXT); ok {
results = append(results, txt.Txt...)
}
}
if len(results) > 0 {
return results, nil
}
}
return nil, fmt.Errorf("no TXT records found")
}
func CheckSPF(domain string) (string, bool) {
txts, err := lookupTXT(domain)
if err != nil {
return "", false
}
for _, txt := range txts {
if strings.HasPrefix(txt, "v=spf1") {
return txt, true
}
}
return "", false
}
func CheckDMARC(domain string) (string, bool) {
dmarc := "_dmarc." + domain
txts, err := lookupTXT(dmarc)
if err != nil || len(txts) == 0 {
return "", false
}
return txts[0], true
}
var rbls = []string{
"zen.spamhaus.org",
"bl.spamcop.net",
"b.barracudacentral.org",
}
func CheckBlacklists(ip string) []string {
var listed []string
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return listed
}
reversed := fmt.Sprintf("%s.%s.%s.%s", parts[3], parts[2], parts[1], parts[0])
for _, rbl := range rbls {
query := fmt.Sprintf("%s.%s.", reversed, rbl)
m := new(dns.Msg)
m.SetQuestion(query, dns.TypeA)
for _, resolver := range resolvers {
resp, err := dns.Exchange(m, resolver)
if err == nil && resp != nil && len(resp.Answer) > 0 {
for _, ans := range resp.Answer {
if a, ok := ans.(*dns.A); ok {
ip := a.A.String()
if strings.HasPrefix(ip, "127.0.0.") {
listed = append(listed, rbl)
break
}
}
}
}
}
}
return listed
}

35
resource.rc Normal file
View File

@@ -0,0 +1,35 @@
// Windows Resource File for HeaderAnalyzer
// This file defines the icon that will be embedded in the executable
// Application icon (ID 1 is the default for main application icon)
1 ICON "icon.ico"
// Version information
1 VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "HeaderAnalyzer"
VALUE "FileDescription", "Email Header Analyzer"
VALUE "FileVersion", "1.0.0.0"
VALUE "InternalName", "headeranalyzer"
VALUE "LegalCopyright", "MIT License"
VALUE "OriginalFilename", "headeranalyzer.exe"
VALUE "ProductName", "HeaderAnalyzer"
VALUE "ProductVersion", "1.0.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

92
web/dns.html Normal file
View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<title>DNS Tools</title>
<link rel="stylesheet" href="/static/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.dns-tools-container { max-width: 900px; margin: 0 auto; }
.dns-query-form { display: flex; gap: 10px; margin-bottom: 10px; }
.dns-query-form input, .dns-query-form select { padding: 7px; border-radius: 4px; border: 1px solid #444; background: #232323; color: #e0e0e0; }
.dns-query-form button { padding: 7px 16px; }
.dns-results { margin-top: 10px; }
.dns-result-block { background: #232323; border-radius: 6px; margin-bottom: 12px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.12); }
.dns-result-block pre { white-space: pre-wrap; word-break: break-word; font-size: 1em; }
.save-btns { margin-bottom: 10px; }
</style>
</head>
<body>
<nav>
<a href="/">Analyze New Header</a>
<a href="/dns">DNS Tools</a>
<a href="/password">Password Generator</a>
</nav>
<div class="dns-tools-container">
<h1>DNS Tools</h1>
<form class="dns-query-form" id="dnsForm" onsubmit="return false;">
<input type="text" id="dnsInput" placeholder="Enter domain or IP" required>
<select id="dnsType">
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="MX">MX</option>
<option value="TXT">TXT</option>
<option value="NS">NS</option>
<option value="CNAME">CNAME</option>
<option value="PTR">PTR</option>
<option value="SOA">SOA</option>
<option value="SPF">SPF</option>
<option value="DKIM">DKIM</option>
<option value="DMARC">DMARC</option>
<option value="WHOIS">WHOIS</option>
</select>
<input type="text" id="dnsServer" placeholder="Custom DNS server (optional)" style="width:180px;" autocomplete="off">
<button type="submit">Query</button>
</form>
<div class="save-btns">
<button onclick="saveResults('csv')">Save as CSV</button>
<button onclick="saveResults('txt')">Save as TXT</button>
</div>
<div class="dns-results" id="dnsResults"></div>
</div>
<script>
let results = [];
function renderResults() {
const container = document.getElementById('dnsResults');
container.innerHTML = '';
results.forEach(r => {
const block = document.createElement('div');
block.className = 'dns-result-block';
block.innerHTML = `<b>${r.type} for ${r.query}</b><br><pre>${r.result}</pre>`;
container.appendChild(block);
});
}
document.getElementById('dnsForm').addEventListener('submit', async function() {
const query = document.getElementById('dnsInput').value.trim();
const type = document.getElementById('dnsType').value;
let url = `/api/dns?query=${encodeURIComponent(query)}&type=${encodeURIComponent(type)}`;
const dnsServer = document.getElementById('dnsServer').value.trim();
if (dnsServer) url += `&server=${encodeURIComponent(dnsServer)}`;
let res = await fetch(url);
let data = await res.text();
results.unshift({query, type, result: data});
renderResults();
});
function saveResults(format) {
let content = '';
if (format === 'csv') {
content = 'Type,Query,Result\n' + results.map(r => `${r.type},${r.query},"${r.result.replace(/"/g, '""')}"`).join('\n');
} else {
content = results.map(r => `${r.type} for ${r.query}\n${r.result}\n`).join('\n');
}
const blob = new Blob([content], {type: 'text/plain'});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `dnswhois_results.${format}`;
link.click();
}
</script>
</body>
</html>

BIN
web/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
web/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

374
web/index.html Normal file
View File

@@ -0,0 +1,374 @@
<!DOCTYPE html>
<html>
<head>
<title>Email Header Analyzer</title>
<link rel="stylesheet" href="/static/style.css">
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<nav>
<a href="/">Analyze New Header</a>
<a href="/dns">DNS Tools</a>
<a href="/password">Password Generator</a>
</nav>
<div class="container">
<h1>Email Header Analyzer</h1>
{{if not .From}}
<form method="POST">
<textarea name="headers" placeholder="Paste email headers here..."></textarea>
<br>
<button type="submit">Analyze Headers</button>
</form>
{{end}}
{{if .From}}
<div id="report" class="container">
<div class="section" style="display: flex; align-items: flex-start; justify-content: space-between; gap: 30px;">
<div style="flex: 1 1 0; min-width: 0;">
<h2>Sender Identification</h2>
<div class="grid">
<div>
<p><b>Envelope Sender (Return-Path):</b> {{.EnvelopeSender}}</p>
<p><b>From Domain:</b> {{.FromDomain}}</p>
<p><b>Sending Server:</b> {{.SendingServer}}</p>
</div>
<div class="score-indicators">
<span class="status {{if .SPFPass}}good{{else}}error{{end}}" title="SPF">SPF {{if .SPFPass}}✓{{else}}✗{{end}}</span>
<span class="status {{if .DMARCPass}}good{{else}}error{{end}}" title="DMARC">DMARC {{if .DMARCPass}}✓{{else}}✗{{end}}</span>
<span class="status {{if .DKIMPass}}good{{else}}error{{end}}" title="DKIM">DKIM {{if .DKIMPass}}✓{{else}}✗{{end}}</span>
<span class="status {{if .Encrypted}}good{{else}}error{{end}}" title="Encrypted">Encrypted {{if .Encrypted}}✓{{else}}✗{{end}}</span>
{{if .Blacklists}}
<span class="status error" title="{{range .Blacklists}}{{.}}, {{end}}">Blacklisted {{len .Blacklists}} times</span>
{{else}}
<span class="status good">Not listed on major blacklists</span>
{{end}}
</div>
</div>
{{if .SenderRep}}
<div>
<b><span>Sender Reputation: </span></b><div class="status {{if contains .SenderRep "EXCELLENT"}}good{{else if contains .SenderRep "GOOD"}}good{{else if contains .SenderRep "FAIR"}}warning{{else}}error{{end}}">
{{.SenderRep}}
</div>
</div>
{{end}}
<div class="explanation">
<small>
<b>Envelope Sender</b> is the real sender used for delivery (can differ from From).<br>
<b>From Domain</b> is the domain shown to the recipient.<br>
<b>Sending Server</b> is the host or IP that actually sent the message (from first Received header).<br>
If these differ, the message may be sent on behalf of another user or via a third-party service.
</small>
</div>
</div>
</div>
<details id="all-headers" class="section" style="margin-top:10px;">
<summary><b style="font-size: 1.5em;">All Email Headers Table</b></summary>
<div style="margin-bottom:10px;">
<input type="text" id="headerSearch" placeholder="Search headers..." style="width: 100%; max-width: 350px; padding: 5px; border-radius: 4px; border: 1px solid #444; background: #232323; color: #e0e0e0;">
</div>
<div style="overflow-x:auto;">
<table id="headersTable" style="width:100%; border-collapse:collapse; border:1px solid #444;">
<thead>
<tr>
<th style="text-align:left; padding:4px 8px; border:1px solid #444; width: 180px; background:#232323;">Header Name</th>
<th style="text-align:left; padding:4px 8px; border:1px solid #444; background:#232323;">Value</th>
</tr>
</thead>
<tbody>
{{range $k, $v := .AllHeaders}}
<tr>
<td style="vertical-align:top; padding:4px 8px; border:1px solid #444; word-break:break-word;">{{$k}}</td>
<td style="vertical-align:top; padding:4px 8px; border:1px solid #444; white-space:pre-wrap; word-break:break-word;">{{$v}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</details>
<div class="section">
<h2>Basic Information</h2>
<div class="grid">
<div>
<p><b>From:</b> {{.From}}</p>
<p><b>To:</b> {{.To}}</p>
<p><b>Subject:</b> {{.Subject}}</p>
<p><b>Date:</b> {{.Date}}</p>
{{if .ReplyTo}}<p><b>Reply-To:</b> {{.ReplyTo}}</p>{{end}}
</div>
<div>
<p><b>Message-ID:</b> {{.MessageID}}</p>
<p><b>Priority:</b> {{.Priority}}</p>
<p><b>Content Type:</b> {{.ContentType}}</p>
<p><b>Encoding:</b> {{.Encoding}}</p>
</div>
</div>
</div>
<div class="section">
<h2>Mail Flow</h2>
<ul class="mail-flow">
{{range .Received}}<li>{{.}}</li>{{end}}
</ul>
</div>
{{if ne .DeliveryDelay "Insufficient data for delay analysis"}}
<div class="section">
<h2>Delivery Analysis</h2>
<p><b>Delivery Timing:</b> {{.DeliveryDelay}}</p>
{{if .GeoLocation}}<p><b>Geographic Info:</b> {{.GeoLocation}}</p>{{end}}
</div>
{{end}}
<div class="section">
<h2>Security Analysis</h2>
<div class="security-analysis-vertical">
<div class="section">
<h3>SPF Authentication</h3>
<div class="status {{if .SPFPass}}good{{else}}error{{end}}">
{{if .SPFPass}}✓ Passed{{else}}✗ Failed{{end}}
</div>
<p>{{.SPFDetails}}</p>
{{if .SPFRecord}}<pre>{{.SPFRecord}}</pre>{{end}}
{{if .SPFHeader}}
<details class="details"><summary>Show SPF Header</summary><pre>{{.SPFHeader}}</pre></details>
{{end}}
</div>
<div class="section">
<h3>DMARC Policy</h3>
<div class="status {{if .DMARCPass}}good{{else}}error{{end}}">
{{if .DMARCPass}}✓ Passed{{else}}✗ Failed{{end}}
</div>
<p>{{.DMARCDetails}}</p>
{{if .DMARCRecord}}<pre>{{.DMARCRecord}}</pre>{{end}}
{{if .DMARCHeader}}
<details class="details"><summary>Show DMARC Header</summary><pre>{{.DMARCHeader}}</pre></details>
{{end}}
</div>
<div class="section">
<h3>DKIM Signature</h3>
<div class="status {{if .DKIMPass}}good{{else}}error{{end}}">
{{if .DKIMPass}}✓ Present{{else}}✗ Missing{{end}}
</div>
<p>{{.DKIMDetails}}</p>
{{if .DKIM}}
<details class="details"><summary>Show DKIM Header</summary><pre>{{.DKIM}}</pre></details>
{{else if .DKIMHeader}}
<details class="details"><summary>Show DKIM Header</summary><pre>{{.DKIMHeader}}</pre></details>
{{end}}
</div>
</div>
</div>
<div class="section">
<h2>Encryption</h2>
<div class="status {{if .Encrypted}}good{{else}}error{{end}}">
{{if .Encrypted}}Encrypted (TLS){{else}}Not Encrypted{{end}}
</div>
<details><summary>Show Encryption Details</summary><pre>{{.EncryptionDetail}}</pre></details>
</div>
{{if .Warnings}}
<div class="section">
<h2>Warnings</h2>
<ul>
{{range .Warnings}}<li class="status warning">⚠️ {{.}}</li>{{end}}
</ul>
</div>
{{end}}
{{if .SecurityFlags}}
<div class="section">
<h2>Security Flags</h2>
<ul>
{{range .SecurityFlags}}<li class="status">🔒 {{.}}</li>{{end}}
</ul>
</div>
{{end}}
{{if .Blacklists}}
<div class="section">
<h2>Blacklist Status</h2>
<div style="margin-bottom: 6px;">
<b>Checked:</b>
{{if .SendingServer}}IP {{.SendingServer}}{{else if .FromDomain}}Domain {{.FromDomain}}{{end}}
</div>
<div class="status error">⚠️ Listed on the following blacklists:</div>
<ul>
{{range .Blacklists}}<li>{{.}}</li>{{end}}
</ul>
</div>
{{end}}
{{if .SpamFlags}}
<div class="section">
<h2>Spam Analysis</h2>
<div class="status {{if gt (len .SpamFlags) 0}}warning{{else}}good{{end}}">
{{if gt (len .SpamFlags) 0}}⚠️ Spam Indicators Found{{else}}✓ No Spam Indicators{{end}}
</div>
{{if .SpamScore}}<p><b>Spam Score:</b> {{.SpamScore}}</p>{{end}}
{{if .SpamFlags}}
<ul>
{{range .SpamFlags}}<li>{{.}}</li>{{end}}
</ul>
{{end}}
</div>
{{end}}
{{if ne .VirusInfo "No virus scanning information found"}}
<div class="section">
<h2>Virus Scanning</h2>
<div class="status good">🛡️ Virus Scanning Information</div>
<p>{{.VirusInfo}}</p>
</div>
{{end}}
{{if .PhishingRisk}}
<div class="section">
<h2>Security Risk Assessment</h2>
<div class="security-analysis-vertical">
<div class="section">
<h3>Phishing Risk</h3>
<div class="status {{if eq (index (splitString .PhishingRisk " ") 0) "HIGH"}}error{{else if eq (index (splitString .PhishingRisk " ") 0) "MEDIUM"}}warning{{else}}good{{end}}">
{{.PhishingRisk}}
</div>
</div>
<div class="section">
<h3>Spoofing Risk</h3>
<div class="status {{if contains .SpoofingRisk "POTENTIAL"}}warning{{else}}good{{end}}">
{{.SpoofingRisk}}
</div>
</div>
</div>
</div>
{{end}}
{{if .ListInfo}}
<div class="section">
<h2>Mailing List Information</h2>
<ul>
{{range .ListInfo}}<li>{{.}}</li>{{end}}
</ul>
{{if .AutoReply}}<p class="status">📧 Auto-reply message detected</p>{{end}}
{{if .BulkEmail}}<p class="status">📬 Bulk/marketing email detected</p>{{end}}
</div>
{{end}}
{{if .Compliance}}
<div class="section">
<h2>Compliance Information</h2>
<ul>
{{range .Compliance}}<li class="status good">✓ {{.}}</li>{{end}}
</ul>
</div>
{{end}}
{{if .ARC}}
<div class="section">
<h2>ARC (Authenticated Received Chain)</h2>
<details><summary>Show ARC Headers</summary>
<ul>
{{range .ARC}}<li><pre>{{.}}</pre></li>{{end}}
</ul>
</details>
</div>
{{end}}
{{if ne .BIMI "No BIMI record found"}}
<div class="section">
<h2>Brand Indicators (BIMI)</h2>
<p>{{.BIMI}}</p>
</div>
{{end}}
{{if .Attachments}}
<div class="section">
<h2>Attachment Information</h2>
<ul>
{{range .Attachments}}<li>{{.}}</li>{{end}}
</ul>
</div>
{{end}}
{{if .URLs}}
<div class="section">
<h2>URL Information</h2>
<ul>
{{range .URLs}}<li>{{.}}</li>{{end}}
</ul>
</div>
{{end}}
{{if ne .ThreadInfo "No threading information available"}}
<div class="section">
<h2>Message Threading</h2>
<details><summary>Show Threading Information</summary>
<pre>{{.ThreadInfo}}</pre>
</details>
</div>
{{end}}
<div class="section">
<button onclick="exportPDF()" type="button">Export as PDF</button>
<button onclick="exportImage()" type="button">Save as Image</button>
</div>
</div>
{{end}}
<script>
// Template helper functions for the enhanced features
function splitString(str, delimiter) {
return str.split(delimiter);
}
function contains(str, substr) {
return str.includes(substr);
}
function exportImage() {
html2canvas(document.querySelector("#report")).then(canvas => {
let link = document.createElement("a");
link.download = "email-analysis.png";
link.href = canvas.toDataURL();
link.click();
});
}
function exportPDF() {
// Expand all details before export
document.querySelectorAll('#report details').forEach(d => d.open = true);
document.getElementById('all-headers').open = true;
const element = document.getElementById('report');
const opt = {
margin: 0.1,
filename: 'email-analysis.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait', putOnlyUsedFonts:true }
};
html2pdf().set(opt).from(element).save();
}
// Header table search
document.addEventListener('DOMContentLoaded', function() {
var search = document.getElementById('headerSearch');
if (search) {
search.addEventListener('input', function() {
var filter = search.value.toLowerCase();
var rows = document.querySelectorAll('#headersTable tbody tr');
rows.forEach(function(row) {
var text = row.textContent.toLowerCase();
row.style.display = text.indexOf(filter) > -1 ? '' : 'none';
});
});
}
});
</script>
</body>
</html>

664
web/password.html Normal file
View File

@@ -0,0 +1,664 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password Generator - HeaderAnalyzer</title>
<link rel="stylesheet" href="/static/style.css">
<style>
.password-generator {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.password-output {
background: #1a1a1a;
border: 2px solid #333;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 18px;
word-break: break-all;
position: relative;
}
.password-text {
color: #00ff88;
font-weight: bold;
margin-bottom: 10px;
}
.copy-btn {
background: #007acc;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.copy-btn:hover {
background: #005999;
}
.copy-btn.copied {
background: #00aa44;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin: 20px 0;
}
.control-group {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 20px;
}
.control-group h3 {
margin-top: 0;
color: #00ff88;
border-bottom: 1px solid #333;
padding-bottom: 10px;
}
.form-row {
margin: 15px 0;
display: flex;
align-items: center;
gap: 10px;
}
.form-row label {
flex: 1;
color: #ccc;
}
.form-row input[type="number"],
.form-row input[type="text"],
.form-row select {
background: #2a2a2a;
border: 1px solid #444;
color: #fff;
padding: 8px;
border-radius: 4px;
width: 120px;
}
.form-row input[type="text"] {
width: 200px;
}
.form-row input[type="checkbox"] {
width: auto;
}
.generate-btn {
background: #00aa44;
color: white;
border: none;
padding: 15px 30px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
width: 100%;
margin: 20px 0;
}
.generate-btn:hover {
background: #008833;
}
.tab-buttons {
display: flex;
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #333;
}
.tab-btn {
flex: 1;
background: #2a2a2a;
color: #ccc;
border: none;
padding: 15px;
cursor: pointer;
font-size: 16px;
}
.tab-btn.active {
background: #007acc;
color: white;
}
.tab-btn:hover:not(.active) {
background: #333;
}
.passphrase-controls {
display: none;
}
.passphrase-controls.active {
display: block;
}
.url-share {
background: #2a2a2a;
border: 1px solid #444;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
}
.url-share input {
background: #1a1a1a;
border: 1px solid #333;
color: #ccc;
padding: 8px;
border-radius: 4px;
width: 100%;
font-size: 12px;
}
@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<nav>
<div class="nav-container">
<div class="nav-links">
<a href="/">Email Analysis</a>
<a href="/dns">DNS Tools</a>
<a href="/password" class="active">Password Generator</a>
</div>
</div>
</nav>
<div class="container password-generator">
<h1>🔐 Password Generator</h1>
<div class="tab-buttons">
<button class="tab-btn" id="randomTab">Random Password</button>
<button class="tab-btn active" id="passphraseTab">Passphrase</button>
</div>
<div class="password-output">
<div class="password-text" id="passwordDisplay">Click "Generate Password" to create a secure password</div>
<button class="copy-btn" id="copyBtn" onclick="copyPassword()" style="display: none;">Copy to Clipboard</button>
</div>
<button class="generate-btn" onclick="generatePassword()">🎲 Generate Password</button>
<div class="controls">
<div class="control-group">
<h3>🔧 Basic Settings</h3>
<div class="form-row">
<label for="length">Password Length:</label>
<input type="number" id="length" min="4" max="128" value="12">
</div>
<div class="form-row">
<label for="includeUpper">Include Uppercase (A-Z):</label>
<input type="checkbox" id="includeUpper" checked>
</div>
<div class="form-row">
<label for="includeLower">Include Lowercase (a-z):</label>
<input type="checkbox" id="includeLower" checked>
</div>
<div class="form-row">
<label for="numberCount">Number of Digits:</label>
<input type="number" id="numberCount" min="0" max="20" value="1">
</div>
<div class="form-row">
<label for="specialChars">Special Characters:</label>
<input type="text" id="specialChars" value="!@#$%^&*-_=+">
</div>
<div class="form-row">
<label for="noConsecutive">No consecutive identical characters:</label>
<input type="checkbox" id="noConsecutive">
</div>
</div>
<div class="control-group">
<h3>🎯 Advanced Settings</h3>
<div class="passphrase-controls active" id="passphraseControls">
<div class="form-row">
<label for="wordCount">Number of Words:</label>
<input type="number" id="wordCount" min="2" max="10" value="3">
</div>
<div class="form-row">
<label for="passphraseUseNumbers">Include Numbers:</label>
<input type="checkbox" id="passphraseUseNumbers" checked>
</div>
<div class="form-row">
<label for="passphraseUseSpecial">Include Special Characters:</label>
<input type="checkbox" id="passphraseUseSpecial" checked>
</div>
<div class="form-row">
<label for="numberPosition">Number Position:</label>
<select id="numberPosition">
<option value="end">At End</option>
<option value="start">At Start</option>
<option value="each">After Each Word</option>
</select>
</div>
</div>
<div class="form-row">
<label>Strength Indicator:</label>
<div id="strengthIndicator" style="color: #999;">Generate a password to see strength</div>
</div>
<div class="form-row">
<label>Word List Status:</label>
<div id="wordListStatus" style="color: #999; font-size: 12px;">Loading...</div>
</div>
<div class="form-row">
<label for="enableHistory">Save Password History (7 days):</label>
<input type="checkbox" id="enableHistory">
</div>
</div>
</div>
<div id="historySection" style="display: none; margin-top: 20px;">
<div class="control-group" style="max-width: 100%;">
<h3>📜 Password History</h3>
<div style="max-height: 300px; overflow-y: auto;">
<table id="historyTable" style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #2a2a2a;">
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Timestamp</th>
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Password</th>
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Type</th>
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Action</th>
</tr>
</thead>
<tbody id="historyBody">
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let currentMode = 'passphrase'; // Default to passphrase
// Load settings from cookies and URL parameters
window.addEventListener('load', function() {
loadSettings();
updateShareUrl();
loadWordListInfo();
// Auto-generate if URL has parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString()) {
generatePassword();
}
});
async function loadWordListInfo() {
try {
const response = await fetch('/api/password/info');
if (response.ok) {
const info = await response.json();
document.getElementById('wordListStatus').innerHTML =
`${info.wordCount} words from ${info.source}<br><small>Last updated: ${info.lastUpdate}</small>`;
} else {
document.getElementById('wordListStatus').textContent = 'Failed to load word list info';
}
} catch (error) {
document.getElementById('wordListStatus').textContent = 'Error loading word list info';
}
}
function switchTab(mode) {
currentMode = mode;
document.getElementById('randomTab').classList.toggle('active', mode === 'random');
document.getElementById('passphraseTab').classList.toggle('active', mode === 'passphrase');
document.getElementById('passphraseControls').classList.toggle('active', mode === 'passphrase');
saveSettings();
updateShareUrl();
}
document.getElementById('randomTab').addEventListener('click', () => switchTab('random'));
document.getElementById('passphraseTab').addEventListener('click', () => switchTab('passphrase'));
function getConfig() {
return {
length: parseInt(document.getElementById('length').value),
includeUpper: document.getElementById('includeUpper').checked,
includeLower: document.getElementById('includeLower').checked,
numberCount: parseInt(document.getElementById('numberCount').value),
specialChars: document.getElementById('specialChars').value,
noConsecutive: document.getElementById('noConsecutive').checked,
usePassphrase: currentMode === 'passphrase',
wordCount: parseInt(document.getElementById('wordCount').value),
numberPosition: document.getElementById('numberPosition').value,
passphraseUseNumbers: document.getElementById('passphraseUseNumbers').checked,
passphraseUseSpecial: document.getElementById('passphraseUseSpecial').checked,
enableHistory: document.getElementById('enableHistory').checked
};
}
function setConfig(config) {
document.getElementById('length').value = config.length || 12;
document.getElementById('includeUpper').checked = config.includeUpper !== false;
document.getElementById('includeLower').checked = config.includeLower !== false;
document.getElementById('numberCount').value = config.numberCount || 1;
document.getElementById('specialChars').value = config.specialChars || '!@#$%^&*-_=+';
document.getElementById('noConsecutive').checked = config.noConsecutive || false;
document.getElementById('wordCount').value = config.wordCount || 3;
document.getElementById('numberPosition').value = config.numberPosition || 'end';
document.getElementById('passphraseUseNumbers').checked = config.passphraseUseNumbers !== false;
document.getElementById('passphraseUseSpecial').checked = config.passphraseUseSpecial !== false;
document.getElementById('enableHistory').checked = config.enableHistory || false;
if (config.usePassphrase !== false) { // Default to passphrase
switchTab('passphrase');
} else {
switchTab('random');
}
}
function saveSettings() {
const config = getConfig();
config.mode = currentMode;
document.cookie = 'passwordGenSettings=' + encodeURIComponent(JSON.stringify(config)) + '; max-age=31536000; path=/';
}
function loadSettings() {
// First try URL parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString()) {
const config = {};
for (const [key, value] of urlParams) {
if (key === 'includeUpper' || key === 'includeLower' || key === 'noConsecutive' || key === 'usePassphrase' || key === 'passphraseUseNumbers' || key === 'passphraseUseSpecial' || key === 'enableHistory') {
config[key] = value === 'true';
} else if (key === 'length' || key === 'numberCount' || key === 'wordCount') {
config[key] = parseInt(value);
} else {
config[key] = value;
}
}
setConfig(config);
return;
}
// Then try cookies
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'passwordGenSettings') {
try {
const config = JSON.parse(decodeURIComponent(value));
setConfig(config);
currentMode = config.mode || 'passphrase'; // Default to passphrase
switchTab(currentMode);
} catch (e) {
console.error('Failed to parse cookie settings:', e);
// Set defaults on error
switchTab('passphrase');
}
break;
}
}
}
function updateShareUrl() {
const config = getConfig();
const params = new URLSearchParams();
for (const [key, value] of Object.entries(config)) {
params.set(key, value.toString());
}
const newUrl = window.location.pathname + '?' + params.toString();
// Update browser URL without page reload
if (window.history && window.history.pushState) {
window.history.replaceState({}, '', newUrl);
}
}
// Add event listeners to update settings
document.querySelectorAll('input, select').forEach(element => {
element.addEventListener('change', function() {
saveSettings();
updateShareUrl();
});
});
async function generatePassword() {
const config = getConfig();
try {
const response = await fetch('/api/password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config)
});
if (!response.ok) {
throw new Error('Failed to generate password');
}
const result = await response.text();
document.getElementById('passwordDisplay').textContent = result;
document.getElementById('copyBtn').style.display = 'inline-block';
// Save to history
const passwordType = config.usePassphrase ? 'Passphrase' : 'Random';
savePasswordToHistory(result, passwordType);
// Update strength indicator
updateStrengthIndicator(result, config);
// Refresh word list info if using passphrase
if (config.usePassphrase) {
loadWordListInfo();
}
} catch (error) {
document.getElementById('passwordDisplay').textContent = 'Error: ' + error.message;
document.getElementById('copyBtn').style.display = 'none';
}
}
function updateStrengthIndicator(password, config) {
let score = 0;
let feedback = [];
// Length score
if (password.length >= 12) score += 25;
else if (password.length >= 8) score += 15;
else score += 5;
// Character variety
if (/[a-z]/.test(password)) score += 10;
if (/[A-Z]/.test(password)) score += 10;
if (/[0-9]/.test(password)) score += 10;
if (/[^a-zA-Z0-9]/.test(password)) score += 15;
// Length bonus
if (password.length >= 16) score += 10;
if (password.length >= 20) score += 10;
// Passphrase bonus
if (config.usePassphrase) score += 10;
// No consecutive bonus
if (config.noConsecutive) score += 5;
let strength, color;
if (score >= 80) {
strength = "Very Strong 💪";
color = "#00aa44";
} else if (score >= 60) {
strength = "Strong 🔒";
color = "#007acc";
} else if (score >= 40) {
strength = "Medium ⚠️";
color = "#ff8800";
} else {
strength = "Weak ❌";
color = "#cc0000";
}
document.getElementById('strengthIndicator').innerHTML =
`<span style="color: ${color}">${strength}</span> (Score: ${score}/100)`;
}
function copyPassword() {
const passwordText = document.getElementById('passwordDisplay').textContent;
navigator.clipboard.writeText(passwordText).then(function() {
const btn = document.getElementById('copyBtn');
btn.textContent = 'Copied! ✓';
btn.classList.add('copied');
setTimeout(function() {
btn.textContent = 'Copy to Clipboard';
btn.classList.remove('copied');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy: ', err);
});
}
// Password History Management
function savePasswordToHistory(password, type) {
if (!document.getElementById('enableHistory').checked) {
return;
}
const history = getPasswordHistory();
const timestamp = new Date().toLocaleString();
history.unshift({
timestamp: timestamp,
password: password,
type: type,
date: new Date().getTime()
});
// Keep only last 50 passwords and remove entries older than 7 days
const sevenDaysAgo = new Date().getTime() - (7 * 24 * 60 * 60 * 1000);
const filteredHistory = history
.filter(item => item.date > sevenDaysAgo)
.slice(0, 50);
localStorage.setItem('passwordHistory', JSON.stringify(filteredHistory));
updateHistoryDisplay();
}
function getPasswordHistory() {
try {
const history = localStorage.getItem('passwordHistory');
return history ? JSON.parse(history) : [];
} catch (e) {
console.error('Failed to parse password history:', e);
return [];
}
}
function clearPasswordHistory() {
localStorage.removeItem('passwordHistory');
updateHistoryDisplay();
}
function updateHistoryDisplay() {
const historyEnabled = document.getElementById('enableHistory').checked;
const historySection = document.getElementById('historySection');
if (!historyEnabled) {
historySection.style.display = 'none';
return;
}
const history = getPasswordHistory();
const tbody = document.getElementById('historyBody');
if (history.length === 0) {
historySection.style.display = 'none';
return;
}
historySection.style.display = 'block';
tbody.innerHTML = '';
history.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td style="padding: 8px; border: 1px solid #444; font-size: 12px; color: #ccc;">${item.timestamp}</td>
<td style="padding: 8px; border: 1px solid #444; font-family: monospace; word-break: break-all; color: #00ff88;">${item.password}</td>
<td style="padding: 8px; border: 1px solid #444; font-size: 12px; color: #ccc;">${item.type}</td>
<td style="padding: 8px; border: 1px solid #444;">
<button onclick="copyPasswordFromHistory('${item.password}')" style="background: #007acc; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;">Copy</button>
</td>
`;
tbody.appendChild(row);
});
}
function copyPasswordFromHistory(password) {
navigator.clipboard.writeText(password).then(function() {
// Visual feedback could be added here
console.log('Password copied from history');
}).catch(function(err) {
console.error('Failed to copy password from history: ', err);
});
}
// History toggle event listener
document.getElementById('enableHistory').addEventListener('change', function() {
if (!this.checked) {
clearPasswordHistory();
}
updateHistoryDisplay();
saveSettings();
});
// Load settings from cookies and URL parameters
window.addEventListener('load', function() {
loadSettings();
updateShareUrl();
loadWordListInfo();
updateHistoryDisplay();
// Auto-generate if URL has parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString()) {
generatePassword();
}
});
</script>
</body>
</html>

307
web/style.css Normal file
View File

@@ -0,0 +1,307 @@
:root {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--section-bg: #2d2d2d;
--border-color: #404040;
--highlight: #3c5c7c;
--success: #4caf50;
--warning: #ff9800;
--error: #f44336;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
}
h1, h2, h3 {
color: var(--text-color);
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
textarea {
width: 100%;
height: 120px;
background-color: var(--section-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 6px;
border-radius: 4px;
font-family: monospace;
margin-bottom: 10px;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.section {
background-color: var(--section-bg);
border-radius: 8px;
padding: 12px 15px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.score-container {
text-align: center;
padding: 20px;
margin: 20px 0;
}
.score-flex {
display: flex;
align-items: center;
justify-content: center;
gap: 60px;
width: 100%;
}
.score-left {
display: flex;
flex-direction: column;
align-items: center;
min-width: 180px;
}
.score-indicators {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.score-container h2 {
font-size: 1.3em;
margin-bottom: 8px;
border-bottom: none;
padding-bottom: 0;
}
.score-circle {
width: 110px;
height: 110px;
border-radius: 50%;
margin: 0 auto;
position: relative;
background: conic-gradient(var(--success) 0%, var(--success) var(--score), #444 var(--score), #444 100%);
}
.score-inner {
position: absolute;
width: 90px;
height: 90px;
background: var(--section-bg);
border-radius: 50%;
top: 10px;
left: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.7em;
font-weight: bold;
}
.status {
padding: 8px 12px;
margin: 5px 0;
border-radius: 4px;
display: inline-block;
font-weight: bold;
}
.status.good {
background-color: #2d5a2d;
color: #90EE90;
border-left: 4px solid #4CAF50;
}
.status.warning {
background-color: #5a4d2d;
color: #FFD700;
border-left: 4px solid #FF9800;
}
.status.error {
background-color: #5a2d2d;
color: #FFB6C1;
border-left: 4px solid #f44336;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: start;
margin: 10px 0;
width: 100%;
}
pre, code {
background-color: var(--bg-color);
padding: 6px;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.97em;
max-width: 100%;
}
.details pre {
white-space: pre-wrap;
word-break: break-word;
}
.mail-flow {
position: relative;
list-style: none;
padding-left: 20px;
}
.mail-flow li {
position: relative;
padding: 10px;
margin: 10px 0;
background-color: var(--bg-color);
border-radius: 4px;
}
.mail-flow li::before {
content: "↓";
position: absolute;
left: -20px;
color: white;
}
.mail-flow li:first-child::before {
display: none;
}
.compact-score {
padding: 10px 15px;
margin: 10px 0 10px 0;
}
/* Security Analysis vertical layout */
.security-analysis-vertical {
display: flex;
flex-direction: column;
gap: 18px;
}
.security-analysis-vertical .section {
margin-bottom: 15px;
padding: 10px;
border-left: 3px solid #555;
background: none;
box-shadow: none;
}
nav {
width: 100%;
background: #181818;
padding: 0.5em 0 0.5em 1.5em;
margin-bottom: 18px;
box-shadow: 0 2px 4px rgba(0,0,0,0.12);
display: flex;
align-items: center;
}
nav a {
color: #fff;
text-decoration: none;
font-weight: bold;
font-size: 1.1em;
margin-right: 2em;
letter-spacing: 0.5px;
}
nav a:hover {
text-decoration: underline;
}
.container, .section, .grid {
word-break: break-word;
overflow-wrap: anywhere;
}
.explanation {
background-color: #2a2a2a;
border-left: 4px solid #555;
padding: 10px;
margin: 10px 0;
border-radius: 0 4px 4px 0;
}
.explanation small {
color: #ccc;
line-height: 1.4;
}
@media print {
body {
background-color: white;
color: black;
}
.section {
background-color: white;
color: black;
box-shadow: none;
border: 1px solid #ccc;
}
.score-circle, .score-inner {
background: white;
color: black;
}
pre, code {
background: #f5f5f5;
color: black;
border: 1px solid #ccc;
white-space: pre-wrap;
word-break: break-word;
}
}
@media (max-width: 700px) {
.score-flex {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.score-indicators {
flex-direction: row;
gap: 8px;
min-width: 0;
justify-content: center;
}
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}