diff --git a/App.axaml b/App.axaml new file mode 100644 index 0000000..0b01f3f --- /dev/null +++ b/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/App.axaml.cs b/App.axaml.cs new file mode 100644 index 0000000..e55ca4e --- /dev/null +++ b/App.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace kaelus +{ + public partial class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} \ No newline at end of file diff --git a/Assets/kaelus-icon.ico b/Assets/kaelus-icon.ico new file mode 100644 index 0000000..82615f6 Binary files /dev/null and b/Assets/kaelus-icon.ico differ diff --git a/Engine.cs b/Engine.cs new file mode 100644 index 0000000..753474c --- /dev/null +++ b/Engine.cs @@ -0,0 +1,186 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; + +namespace kaelus +{ + internal class Engine + { + #region statics + + internal static HashSet foundEmails = new HashSet(); + + internal static string finishedResults = ""; + + #endregion + + #region static methods + + internal static void ExtractSourceCode(string url) + { + + bool isUrl = IsUrl(url); + + if (isUrl) + { + + } + else + { + url = "https://" + url; + } + + // Scan the main index page and get all links on it + ScanLinks(url); + + // Display the collected emails at the end + DisplayCollectedEmails(); + } + + public static string kaelusScan(string url) + { + bool isUrl = IsUrl(url); + + if (isUrl) + { + + } + else + { + url = "https://" + url; + } + + // Scan the main index page and get all links on it + ScanLinks(url); + + // Display the collected emails at the end + DisplayCollectedEmails(); + return finishedResults; + } + + internal static void ScanLinks(String url) + { + List links = new List(); + + // Fetch page source + string pageContent = FetchPageContent(url); + + if (string.IsNullOrEmpty(pageContent)) + { + Console.WriteLine("No content found on the page."); + return; + } + + // Use HtmlAgilityPack to parse the HTML and find all links + HtmlDocument doc = new HtmlDocument(); + doc.LoadHtml(pageContent); + + foreach (HtmlNode link in doc.DocumentNode.SelectNodes("//a[@href]")) + { + string href = link.GetAttributeValue("href", string.Empty); + + // If it's a relative link, convert it to an absolute URL + Uri baseUri = new Uri(url); + Uri fullUri = new Uri(baseUri, href); + + if (fullUri.Host == baseUri.Host) // Ensure it's the same domain + { + links.Add(fullUri.ToString()); + } + } + + // Scan each link for email addresses + foreach (string link in links) + { + //Console.WriteLine(kaleidolib.lib.Formatting.dim($"Scanning {link} for email addresses...")); + string content = FetchPageContent(link); + if (!string.IsNullOrEmpty(content)) + { + GetMails(content); + } + } + } + + // Method to fetch the page content + internal static string FetchPageContent(string url) + { + try + { + using (HttpClient client = new()) + { + HttpResponseMessage response = client.GetAsync(url).Result; + if (response.IsSuccessStatusCode) + { + return response.Content.ReadAsStringAsync().Result; + } + else + { + Console.WriteLine($"Failed to fetch {url}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error fetching page content: {ex.Message}"); + } + return string.Empty; + } + + // Method to collect emails found on a page + internal static void GetMails(String sourceCode) + { + string emailPattern = @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"; + + // Use Regex.Matches to find all occurrences of email addresses in the input string + MatchCollection matches = Regex.Matches(sourceCode, emailPattern, RegexOptions.IgnoreCase); + + if (matches.Count > 0) + { + foreach (Match match in matches) + { + // Add to the HashSet to ensure uniqueness + foundEmails.Add(match.Value); + } + } + } + + // Display collected emails and filter duplicates + internal static void DisplayCollectedEmails() + { + if (foundEmails.Count > 0) + { + //Console.WriteLine(Color.green("Good news! I found the following unique email addresses:")); + foreach (var email in foundEmails) + { + Console.WriteLine(email); + finishedResults = finishedResults + email + "\n"; + } + } + else + { + //Console.WriteLine(Color.red("No email addresses found.")); + finishedResults = "No email addresses found"; + } + } + + static bool IsUrl(string input) + { + return Uri.TryCreate(input, UriKind.Absolute, out Uri result) && + (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps); + } + + public static String GetTimestamp(DateTime value) + { + return value.ToString("yyyyMMddHHmmss"); + } + + #endregion + } +} diff --git a/MainWindow.axaml b/MainWindow.axaml new file mode 100644 index 0000000..e9fab53 --- /dev/null +++ b/MainWindow.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MainWindow.axaml.cs b/MainWindow.axaml.cs new file mode 100644 index 0000000..5c42205 --- /dev/null +++ b/MainWindow.axaml.cs @@ -0,0 +1,78 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using MsBox.Avalonia; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace kaelus +{ + public partial class MainWindow : Window + { + + public string resultBoxText; + + public MainWindow() + { + InitializeComponent(); + } + public async void StartScan(object sender, RoutedEventArgs args) + { + string url = URLBox.Text; + + // Create a CancellationTokenSource to manage the cancellation + var cancellationTokenSource = new CancellationTokenSource(); + + // Run both tasks concurrently + var progressTask = makeProgress(cancellationTokenSource.Token); + var resultTask = RunKaelusScanAsync(url); + + // When the resultTask completes, cancel the progressTask + await resultTask; + cancellationTokenSource.Cancel(); + + // Await progressTask to ensure proper cleanup + try + { + await progressTask; + } + catch (OperationCanceledException) + { + ScanProgress.IsVisible = false; + } + } + + public void ShowHelp(object sender, RoutedEventArgs args) + { + var helpBox = MessageBoxManager.GetMessageBoxStandard("KAELUS Help", "Just enter the desired URL and click Start. KAELUS will do the rest", MsBox.Avalonia.Enums.ButtonEnum.Ok); + var result = helpBox.ShowAsPopupAsync(this); + } + + public async Task makeProgress(CancellationToken cancellationToken) + { + // Fake Progress bar (to indicate work) + ScanProgress.Value = 0; + ScanProgress.IsVisible = true; + ResultBox.Text = "Scanning for email addresses..."; + + while (ScanProgress.Value < ScanProgress.Maximum) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + await Task.Delay(2000, cancellationToken); + ScanProgress.Value = Math.Min(ScanProgress.Value + 1, ScanProgress.Maximum); + } + + } + + private async Task RunKaelusScanAsync(string url) + { + string result = await Task.Run(() => Engine.kaelusScan(url)); + + ResultBox.Text = result; + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..ea7bb2e --- /dev/null +++ b/Program.cs @@ -0,0 +1,22 @@ +using Avalonia; +using System; + +namespace kaelus +{ + internal class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); + } +} diff --git a/README.md b/README.md index b18687c..c59fbe9 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# kaelus \ No newline at end of file +![Repo-Image](https://druffko.gg/github-images/kaelus-repo.png) + +
+ +![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/druffko/kaelus?include_prereleases) + +![.NET Version](https://img.shields.io/badge/.NET-8.0-brightgreen) +![.NET Version](https://img.shields.io/badge/OS-Windows+macOS-lightgray) +![GitHub last commit](https://img.shields.io/github/last-commit/druffko/kaelus) + +
+ + ![GitHub All Releases](https://img.shields.io/github/downloads/druffko/kaelus/total) + ![GitHub closed issues](https://img.shields.io/github/issues-closed/druffko/kaelus) + ![GitHub issues](https://img.shields.io/github/issues/druffko/kaelus) + +

KAELUS

+

+ KAELUS is a cross-platform (Windows & macOS) Desktop application written in C#, that extracts E-Mail-Addresses from any URL you enter.
+ Based on druffko/kaelus-engine +

+
+ +## Table of Contents +- [About](#about) +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [License](#license) +- [Contact](#contact) + +--- + +## About + +KAELUS is a cross-platform (Windows & macOS) Desktop application written in C#, that extracts E-Mail-Addresses from any URL you enter. + +By automatically scanning the source code of each and every link with the same host domain it is able to find email addresses that are listed on said domain, without having to go through them manually. + +![UI-Screenshot](https://druffko.gg/github-images/kaelus/kaelus-ui.png) + +--- + +## Features + +- ✅ Extract email addresses from websites +- ✅ Scans every link on a website +- ✅ Works on Windows and macOS +- ✅ Show results in GUI + +--- + +## Installation + +### Download the latest version + +To start off, please head to the [releases page](https://github.com/druffko/kaelus/releases) and download a pre-built package. + +*If you don't trust me for some reason, feel free to download the latest released source code and build it your self.* + +### Launch the application + +Launch the application using double clicking. If the Application won't execute because it's from a "unknown developer", right-click and click "open". + +--- + +## Usage + +### Getting email addresses +Simply enter the URL you want to scan. You can either enter the "http://" or "https://" or just simply leave it out. FindMail will "fix" the URL. + +### Click start +Just click start and the applicaton will search the website for email-addresses. Results will be returned in a scrollable textview. + +--- + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository +2. Create a new branch (`git checkout -b feature-name`) +3. Commit your changes (`git commit -m 'Add some feature'`) +4. Push to the branch (`git push origin feature-name`) +5. Open a pull request + +--- + +## License + +This project is licensed under the [MIT License](LICENSE). + +--- + +## Contact + +- **druffko** - [@druffko](https://twitter.com/druffko) - hi@druffko.gg +- **Project Link** - https://github.com/druffko/kaelus + +Feel free to reach out if you have any questions or suggestions! + +--- diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..519d5ba --- /dev/null +++ b/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/kaelus.csproj b/kaelus.csproj new file mode 100644 index 0000000..f19f6f4 --- /dev/null +++ b/kaelus.csproj @@ -0,0 +1,25 @@ + + + WinExe + net8.0 + enable + true + app.manifest + true + Assets\kaelus-icon.ico + + + + + + + + + + + + + + + + diff --git a/kaelus.sln b/kaelus.sln new file mode 100644 index 0000000..7c0678d --- /dev/null +++ b/kaelus.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kaelus", "kaelus.csproj", "{F8CAB4F6-5FA4-4A1D-B0FD-C567E78C332D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F8CAB4F6-5FA4-4A1D-B0FD-C567E78C332D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8CAB4F6-5FA4-4A1D-B0FD-C567E78C332D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8CAB4F6-5FA4-4A1D-B0FD-C567E78C332D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8CAB4F6-5FA4-4A1D-B0FD-C567E78C332D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {198F7A01-39D3-45F1-BA2D-3B6E73B82932} + EndGlobalSection +EndGlobal