Full Code of uilicious/inboxkitten for AI

master fa1270fea685 cached
71 files
141.8 KB
42.1k tokens
23 symbols
1 requests
Download .txt
Repository: uilicious/inboxkitten
Branch: master
Commit: fa1270fea685
Files: 71
Total size: 141.8 KB

Directory structure:
gitextract_chfcm49f/

├── .dockerignore
├── .gitignore
├── .travis.yml
├── CODE-GUIDE.md
├── DEPLOY-GUIDE-LOCALHOST.md
├── DEPLOY-GUIDE-SERVERLESS.md
├── Dockerfile
├── LICENSE
├── README.md
├── api/
│   ├── app.js
│   ├── cloudflare.js
│   ├── config/
│   │   ├── cacheControl.js
│   │   ├── kittenRouterConfig.sample.js
│   │   └── mailgunConfig.sample.js
│   ├── firebase.js
│   ├── package.json
│   ├── public/
│   │   ├── .gitignore
│   │   └── _anchor.txt
│   ├── src/
│   │   ├── api/
│   │   │   ├── mailGetHtml.js
│   │   │   ├── mailGetInfo.js
│   │   │   ├── mailGetUrl.js
│   │   │   └── mailList.js
│   │   ├── app-setup.js
│   │   ├── cloudflare-api/
│   │   │   ├── KittenRouter.js
│   │   │   ├── mailGetHtml.js
│   │   │   ├── mailGetKey.js
│   │   │   ├── mailList.js
│   │   │   └── optionsHandler.js
│   │   └── mailgunReader.js
│   ├── test/
│   │   └── mailgunReader.test.js
│   └── webpack.config.js
├── build.sh
├── cli/
│   ├── Makefile
│   ├── README.md
│   ├── go.sh
│   └── src/
│       └── inboxkitten.go
├── config.sh
├── deploy/
│   ├── cloudflare/
│   │   └── deploy.sh
│   └── firebase/
│       ├── deploy.sh
│       └── firebase.json
├── docker-dev-build.sh
├── docker-entrypoint.sh
├── docker-notes.md
└── ui/
    ├── .gitignore
    ├── README.md
    ├── commonshost.js
    ├── config/
    │   ├── apiconfig.sample.js
    │   ├── carbonads.js
    │   ├── dev.env.js
    │   ├── index.js
    │   ├── prod.env.js
    │   └── shareConfig.js
    ├── index.html
    ├── package.json
    ├── src/
    │   ├── App.vue
    │   ├── components/
    │   │   ├── CarbonAds.vue
    │   │   ├── NavBar.vue
    │   │   └── mail/
    │   │       ├── inbox.vue
    │   │       ├── message_detail.vue
    │   │       └── message_list.vue
    │   ├── kittenrouter.vue
    │   ├── landingpage.vue
    │   ├── main.js
    │   ├── router/
    │   │   └── index.js
    │   ├── scss/
    │   │   ├── _color.scss
    │   │   ├── _common.scss
    │   │   ├── _producthunt.scss
    │   │   └── landingpage.scss
    │   └── store/
    │       └── emailStore.js
    ├── uilicious-test/
    │   └── inboxkitten.test.js
    └── vite.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
#
# Hidden files ignore
#
.*

#
# Remove uneeded files from docker context
#
/api/node_modules
/ui/node_modules
/ui/dist
/cli/**
/deploy/**

#
# Config files to ignore
#
/ui/config/apiconfig.js
/api/config/mailgunConfig.js


================================================
FILE: .gitignore
================================================
# Hidden files
**/.*

# lib / build folders folder
**/node_modules
**/bin
**/dist
**/gopath

# UI build folder ignore
ui/build/*

# deploy build files
deploy/firebase/functions
deploy/firebase/public
deploy/firebase/*.log

# Remove out config files
api/config/*.js
ui/config/apiconfig.js

# Include sample config files
!api/config/*.sample.js
!api/config/cacheControl.js

# Whitelist anchor files & .gitignore
!.gitignore
!.dockerignore
!.travis.yml
!**/.anchor

================================================
FILE: .travis.yml
================================================
sudo: false
language: go

# Our CLI toolchain is based on node_js
node_js: "8.10"
# Installation of subdependencies
before_install:
  # Install firebase tools
  - npm install -g firebase-tools
  # # Installing of uilicious toolchain
  # - npm install -g uilicious-cli
# Addons (for build debugging)
addons:
  apt:
    packages:
      - tree
# Building and testing on travis
script:
  - ./config.sh
  - ./build.sh
  - echo "Displaying project file tree (for easy debugging)" && tree -L 2
# The actual deploy scripts
deploy:
  - provider: script
    skip_cleanup: true
    script: cd ./deploy/firebase && ./deploy.sh --token "$FIREBASE_TOKEN" --project "$FIREBASE_PROJECT"
    on:
      branch: master
# Dependencies caching (for NPM, and GO)
cache:
  directories:
    - api/node_modules
    - ui/node_modules
    - cli/gopath


================================================
FILE: CODE-GUIDE.md
================================================
# Code Guide
If you are interested to developing Inboxkitten, this is a brief guide of how  Inboxkitten has been structured.

## Main Components
 - API - that serves as the backbone to connect to MailGun
 - UI - the user interface that you see on <a href="https://inboxkitten.com" target="_blank">Inboxkitten</a> that interacts with the API
 - CLI - the command line tool that you can use to interact with the API

### API
Under the `api` folder is where all the code resides for the API component. The API component utilizes `axios` to perform its requests to Mailgun.

- The configuration settings of the API are all located under `config/`
    - `mailgunConfig.js` is the configuration file that contains the keys and domains 
    - `mailgunConfig.sample.js` is the template that `./config.sh` used to write into `mailgunConfig.js`
- The rest of the code resides in `src/`.
    - The main point of entry in an ExpressJS setup is at `app-setup.js`. In this file, 
    - `mailgunReader.js` contains the underlying code that connects to Mailgun
    - the `api` folder will contain the code that performs the validation of the params in the endpoint that the user called before sending over to `mailgunReader.js`.
- The `test` folder contains the mocha test cases to check the `mailgunReader.js`.

To add any endpoints, it is recommended to create a prototype function in `mailgunReader.js` that performs the execution that connects to Mailgun. Following which, you should create the endpoint that user will be using as a new file under `src/api/` folder for easy maintenance.

### UI
The UI component code is under `ui` folder. It is constructed using Vue.js for its frontend development and `axios` to perform to requests to API component.

- The configuration settings of the UI are all located under `config/`
    - `apiconfig.js` contains the configuration for connecting to API component as well as the domain to display on the UI.
    - `apiconfig.sample.js` is the template used in `./config.sh` for writing into `apiconfig.js`
    - The other configuration to be concerned would be the `shareConfig.js` where it is the settings for shareable features such as Twitter's tweeting and GitHub's fork.
    - The other files are auto generated files by vue-cli.
- The `src` folder contains the body for UI. It is separated into 3 folders.
    - The `assets` will contain the images.
    - The `components` will contain the components used in the UI.
    - The `router` is an auto generated file but it is used to add subpaths to link to the components.
    - The `scss` contains the styling used for Inboxkitten's UI.
- The `dist` folder contains the files built using the `npm run build` command.
- The `uilicious-test` is an uilicious test script that can be ran on [test.uilicious.com](https://test.uilicious.com) to check if your email has been received properly.

The main entrypoint will be the `App.vue` and by default the Vue router will direct to `landingpage.vue`.


### CLI
The CLI is under the `cli` folder. There are only one file that performs the tasks to connect to the API component. It is `inboxkitten.go` under the `src` folder. The `go.sh` script is a custom go script that ensures the environment is within the `cli` folder.

================================================
FILE: DEPLOY-GUIDE-LOCALHOST.md
================================================
# Developing on localhost / Custom deployment

Note: You will still need to do the mail gun setup in the firebase guide.

Instead of running `./config.sh`, you should setup the config files respectively for the deployment.

## Running the api server

**Configuring : api/config/mailgunConfig.js**

```
module.exports = {
	"apiKey" : "<MAILGUN_API_KEY>",
	"emailDomain" : "<MAILGUN_EMAIL_DOMAIN>",
	"corsOrigin" : "http://localhost:8000"
}
```

+ MAILGUN_API_KEY : Mailgun private api key
+ MAILGUN_EMAIL_DOMAIN : domain for mailgun 
+ UI_HOST : Url to the UI domain. `http://localhost:8000` is the default for the UI `npm run dev`, 

**Running the express.js API server**

```
	# Assuming that you are on the root directory of Inboxkitten
	$ cd api
	
	# Start the server
	$ npm start
```

Validate your API server is online at `http://localhost:8000/api/v1/mail/list?recipient=hello-world`

You should see an empty array representing an empty inbox.

## Running the ui server - in development mode

**Configuring ui/config/apiconfig.js**
```
export default {
	apiUrl: 'http://localhost:8000/api/v1/mail',
	domain: '<MAILGUN_EMAIL_DOMAIN>'
}
```

+ apiUrl : Api server to point to, `localhost:8000` is the default for the api server `npm start`
+ MAILGUN_EMAIL_DOMAIN : domain for mailgun 

**Running the nodejs+webpack UI server**

```
	# Assuming that you are on the root directory of Inboxkitten
	$ cd ui
	
	# If you do not have a http server, you can install one
	$ npm run dev
```

You can now access it on `http://localhost:8000` and enjoy your kitten-ventures.

## Running cli

This is built using the `./build.sh` script

```
	# Assuming that you are on the root directory of Inboxkitten
	$ cd cli
	
	# You can immediately execute the executable
	$ ./bin/inboxkitten
	
	# Retrieving list of email
	$ ./bin/inboxkitten list exampleEmailName
	
	# Retrieving individual email
	$ ./bin/inboxkitten get sw eyJwIjpmYWxzZSwiayI6IjI3YzVkNmZkLTk5ZjQtNGY5MC1iYTM4LThiNDBhNTJmNzA1OCIsInMiOiI0NzFhZjYxYjA4IiwiYyI6InRhbmtiIn0=
	
	# Target different api endpoint
	$ ./bin/inboxkitten -api http://localhost:8000/api/v1 (list|get) [params]             
```
To run without compilation of `inboxkitten.go` in the src/ folder
```
	$ ./go.sh run src/inboxkitten.go
```

## Calling the API using curl
If you have your API running on port `8000`,
```
	# Get list of email
	$ curl localhost:8000/api/v1/mail/list\?recipient=hard-dust-64

	# Get individual email
	$ curl localhost:8000/api/v1/mail/list\?mailKey=se-eyJwIjpmYWxzZSwiayI6ImVlMWNiMTAzLWZhZjMtNDg3Ni04MjI2LWE1YmE1ZTU3YzMxMiIsInMiOiI3NTdhNTY5ZGFkIiwiYyI6InRhbmtiIn0=
```

If you have it hosted on the net, change the endpoint to where you have hosted it on :)

================================================
FILE: DEPLOY-GUIDE-SERVERLESS.md
================================================
# Serverless Deployment Guide

Follow the 5 steps guide below to get started on Firebase!

- [Step 0 - Clone Me](#step-0---clone-me)
- [Step 1 - Setup Serverless provider](#step-1---mailgun--firebase-signup)
- [Step 2 - Configuration](#step-2---configuration)
- [Step 3 - Build the package](#step-3---build-the-package)
- [Step 4 - Deployment](#step-4---deployment)

> Also do let us know how we can help make this better 😺

## Step 0 - Clone Me

```
	$ git clone https://github.com/uilicious/inboxkitten.git
```

### Step 1a - Setup Firebase

1. Go to <a href="https://firebase.google.com" target="_blank">Firebase</a> and click on `Get Started`.
2. Sign in with your favorite Google account.
3. Click on `Add Project` and create your own firebase inboxkitten project.
4. Remember the project ID  

On your local machine where your InboxKitten is located at,
```
	# Go to the root folder of InboxKitten
	$ cd <the place where you clone your inboxkitten>
	
	# Ensure that firebase CLI tool is installed
	$ npm install -g firebase-tools
	
	# Login to your firebase account
	$ firebase login
	
	# Set your firebase project
	$ firebase use --add <project name that you remembered>
```

OR

### Step 1b - Setup Cloudflare Workers

1. Go to <a href="https://cloudflare.com" target="_blank">Cloudflare</a> and signup with a domain.
2. Setup cloudflare worker and get an API key
___

## Step 2 - Configuration

In the root directory of Inboxkitten, run the following command
```
	$ ./config.sh
```

During the run time of `./config.sh`, there are three environment variables that is being used to set the configuration for your configuration files.

1. `MAILGUN_EMAIL_DOMAIN` - any custom domain that you owned or the default domain in Mailgun
2. `WEBSITE_DOMAIN`  - any custom domain that you owned. If you use your default firebase url, it will be `<Your project>.firebaseapp.com`
3. `MAILGUN_API_KEY` - retrieve the api key from your Mailgun account

<img src="./assets/configuration.png" alt="configuration" width="500px"/>

___

## Step 3 - Build the package

```
	$ ./build.sh
```

`./build.sh` will package the three components to be ready for deployment.

___

## Step 4 - Deployment

For API deployment on Firebase:

```
	# Run the deployment script
	$ ./deploy/firebase/deploy.sh 
```

For API deployment on Cloudflare:

```
	# Run the deployment script
	$ ./deploy/cloudflare/deploy.sh 
```

================================================
FILE: Dockerfile
================================================
#-------------------------------------------------------
#
# Base alpine images with all the runtime os dependencies
#
# Note that the node-sass is broken with node 12
# https://github.com/nodejs/docker-node/issues/1028
#
#-------------------------------------------------------

# Does basic node, and runtime dependencies
FROM node:14-alpine AS baseimage
RUN apk add --no-cache gettext
RUN mkdir -p /application/
# WORKDIR /application/

#-------------------------------------------------------
#
# code builders (used by dockerbuilders)
#
#-------------------------------------------------------

# Install dependencies for some NPM modules
FROM baseimage AS codebuilder
# RUN apk add --no-cache make gcc g++ python

#-------------------------------------------------------
#
# Docker builders (also resets node_modules)
#
# Note each major dependency is compiled seperately
# so as to isolate the impact of each code change
#
#-------------------------------------------------------

# Build the API
# with reseted node_modules
FROM codebuilder AS apibuilder
# copy and download dependencies
COPY api/package.json /application/api-mods/package.json
RUN cd /application/api-mods/ && npm install
# copy source code
COPY api /application/api/
RUN rm -rf /application/api/node_modules
# merge in dependnecies
RUN cp -r /application/api-mods/node_modules /application/api/node_modules
RUN ls /application/api/

# Build the UI
# with reseted node_modules
FROM codebuilder AS uibuilder
# copy and reset the code
COPY ui  /application/ui/
RUN rm -rf /application/ui/node_modules
RUN rm -rf /application/ui/dist
RUN cd /application/ui  && ls && npm install
# Lets do the UI build
RUN cp /application/ui/config/apiconfig.sample.js /application/ui/config/apiconfig.js
RUN cd /application/ui && npm run build

# Entry script 
# & Permission reset
FROM codebuilder AS entrypointbuilder
COPY docker-entrypoint.sh  /application/docker-entrypoint.sh
RUN chmod +x /application/docker-entrypoint.sh

#-------------------------------------------------------
#
# Full Docker application
#
#-------------------------------------------------------
FROM baseimage as inboxkitten

# Copy over the built files
COPY --from=apibuilder        /application/api                  /application/api
COPY --from=uibuilder         /application/ui/dist              /application/ui-dist
COPY --from=entrypointbuilder /application/docker-entrypoint.sh /application/docker-entrypoint.sh

# Debugging logging
# RUN ls /application/./
# RUN ls /application/ui-dist
# RUN ls /application/api

# Expose the server port
EXPOSE 8000

#
# Configurable environment variable
#
ENV MAILGUN_EMAIL_DOMAIN=""
ENV MAILGUN_API_KEY=""
ENV WEBSITE_DOMAIN=""

# Setup the workdir
WORKDIR "/application/"

# Setup the entrypoint
ENTRYPOINT [ "/application/docker-entrypoint.sh" ]
CMD []


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2018 Uilicious Private Limited.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
[![inboxkitten header](./ui/static/inbox-kitten-opengraph.jpg)](https://inboxkitten.com)

# Open-Source Disposable Email - Served by Serverless Kittens

[![Build Status](https://travis-ci.org/uilicious/inboxkitten.svg?branch=master)](https://travis-ci.org/uilicious/inboxkitten)

[Inboxkitten](https://inboxkitten.com) is an open-source disposable email service that you can freely deploy adopt on your own!

Visit [our site](https://inboxkitten.com) to give a spin, or ...

# Docker Deployment Guide

Its one simple line - to use our prebuilt docker container.

Note you will need to [setup your mailgun account first](#setup-mailgun)

```
# PS: you should modify this for your use case
docker run \
	-e MAILGUN_EMAIL_DOMAIN="<email-domain>" \
	-e MAILGUN_API_KEY="<api-key>" \
	-e WEBSITE_DOMAIN="localhost:8000" \
	-p 8000:8000 \
	uilicious/inboxkitten
```

And head over to port 8000 - for your inboxkitten

# Other Deployment Options

- [Serverless deployment guide (for cloudflare/firebase)](./DEPLOY-GUIDE-SERVERLESS.md)
- [localhost/custom deployment/configuration guide](./DEPLOY-GUIDE-LOCALHOST)

# Support us on product hunt 🚀

+ https://www.producthunt.com/posts/inboxkitten

# Somewhat related blog / articles

+ [The Stack : Making a free open-source disposable email service prototype (inboxkitten.com) in 14 hours](https://dev.to/picocreator/the-stack-making-a-free-open-source-disposable-email-service-prototype-inboxkittencom-in-14-hours-206g)
+ [What I have learnt from a 14 hours project](https://dev.to/jmtiong/what-i-have-learnt-from-a-14-hours-project-2joo)
+ [Development timeline](https://blog.uilicious.com/development-timeline-for-inboxkitten-com-lessons-learnt-e802a2f0a47c)

# Other References

- [Coding Guide](./CODE-GUIDE.md)

# Looking for sponsor

Note: Due to this project rather heavy traffic usage, a good half sadly spam/bot related, we are looking for a hosting sponsor / sponsor to subsidise running cost
___

## How to Setup Mailgun - and get your free API key

### Mailgun
To sign up for a Mailgun account, go to the <a href="https://signup.mailgun.com/new/signup" target="_blank">signup</a> page.

> 2021 Udpate: Inbound routing for mailgun, now requires any paid account (starting at $35/month) see : https://www.mailgun.com/pricing/

#### Custom Domain
```
	1. Click on `Add New Domain` button under your Domains panel. 
	2. Follow the steps accordingly
```
> You can use the default domain that was provided by Mailgun if you do not have your own domain.

#### Routes Configuration
After setting up your domain, in order for you to receive email, you have to configure the routes. <a href="https://documentation.mailgun.com/en/latest/quickstart-receiving.html" target="_blank">Routes</a> act as rules that will filter through all the incoming mails and execute actions on matched conditions.

In your Routes panel, simply click on `Create Route` button and follow the steps accordingly.

<img src="./assets/mailgun_create_route.png" alt="Mailgun Route" width="600px"/>

> The above route will match all names ending with `@inboxkitten.com`, store them in the storage that mailgun provides (only for 3 days) and stop processing any other rules once this route is matched. 

#### Mailgun API Key
You can locate your Mailgun API key by clicking on the domain that you are managing. In it you can see your API key.

<img src="./assets/mailgun_api_key.png" alt="Mailgun API key" width="500px"/>

Or you can go to the security settings and locate the API key there.

<img src="./assets/mailgun_api_key_2.png" alt="Mailgun API key" width="500px"/>

___


================================================
FILE: api/app.js
================================================
// Cache control settings
const cacheControl = require("./config/cacheControl");

// app package loading
let app = require("./src/app-setup");

// Setup the routes
app.get("/api/v1/mail/list",    require("./src/api/mailList"));
app.get("/api/v1/mail/getInfo", require("./src/api/mailGetInfo"));
app.get("/api/v1/mail/getHtml", require("./src/api/mailGetHtml"));

// Legacy fallback behaviour - 
// Note this is to be deprecated (after updating UI)
app.get("/api/v1/mail/getKey",  require("./src/api/mailGetInfo"));

// Static regex 
const staticRegex = /static\/(js|css|img)\/(.+)\.([a-zA-Z0-9]+)\.(css|js|png|gif)/g;

// Static folder hosting with cache control
// See express static options: https://expressjs.com/en/4x/api.html#express.static
app.use( app.express.static("public", {
	etag: true,
	setHeaders: function (res, path, stat) {
		if( staticRegex.test(path) ) {
			res.set('cache-control', cacheControl.immutable);
		} else {
			res.set('cache-control', cacheControl.static   );
		}
	}
}) )

// Custom 404 handling - use index.html
app.use(function(req, res) {
	res.set('cache-control', cacheControl.static)
	res.sendFile(__dirname + '/public/index.html');
});

// Setup the server
var server = app.listen(8000, function () {
	console.log("app running on port.", server.address().port);
});


================================================
FILE: api/cloudflare.js
================================================
/**
 * Cloudflare fetch result handling
 */
addEventListener('fetch', event => {
	event.respondWith(handleFetchEvent(event))
})

const KittenRouter = require("kittenrouter");
const kittenRouterConfig = require("./config/kittenRouterConfig") || {};
const router = new KittenRouter(kittenRouterConfig);

async function handleFetchEvent(event) {

	// Get the request object
	let req = event.request;
	// Get the request URL
	let url = new URL(req.url)

	// Does the CORS options hanlding
	if (req.method === "OPTIONS") {
		return require("./src/cloudflare-api/optionsHandler")(req)
	} 

	// Get the pathname
	let pathname = url.pathname;
	
	// Does API processing
	if(pathname === "/api/v1/mail/list"){
		return require("./src/cloudflare-api/mailList")(url)
	} else if(pathname === "/api/v1/mail/getKey") {
		return require("./src/cloudflare-api/mailGetKey")(url)
	} else if (pathname === "/api/v1/mail/getHtml") {
		return require("./src/cloudflare-api/mailGetHtml")(url)
	} else if (pathname.startsWith("/api/")) {
		// Throw an exception for invalid API endpoints
		return new Response('Invalid endpoint - ' + pathname, { status: 400, statusText: 'INVALID_ENDPOINT' });
	}

	// KittenRouter handling
	if( 
		pathname === "" || pathname === "/" ||
		pathname.startsWith("/inbox/") || 
		pathname.startsWith("/static/css") || 
		pathname.startsWith("/static/img") || 
		pathname.startsWith("/static/js")
	) {
		return router.handleFetchEvent(event);
	}
	
	// Throw an exception for invalid file request
	return new Response('Invalid filepath - ' + url.pathname, { status: 404, statusText: 'UNKNOWN_FILEPATH' });
}


================================================
FILE: api/config/cacheControl.js
================================================
/**
 * Configure the various level of cache controls
 */
module.exports = {
    // Frequent changing dynamic content
    "dynamic"  : "public, max-age=1, max-stale=5, stale-while-revalidate=10, stale-if-error=86400",
    
    // Rarely changing static content
    // with very aggressive caching
    "static"   : "public, max-age=60, max-stale=120, stale-while-revalidate=3600, stale-if-error=86400",

    // Immutable content
    "immutable": "public, max-age=36000, max-stale=72000, stale-while-revalidate=360000, stale-if-error=864000"
}

================================================
FILE: api/config/kittenRouterConfig.sample.js
================================================
//
// Routing and logging options
//
module.exports = {

	// logging endpoint to use
	log : [
		{
			// Currently only elasticsearch is supported, scoped here for future alternatives
			// One possible option is google analytics endpoint
			type : "elasticsearch",

			//
			// Elasticsearch index endpoint 
			//
			url : "https://elasticsearch-server.secret-domain.com/",

			//
			// Authorization header (if needed)
			//
			basicAuthToken : "user:pass",

			//
			// Index prefix for storing data, this is before the "YYYY.MM" is attached
			//
			indexPrefix : "test-data-",

			// Enable logging of the full ipv4/6
			//
			// Else it mask (by default) the last digit of IPv4 address
			// or the "network" routing for IPv6
			// see : https://www.haproxy.com/blog/ip-masking-in-haproxy/
			logTrueIP : false,

			// @TODO support
			// Additional cookies to log
			//
			// Be careful not to log "sensitive" cookies, that can compromise security
			// typically this would be seesion keys.
			// cookies : ["__cfduid", "_ga", "_gid", "account_id"]
		}
	],

	// Routing rules to evaluate, starting from 0 index
	// these routes will always be processed in sequence
	route : [
		// Lets load all requests to commonshost first
		"commonshost.inboxkitten.com",

		// If it fails, we fallback to firebase
		"firebase.inboxkitten.com"
	],

	// Set to true to disable fallback to origin host 
	// when all routes fails
	disableOriginFallback : false,
}

================================================
FILE: api/config/mailgunConfig.sample.js
================================================
//
// API key and valid mailgun domain supported (using sandbox)
//
module.exports = {
	"apiKey"      : "${MAILGUN_API_KEY}",
	"emailDomain" : "${MAILGUN_EMAIL_DOMAIN}",
	//"corsOrigin"  : "*"
}


================================================
FILE: api/firebase.js
================================================
/**
 * This is configured for use within firebase cloud function (or GCP cloud functions)
 * And will be automatically renamed into index.js by the build script
 * 
 * See : https://firebase.google.com/docs/functions/http-events
 */

// Dependencies loading
const functions = require('firebase-functions');
let app         = require("./src/app-setup");

// Setup the routes - for mail list / get
app.get("/api/v1/mail/list",   require("./src/api/mailList"));
app.get("/api/v1/mail/getKey", require("./src/api/mailGetKey"));
app.get("/api/v1/mail/getHtml", require("./src/api/mailGetHtml"));

// Expose the HTTP on request
exports.firebase_api_v1 = functions.https.onRequest(app);


================================================
FILE: api/package.json
================================================
{
  "name": "inboxkitten-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "axios": "^0.18.0",
    "body-parser": "^1.18.3",
    "cors": "^2.8.5",
    "express": "^4.16.3",
    "firebase-admin": "^6.0.0",
    "firebase-functions": "^2.0.5",
    "kittenrouter": "^1.0.3",
    "validator": "^10.7.0"
  },
  "devDependencies": {
    "delay": "^4.0.0",
    "mailgun-js": "^0.20.0",
    "md5": "^2.2.1",
    "mocha": "^5.2.0",
    "shortid": "^2.2.13",
    "uuid": "^3.3.2",
    "webpack": "^4.30.0",
    "webpack-cli": "^3.3.0"
  },
  "scripts": {
    "test": "mocha",
    "start": "node app.js",
    "build-cloudflare": "webpack --mode production cloudflare.js"
  },
  "author": "",
  "license": "MIT"
}


================================================
FILE: api/public/.gitignore
================================================
*
!_anchor.txt
!.gitignore

================================================
FILE: api/public/_anchor.txt
================================================
This directory is used for static file serving via the API

================================================
FILE: api/src/api/mailGetHtml.js
================================================
// Loading mailgun reader and config
const mailgunReader = require("../mailgunReader");
const mailgunConfig = require("../../config/mailgunConfig");
const cacheControl  = require("../../config/cacheControl");

const reader = new mailgunReader(mailgunConfig);

/**
 * Get and return the etatic email HTML content from the mailgun API, given the mailKey
 *
 * @param {*} req
 * @param {*} res
 */
module.exports = function(req, res){

	let region = req.query.region
	let key = req.query.key
	
	if (region == null || region === ""){
		return res.status(400).send('{ "error" : "No `region` param found" }');
	}

	if (key == null || key === ""){
		return res.status(400).send('{ "error" : "No `key` param found" }');
	}

	reader.getKey({region, key}).then(response => {
		let body = response["body-html"] || response["body-plain"]
		if( body === undefined || body == null) {
			body = 'The kittens found no messages :('
		}

		// Add JS injection to force all links to open as a new tab
		// instead of opening inside the iframe
		body += '<script>' +
			'let linkArray = document.getElementsByTagName("a");' +
			'for (let i=0; i<linkArray.length; ++i) { linkArray[i].target="_blank"; }' +
			// eslint-disable-next-line
			'<\/script>'

		res.set('cache-control', cacheControl.static)
		res.status(200).send(body)
	})
	.catch(e => {
		console.error(`Error getting mail HTML for /${region}/${key}: `, e)
		res.status(500).send("{error: '"+e+"'}")
	});
}


================================================
FILE: api/src/api/mailGetInfo.js
================================================
// Loading mailgun reader and config
const mailgunReader = require("../mailgunReader");
const mailgunConfig = require("../../config/mailgunConfig");
const cacheControl  = require("../../config/cacheControl");

const reader = new mailgunReader(mailgunConfig);

/**
 * Get and return the static email header details from the mailgun API given the mailKey
 *
 * @param {*} req
 * @param {*} res
 */
module.exports = function(req, res){

	let region = req.query.region
	let key = req.query.key
	
	if (region == null || region === ""){
		return res.status(400).send('{ "error" : "No `region` param found" }');
	}

	if (key == null || key === ""){
		return res.status(400).send('{ "error" : "No `key` param found" }');
	}
	
	reader.getKey({region, key}).then(response => {
		let emailDetails = {}

		// Format and extract the name of the user
		let [name, ...rest] = formatName(response.from)
		emailDetails.name = name

		// Extract the rest of the email domain after splitting
		if (rest[0].length > 0) {
			emailDetails.emailAddress = ' <' + rest
		}

		// Extract the subject of the response
		emailDetails.subject = response.subject

		// Extract the recipients
		emailDetails.recipients = response.recipients

		// Return with cache control
		res.set('cache-control', cacheControl.static)
		res.status(200).send(emailDetails)
	})
	.catch(e => {
		console.error(`Error getting mail metadata info for /${region}/${key}: `, e)
		res.status(500).send("{error: '"+e+"'}")
	});
}

function formatName (sender) {
	let [name, ...rest] = sender.split(' <')
	return [name, rest]
}


================================================
FILE: api/src/api/mailGetUrl.js
================================================
// Loading mailgun reader and config
const mailgunReader = require("../mailgunReader");
const mailgunConfig = require("../../config/mailgunConfig");
const cacheControl  = require("../../config/cacheControl");

const reader = new mailgunReader(mailgunConfig);

/**
 * Get and return the URL link from the mailgun API - for the mail gcontent
 * 
 * NOTE - this is to be deprecated
 *
 * @param {*} req
 * @param {*} res
 */
module.exports = function(req, res){
	let params = req.query
	let url = params.url
	if (url == null || url === ""){
		 res.status(400).send('{ "error" : "No `url` param found" }');
	}

	reader.getUrl(url).then(response => {
		res.set('cache-control', cacheControl.static)
		res.status(200).send(response)
	})
	.catch(e => {
		console.error("Error: ", error)
		res.status(500).send("{error: '"+e+"'}")
	});
}


================================================
FILE: api/src/api/mailList.js
================================================
// Loading mailgun reader and config
const mailgunReader = require("../mailgunReader");
const mailgunConfig = require("../../config/mailgunConfig");
const cacheControl  = require("../../config/cacheControl");

const reader = new mailgunReader(mailgunConfig);

/**
 * Mail listing API, returns the list of emails
 *
 * @param {*} req
 * @param {*} res
 */
module.exports = function(req, res) {
    let params = req.query;

    // recipient may be:
    // only the username, e.g. "john.doe"
    // or the full email, e.g. "john.doe@domain.com"
    let recipient = params.recipient;

    // Check if recipient is empty
    if (!recipient) {
        return res.status(400).send({ error: "No valid `recipient` param found" });
    }

    // Trim leading and trailing whitespace
    recipient = recipient.trim();

    // If recipient ends with `"@"+mailgunConfig.emailDomain`, remove it
    let pos = recipient.indexOf("@" + mailgunConfig.emailDomain);
    if (pos >= 0) {
        recipient = recipient.substring(0, pos);
    }

    // Validate recipient
    try {
        recipient = validateUsername(recipient);
    } catch (e) {
        return res.status(400).send({ error: "Invalid email" });
    }

    // Empty check
    if (!recipient) {
        return res.status(400).send({ error: "No valid `recipient` param found" });
    }

    reader.recipientEventList(recipient + "@" + mailgunConfig.emailDomain)
        .then(response => {
            res.set('cache-control', cacheControl.dynamic);
            res.status(200).send(response.items);
        })
        .catch(e => {
            console.error(`Error getting list of messages for "${recipient}":`, e);
            res.status(500).send({ error: e.toString() });
        });
};

/**
 * Strictly validate username, rejecting any username that does not conform to the standards
 * @param {*} username username to be validated
 * @returns {string} Validated username
 */
function validateUsername(username) {

    // Step 1: Trim leading and trailing whitespaces
    username = username.trim();

    // Step 2: Throw error if the sanitized string is empty
    if (username.length < 3) {
        throw new Error("Invalid email.");
    }

    // Step 2: Block the domain itself
    if(username.toLowerCase() == mailgunConfig.emailDomain) {
        throw new Error("Invalid email.");
    }

    // Step 3: Check for disallowed characters
    // Allowed characters: alphanumeric, dot (.), underscore (_), hyphen (-), plus (+)
    const disallowedChars = /[^a-zA-Z0-9._+-]/g;
    if (disallowedChars.test(username)) {
        throw new Error("Invalid email.");
    }

    // Step 4: Ensure that the username does not only contains symbols, but at least one alphanumeric character
    if (!/[a-zA-Z0-9]/.test(username)) {
        throw new Error("Invalid email.");
    }

    // Step 5: Reject problematic inputs
    if (/^[-._]+$/.test(username) || username === '-' || username === '_' || username === '.') {
        throw new Error("Invalid email: Username cannot consist solely of special characters.");
    }

    // Step 6: Check for consecutive special characters
    if (/[._-]{2,}/.test(username)) {
        throw new Error("Invalid email: Username cannot contain consecutive special characters.");
    }

    // Step 6: Ensure that the username starts and end with an alphanumeric character instead of a symbol
    if (/^[._+-]/.test(username) || /[._+-]$/.test(username)) {
        throw new Error("Invalid email.");
    }

    // Step 7: Pure numeric usernames are disallowed
    if ((/^[0-9]*$/.test(username)) == true) {
        throw new Error("Invalid email.");
    }

    // Step 8: Ensure that the username starts or end with an alphabetical character
    // This mitigate numeric iterations, but does permit numericalpha sets
    if ( /^[0-9][a-zA-Z]/.test(username) || /[a-zA-Z][0-9]$/.test(username) ) {
        // does nothing
    } else if (!(/^[a-zA-Z]/.test(username) || /[a-zA-Z]$/.test(username))) {
        throw new Error("Invalid email.");
    }

    return username;
}


================================================
FILE: api/src/app-setup.js
================================================

// Dependencies loading
const express       = require("express");
const bodyParser    = require("body-parser");
const cors          = require('cors');
const mailgunConfig = require("../config/mailgunConfig");

// Initializing the express app
const app = express();

// Allow cross site requests (for now)
app.use(cors());

// Setup JSON encoding
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Add an easy way to get the express module
app.express = express;

// Export the app module, for actual server deployment (on X)
module.exports = app;


================================================
FILE: api/src/cloudflare-api/KittenRouter.js
================================================
/**
 * KittenRouter is a utility class used to 
 * 
 * - log request, for monitoring purposes (with ip masking for GDPR)
 * - reroute fetch requests an alternative "origin" endpoint
 * 	- automatically failover on request failure / timeout
 * 	- logging of failed requests
 * 	- fallback to default cloudflare "origin" endpoint
 * 
 * And as the name implies, is a sub project for InboxKitten.
 */

/**
 * Useful various refrence documentation, on how this whole class should be implemented
 * 
 * Cloudflare refrence documentation
 * - https://blog.cloudflare.com/logs-from-the-edge/
 * - https://developers.cloudflare.com/workers/reference/cache-api/
 * - https://blog.cloudflare.com/introducing-the-workers-cache-api-giving-you-control-over-how-your-content-is-cached/
 * - https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
 * - https://support.cloudflare.com/hc/en-us/articles/202494830-Pseudo-IPv4-Supporting-IPv6-addresses-in-legacy-IPv4-applications
 * 
 * Webworker documentation (for fetch API)
 * - https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
 * - https://developer.mozilla.org/en-US/docs/Web/API/Request
 * - https://developer.mozilla.org/en-US/docs/Web/API/Response
 */

//---------------------------------------------------------------------------------------------
//
// The following is an example config object, used to setup KittenRouter
// and kinda serve as a semi-functional spec of how KittenRouter is expected to work.
//
//---------------------------------------------------------------------------------------------

/*
const exampleConfig = {

	// logging endpoint to use
	log : [
		{
			// Currently only elasticsearch is supported, scoped here for future alternatives
			// One possible option is google analytics endpoint
			type : "elasticsearch",

			//
			// Elasticsearch index endpoint 
			//
			url : "https://elasticsearch-server.secret-domain.com/",

			//
			// Authorization header (if needed)
			//
			authUser : "user",
			authPass : "pass",

			//
			// Index prefix for storing data, this is before the "YYYY.MM" is attached
			//
			indexPrefix : "test-data-",

			// Enable logging of the full ipv4/6
			//
			// Else it mask (by default) the last digit of IPv4 address
			// or the "network" routing for IPv6
			// see : https://www.haproxy.com/blog/ip-masking-in-haproxy/
			logTrueIP : false,

			// @TODO support
			// Additional cookies to log
			//
			// Be careful not to log "sensitive" cookies, that can compromise security
			// typically this would be seesion keys.
			// cookies : ["__cfduid", "_ga", "_gid", "account_id"]
		}
	],

	// Routing rules to evaluate, starting from 0 index
	// these routes will always be processed in sequence
	route : [

		// Lets load all requests to commonshost first
		"commonshost.inboxkitten.com",

		// If it fails, we fallback to firebase
		"firebase.inboxkitten.com"

		// // Object based route definitions
		// //-----------------------------------------------------------------
		// {
		// 	// "" host routing will match all
		// 	reqHost : [""],
		// 	// Routing prefix to check for, note that "" will match all
		// 	reqPrefix : [""],
		
		// 	// Origin servers to route to
		// 	host : "host-endpoint-c",
		// 	port : 443,
		// 	protocol : "https",
		// 	// Matching by country
		// 	// Country codes : https://support.cloudflare.com/hc/en-us/articles/205072537-What-are-the-two-letter-country-codes-for-the-Access-Rules-
		// 	country : [
		// 		"SG"
		// 	],
		// 	// Matching by region
		// 	region : [
		// 		"Europe"
		// 	],
		// 	// Timeout to abort request on
			
		// 	// Fetching sub options (like cache overwrite)
		// 	fetchConfig : { cf: { cacheEverything: true } }
	
		// 	// @TODO (consider support for nested object based origin decleration?)
		// 	// Might be useful for some region based routing or maybe crazier?
		// 	// Racing origin requests and terminating the "slower copy"
		// 	//-------------------------------------------------------------------------
		// }
	],

	// Set to true to disable fallback to origin host 
	// when all routes fails
	disableOriginFallback : false,

	// @TODO support default timeout to process a request in milliseconds
	// defaultOriginTimeout : 10000, // 10,000 ms = 10 s

	// @TODO crazier caching options to consider
	// - KeyValue caching (probably pointless, cost wise)
	// - In memory caching (with a limit of 500 objects + 10 kb per object?)
	// See : https://developers.cloudflare.com/workers/writing-workers/storing-data/
}
*/

//---------------------------------------------------------------------------------------------
//
// Logging internal logic
//
//---------------------------------------------------------------------------------------------

// Simple regex to validate for an ipv4
const ipv4_simpleRegex = /^[0-9a-z]{1,3}\.[0-9a-z]{1,3}\.[0-9a-z]{1,3}\.[0-9a-z]{1,3}$/i;

/**
 * Extract from a request, various possible ip properties (in cludflare) 
 * for a valid ipv4 address
 * 
 * @param {Request} request object to extract from
 * @param {Boolean} logTrueIP (defaul false) used to log unmasked ip if true
 * 
 * @return {String} ipv4 string if found, else a blank string of ""
 */
function getIPV4(request, logTrueIP = false) {
	let headers = request.headers;
	let ip = headers.get('cf-pseudo-ipv4') || headers.get('cf-connecting-ipv4') || headers.get('cf-connecting-ip') || '';
	ip = ip.trim();

	// If ipv4 validation failed, return blank
	if(ip === '' || !ipv4_simpleRegex.test(ip)) {
		return "";
	}

	// Assume that its a valid ipv4
	// return immediately if no ipmasking is done
	if(logTrueIP == false) {
		return ip;
	}

	// Time to perform ip masking
	let ip_split = ip.split(".");
	ip_split[3] = "xxx";
	return ip_split.join(".");
}

/**
 * Extract from a request, various possible ip properties (in cludflare) 
 * for a valid ipv6 address
 * 
 * @param {Request} request object to extract from
 * @param {Boolean} logTrueIP (defaul false) used to log unmasked ip if true
 * 
 * @return {String} ipv6 string if found, else a blank string of ""
 */
function getIPV6(request, logTrueIP = false) {
	let headers = request.headers;
	let ip = headers.get('cf-connecting-ipv6') || headers.get('cf-connecting-ip') || '';
	ip = ip.trim();

	// If ipv4 validation passes, return blank
	if(ip === '' || ipv4_simpleRegex.test(ip)) {
		return "";
	}

	// Assume that its an ipv6
	// return immediately if no ipmasking is done
	if(logTrueIP == false) {
		return ip;
	}

	// Time to perform ip masking
	let ip_split = ip.split(":");
	for(let i=2; i<ip_split.length; ++i) {
		ip_split[i] = "xxxx"
	}
	return ip_split.join(":");
}

// Log request with a single config map
async function logRequestWithConfigMap(logConfig, request, response, routeType, routeCount) {
	// Does nothing if logconfig is null
	if( logConfig == null || logConfig.url == null || logConfig.url.length <= 0 ) {
		return null;
	}

	// Index YYYY.MM formating
	let indexYearAndMonth = (new Date()).toISOString().substr(0,7).replace("-",".");
	let indexPrefix = logConfig.indexPrefix || "KittenRouter-log-";

	// The full POST request URL
	let fullLoggingURL = logConfig.url.trim();
	if( !fullLoggingURL.endsWith("/") ) {
		fullLoggingURL = fullLoggingURL+"/";
	}
	fullLoggingURL = fullLoggingURL+logConfig.indexPrefix+indexYearAndMonth+"/_doc/";

	// Trueip logging flag
	let logTrueIP = logConfig.logTrueIP || false;

	// The data to log
	let data = {
		'timestamp': (new Date()).toISOString(),

		'route.type':  routeType,
		'route.count': routeCount,

		'req.url':        request.url,
		'req.referer':    request.referrer || '',
		'req.method':     request.method,

		'req.ipv4':          getIPV4(request, logTrueIP),
		'req.ipv6':          getIPV6(request, logTrueIP),
		'req.host':          request.headers.get('host') || '',
		'req.user-agent':    request.headers.get('user-agent') || '',
		'req.country-code':  request.headers.get('cf-ipcountry') || '',

		'req.cf.ray':     request.headers.get('cf-ray') || '',
		'req.cf.colo':    (request.cf && request.cf.colo) || '',
		'req.tlsVersion': (request.cf && request.cf.tlsVersion) || '',
		'req.tlsCipher':  (request.cf && request.cf.tlsCipher) || '',

		'res.status':           response.status,
		'res.url':              response.url,
		'res.server':           response.headers.get('server') || '',
		'res.via':              response.headers.get('via') || '',
		'res.content-type':     response.headers.get('content-type') || '',
		'res.content-encoding': response.headers.get('content-encoding') || '',
		'res.content-length':   response.headers.get('content-length') || '',
		'res.cache-control':    response.headers.get('cache-control') || '',
		'res.cf.cache-status':  response.headers.get('cf-cache-status') || '',
		'res.cf.ray':           response.headers.get('cf-ray') || '',

		'config.logTrueIP': logTrueIP
	};

	// // Cookies to log
	// let cookieNames = logConfig.cookies;
	// if( cookieNames != null && cookieNames.length > 0 ) {
	// 	let cookieJar = request.headers.get("Cookie")
	// 	// @TODO : iterate cookies and log relevent keys
	// }

	// Lets prepare the headers
	let logHeaders = {
		'Content-Type': 'application/json',
	};

	// Lets handle authentication
	if( logConfig.basicAuthToken && logConfig.basicAuthToken.length > 0 ) {
		logHeaders["Authorization"] = "Basic "+btoa(logConfig.basicAuthToken);
	}

	// The elasticsearch POST request to perform for logging
	let logResult = await fetch(
		fullLoggingURL, {
		method: 'POST',
		body: JSON.stringify(data),
		headers: new Headers(logHeaders)
	})

	// Log the log?
	if( logConfig.consoleDebug ) {
		console.log("KittenRouter logging response", logResult);
	}

	// Return the result
	return logResult;
}

// Log request with a config array
async function logRequestWithConfigArray(configArr, request, response, routeType, routeCount) {
	// Does nothing if configArr is null
	if( configArr == null || configArr.length <= 0 ) {
		return null;
	}

	// Lets iterate the config
	let promiseArray = [];
	for(let i=0; i<configArr.length; ++i) {
		promiseArray[i] = logRequestWithConfigMap(configArr[i], request, response, routeType, routeCount);
	}

	// Return with a single promise object
	return Promise.all(promiseArray);
}

//---------------------------------------------------------------------------------------------
//
// Utility functions
//
//---------------------------------------------------------------------------------------------

/**
 * Clones a URL object, with an origin string
 * 
 * @param {URL} inURL to clone over
 * @param {String} originHostStr to overwrite host with
 * 
 * @return {URL} cloned URL object with new origin host
 */
function cloneUrlWithNewOriginHostString(inURL, originHostStr) {
	let ret = new URL(inURL);
	ret.host = originHostStr;
	return ret;
}

/**
 * Generate a Response object, containing an error
 * @param {String} errorCode for the error
 * @param {String} errorMsg containing error details
 * @param {int} httpCode (default=500) for response object 
 */
function setupResponseError(errorCode, errorMsg, httpCode = 500) {
	let ret = new Response(
		JSON.stringify({
			error: {
				code : errorCode,
				message : errorMsg
			}
		}),
		{ 
			status: httpCode, 
			statusText: errorCode, 
			headers: {
				"Content-Type": "application/json",
				//"KittenRouterException": "true"
			}
		}
	);
	ret._isKittenRouterException = true;
	return ret;
}

/**
 * Lets do a oversimplified check if a response object is valid
 * if the response code is 200~399
 * 
 * @param {*} resObj 
 * 
 * @return true if its a valid response object
 */
function isGoodResponseObject(resObj) {
	return resObj != null && resObj.status >= 200 && resObj.status < 400;
}

/**
 * Check if the given response object is a KittenRouter exception
 * @param {Response} resObj to validate
 * 
 * @return true if its a KittenRouter exception 
 */
function isKittenRouterException(resObj) {
	return resObj != null && resObj._isKittenRouterException;
	// resObj.headers && resObj.headers.get("KittenRouterException") == "true";
}

//---------------------------------------------------------------------------------------------
//
// Routing internal logic
//
//---------------------------------------------------------------------------------------------

/**
 * Makes a request, with a different origin host
 * 
 * @param {String} originHostStr to overwrite host with
 * @param {Request} inRequest to use 
 * 
 * @return {Response} object of the request
 */
// Process a routing request, and return its response object
async function processOriginRoutingStr(originHostStr, inRequest) {
	return fetch( //
		cloneUrlWithNewOriginHostString(inRequest.url,originHostStr), //
		inRequest //
	);
}

// // Process a routing request, and return its response object
// async function processOriginRouting(origin, inRequest) {
// 	if( (typeof origin) === "string" ) {
// 		return processOriginRoutingStr(origin, inRequest);
// 	}
// 	throw "Object based routing config is NOT yet supported";
// }

/**
 * Process a request, and perform the required route request and logging
 * This DOES NOT handle the fetch fallback
 * 
 * @param {Object} configObj conataining both the .route, and .log array config
 * @param {*} fetchEvent provided from cloudflare, attaches logging waitUntil
 * @param {Request} inRequest to process 
 * 
 * @return {Response} if a valid route with result is found, else return final route request failure (if any), else return null
 */
async function processRoutingRequest( configObj, fetchEvent, inRequest ) {
	// Lets get the route, and log array first
	let routeArray = configObj.route;
	let logArray = configObj.log;

	// Return null, on empty routeArray
	if( routeArray == null || routeArray.length <= 0 ) {
		return null;
	}

	// setup the variable for response object
	let resObj = null;

	// Lets iterate the routes
	for( let i=0; i<routeArray.length; ++i ) {
		let route = routeArray[i];

		// Route string processing
		if( (typeof route) === "string" ) {
			// Lets handle string origins
			resObj = await processOriginRoutingStr( route, inRequest );

			// Lets log 
			fetchEvent.waitUntil( logRequestWithConfigArray( logArray, inRequest, resObj, "ROUTE_REQUEST", i) );

			// If its a valid response, return it
			if( isGoodResponseObject(resObj) ) {
				return resObj;
			}

			// Lets continue to next route
			continue;
		}

		// Throw an exception on an unknown route type
		return setupResponseError("UNKNOWN_CONFIG", "Object based route config is not yet supported");
	}

	return resObj;
}

/**
 * Process the fetch event as it comes from cloudflare
 * 
 * @param {Object} configObj for the KittenRouter.config
 * @param {*} fetchEvent provided from cloudflare
 * 
 * @return {Response} if a valid route with result is found, else the last route error (if applicable)
 */
async function processFetchEvent( configObj, fetchEvent ) {
	// Lets unpack out the request
	let inReq = fetchEvent.request;
	let resObj = null;

	// Lets process the routing request
	//----------------------------------------------------------------------

	// Lets try to get a response from a route
	resObj = await processRoutingRequest( configObj, fetchEvent, inReq );

	// Lets return the response object if its valid
	// We do an oversimilified assumption that its valid 
	// if the response code is 200~399
	if( isGoodResponseObject(resObj) ) {
		return resObj;
	}

	// Throw and show results from setupResponseError (for direct feedback loop)
	if( isKittenRouterException(resObj) ) {
		return resObj;
	}

	// At this point all routes are assumed to have failed
	// As such we will attempt to do a fallback to the default origin
	//----------------------------------------------------------------------

	// Origin fallback disabled, return last response, or a hard error
	if( configObj.disableOriginFallback ) {
		// No response object returned by routes : assume no valid routes
		if( resObj == null ) {
			return setupResponseError("NO_VALID_ROUTE", "No valid route found in config");
		}

		// Else return the last failed route response
		return resObj;
	}

	// Lets fetch the cloudflare origin request, log it, and return its result instead
	resObj = await fetch(inReq);
	fetchEvent.waitUntil( logRequestWithConfigArray( configObj.log, inReq, resObj, "ORIGIN_FALLBACK", -1) );
	return resObj;
}

//---------------------------------------------------------------------------------------------
//
// Class implementation (and public interface)
//
//---------------------------------------------------------------------------------------------

class KittenRouter {
	
	/**
	 * Setup KittenRouter instance with the given config
	 * 
	 * @param inConfig for configuring KittenRouter
	 */
	constructor(inConfig) {
		this.config = inConfig || {}; // fallback for blank object (make certain things easier)
	}

	/**
	 * Request handling function, and routing
	 * 
	 * @param  fetchEvent from cloudflare to process
	 * 
	 * @return response object for cloudflare
	 */
	async handleFetchEvent(fetchEvent) {
		return await processFetchEvent(this.config, fetchEvent);
	}
}

// Export out the KittenRouter class, if possible
// Skipped if used directly in cloudflare worker
module.exports = KittenRouter;

//---------------------------------------------------------------------------------------------
//
// Quick and dirty sample implementation (for cloudflare debugging)
//
//---------------------------------------------------------------------------------------------

/*
//
// If module does not exist, this is probably a cloudflare debugging session
// Lets do this =)
//
if( this.module == null ) {
	// KittenRouter setup
	const router = new KittenRouter({

	 // logging endpoint to use
	 log : [
		{
		  // Currently only elasticsearch is supported, scoped here for future alternatives
		  // One possible option is google analytics endpoint
		  type : "elasticsearch",

			//
			// Elasticsearch index endpoint 
			//
		  url : "https://inboxkitten.logging.com/",

			//
			// Authorization header (if needed)
			//
			basicAuthToken : "elasticsearch:password",

			//
			// Index prefix for storing data, this is before the "YYYY.MM" is attached
			//
			indexPrefix : "test-data-",

			// Enable logging of the full ipv4/6
			//
			// Else it mask (by default) the last digit of IPv4 address
			// or the "network" routing for IPv6
			// see : https://www.haproxy.com/blog/ip-masking-in-haproxy/
			logTrueIP : false,

			// @TODO support
			// Additional cookies to log
			//
			// Be careful not to log "sensitive" cookies, that can compromise security
			// typically this would be seesion keys.
			// cookies : ["__cfduid", "_ga", "_gid", "account_id"]
		}
	 ],

		route: [
			"commonshost.inboxkitten.com",
			"firebase.inboxkitten.com"
		]
	});

	// Cloudflare fetch result handling
	addEventListener('fetch', event => {
		event.respondWith(router.handleFetchEvent(event))
	});
}
*/

================================================
FILE: api/src/cloudflare-api/mailGetHtml.js
================================================
/**
 * Making a curl request that looks like
 * curl -X POST --data 'key=world' example.com
 * or
 * curl -X POST --form 'key=world' example.com
 */

let config = require("../../config/mailgunConfig")

module.exports = async function(url) {
    let mailKey = url.searchParams.get('mailKey')
	if (mailKey == null || mailKey === ""){

		return new Response("{error: 'No `mailKey` param found'}",
			{ status: 400, statusText: 'INVALID_PARAMETER', headers: {
				"Content-Type": "application/json"
			} 
		});
	}

	// Setup the authentication option object
    let authenticationKey = this.btoa("api:" + config.apiKey);

	let _authOption = {
		headers: {
			"Authorization" : "BASIC " + authenticationKey
		}
	};

	try {
		// Initialize the variables
		let prefix, key;
			
		// Lets check for newer key format
		if( mailKey.length > 37 ) {
			// Handle newer key format
			let pt = fullKey.lastIndexOf("-", fullKey.length - 36);
			prefix = fullKey.slice(0,pt);
			key = fullKey.slice(pt+1);
		} else {
			// Fallback to original logic
			let pt = fullKey.lastIndexOf("-");
			prefix = fullKey.slice(0,pt);
			key = fullKey.slice(pt+1);
		}
		
        // slice the mailgunApi to include the region
        let apiUrl = "https://api.mailgun.net/v3"
        apiUrl = apiUrl.replace("://", "://"+prefix+".")
        let urlWithParams = apiUrl+"/domains/" + config.emailDomain + "/messages/"+key;
        const response = await fetchGet(urlWithParams, _authOption);
        
        let body = response["body-html"] || response["body-plain"]
		if( body === undefined || body == null) {
			body = 'The kittens found no messages :('
		}

		// Add JS injection to force all links to open as a new tab
		// instead of opening inside the iframe
		body += '<script>' +
			'let linkArray = document.getElementsByTagName("a");' +
			'for (let i=0; i<linkArray.length; ++i) { linkArray[i].target="_blank"; }' +
			// eslint-disable-next-line
			'<\/script>'

		let responseInit = {
			headers: {
				"Content-Type": "text/html"
				//@TODO : Consider caching here?
			}
		}
		return new Response(body, responseInit)
	} catch (err) {
		return new Response("{error: '"+err+"'}",
			{ status: 400, statusText: 'INVALID_PARAMETER', headers: {
				"Content-Type": "application/json"
			} 
		});
	}
}


/**
* Simple fetch get, with response data
* @param {String} urlWithParams
* @param {Object} options
*/
var fetchGet = function(urlWithParams, options){
	return new Promise(function(resolve, reject){
		fetch(urlWithParams, options).then(response => {
			resolve(response.json())
		}).catch(e => {
			reject(e)
		})
	})
}


================================================
FILE: api/src/cloudflare-api/mailGetKey.js
================================================

/**
 * Making a curl request that looks like
 * curl -X POST --data 'key=world' example.com
 * or
 * curl -X POST --form 'key=world' example.com
 */
let config = require("../../config/mailgunConfig")

module.exports = async function(url) {
    let mailKey = url.searchParams.get('mailKey')
	if (mailKey == null || mailKey === ""){
		return new Response("{error: 'No `mailKey` param found'}",
			{ status: 400, statusText: 'INVALID_PARAMETER', headers: {
				"Content-Type": "application/json"
			} 
		});
	}

	// Setup the authentication option object
    let authenticationKey = this.btoa("api:" + config.apiKey);

	let _authOption = {
		headers: {
			"Authorization" : "BASIC " + authenticationKey
		}
	};

	try {
		// Initialize the variables
		let prefix, key;
			
		// Lets check for newer key format
		if( mailKey.length > 37 ) {
			// Handle newer key format
			let pt = fullKey.lastIndexOf("-", fullKey.length - 36);
			prefix = fullKey.slice(0,pt);
			key = fullKey.slice(pt+1);
		} else {
			// Fallback to original logic
			let pt = fullKey.lastIndexOf("-");
			prefix = fullKey.slice(0,pt);
			key = fullKey.slice(pt+1);
		}
		
        // slice the mailgunApi to include the region
        let apiUrl = "https://api.mailgun.net/v3"
        apiUrl = apiUrl.replace("://", "://"+prefix+".")
        let urlWithParams = apiUrl+"/domains/" + config.emailDomain + "/messages/"+key;
		const response = await fetchGet(urlWithParams, _authOption);

        let emailDetails = {}

        // Format and extract the name of the user
        let [name, ...rest] = formatName(response.from)
        emailDetails.name = name

        // Extract the rest of the email domain after splitting
        if (rest[0].length > 0) {
            emailDetails.emailAddress = ' <' + rest
        }

        // Extract the subject of the response
        emailDetails.subject = response.subject

        // Extract the recipients
        emailDetails.recipients = response.recipients

		let responseInit = {
			headers: {
				"Content-Type": "application/json"
			}
		}
		return new Response(JSON.stringify(emailDetails), responseInit)
	} catch (err) {
		return new Response("{error: '"+err+"'}",
			{ status: 400, statusText: 'INVALID_PARAMETER', headers: {
				"Content-Type": "application/json"
			} 
		});
	}
}


/**
* Simple fetch get, with response data
* @param {String} urlWithParams
* @param {Object} options
*/
let fetchGet = function(urlWithParams, options){
	return new Promise(function(resolve, reject){
		fetch(urlWithParams, options).then(response => {
			resolve(response.json())
		}).catch(e => {
			reject(e)
		})
	})
}

function formatName (sender) {
	let [name, ...rest] = sender.split(' <')
	return [name, rest]
}


================================================
FILE: api/src/cloudflare-api/mailList.js
================================================
/**
 * Making a curl request that looks like
 * curl -X POST --data 'key=world' example.com
 * or
 * curl -X POST --form 'key=world' example.com
 */
let config = require("../../config/mailgunConfig")

module.exports = async function(url) {
	let recipient = url.searchParams.get('recipient')
	if (recipient == null){
		return new Response("{error: 'No `recipient` param found'}",
			{ status: 400, statusText: 'INVALID_PARAMETER', headers: {
				"Content-Type": "application/json"
			} 
		});
	}

	// strip off all @domain if there is any
	if(recipient.indexOf("@") >= 0){
		recipient = recipient.substring(0, recipient.indexOf("@"))
	}

	// Setup the authentication option object
	let authenticationKey = this.btoa("api:" + config.apiKey);

	let _authOption = {
		headers: {
			"Authorization" : "BASIC " + authenticationKey
		}
	};

	try {
		const postData = await fetchGet("https://api.mailgun.net/v3/" + config.emailDomain + "/events?recipient="+recipient+"@"+config.emailDomain, _authOption);
		let responseInit = {
			headers: {
				"Content-Type": "application/json"
			}
		}
		return new Response(JSON.stringify(postData.items), responseInit)
	} catch (err) {
		return new Response("{error: '"+err+"'}",
			{ status: 400, statusText: 'INVALID_PARAMETER', headers: {
				"Content-Type": "application/json"
			} 
		});
	}
}


/**
* Simple fetch get, with response data
* @param {String} urlWithParams
* @param {Object} options
*/
var fetchGet = function(urlWithParams, options){
	return new Promise(function(resolve, reject){
		fetch(urlWithParams, options).then(response => {
			resolve(response.json())
		}).catch(e => {
			reject(e)
		})
	})
}


================================================
FILE: api/src/cloudflare-api/optionsHandler.js
================================================
/**
 * CORS Options handling for cloudflare api
 */
const config = require("../../config/mailgunConfig")

const corsHeaders = {
	"Access-Control-Allow-Origin": config.corsOrigin,
	"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
	"Access-Control-Allow-Headers": "Content-Type",
}

module.exports = async function optionsHandler(request) {
	// Handle CORS pre-flight request.
	if (
		request.headers.get("Origin") !== null &&
		request.headers.get("Access-Control-Request-Method") !== null &&
		request.headers.get("Access-Control-Request-Headers") !== null
	) {
		return new Response(null, {
			headers: corsHeaders
		});
	} 

	// Handle standard OPTIONS request.
	return new Response(null, {
		headers: {
			"Allow": "GET, POST, OPTIONS",
		}
	});
}

================================================
FILE: api/src/mailgunReader.js
================================================
// AXIOS dependencies
const axios = require("axios");

/**
* Simple axois get, with response data
* @param {String} urlWithParams
* @param {Object} options
*/
var axiosGet = function(urlWithParams, options){
	return new Promise(function(resolve, reject){
		// console.log(urlWithParams);
		axios.get(urlWithParams, options).then(response => {
			resolve(response.data)
		}).catch(e => {
			// console.log(e);
			reject(e)
		})
	})
}

/**
* Simple MailgunApi accessor class for reading event stream, and saved emails
*
* Example usage
* ```
* let reader = new mailgunReader( { apiKey:"api-*****", emailDomain:"inboxkitten.com" })
*
* // Returns a list of email recieve events
* reader.recipientEventList("some-domain.inboxkitten.com");
*
* // Get and return the email json
* reader.getRecipentEmail("some-email-id");
* ```
*/
let mailgunReader = function mailgunReader(config) {

	// The config object being used
	this._config = config;

	// Validate the config for required parameters
	if( this._config.apiKey == null || this._config.apiKey.length <= 0 ) {
		throw new Error("Missing config.apiKey");
	}
	if( this._config.emailDomain == null || this._config.emailDomain.length <= 0 ) {
		throw new Error("Missing config.emailDomain");
	}

	// Default mailgun domain if not used
	this._config.mailgunApi = this._config.mailgunApi || "https://api.mailgun.net/v3";

	// Setup the authentication option object
	this._authOption = {
		auth: {
			username : "api",
			password : this._config.apiKey
		}
	};
}

/**
 * Validate the request email against list of domains
 *
 * @param {String} email
 */
mailgunReader.prototype.recipientEmailValidation = function recipientEmailValidation(email) {
	// @TODO - the validation
	return true;
}

/**
 * Get and return a list of email events
 *
 * See : https://documentation.mailgun.com/en/latest/api-events.html#event-structure
 *
 * @param {String} email
 *
 * @return  Promise object, returning list of email events
 */
mailgunReader.prototype.recipientEventList = function recipientEventList(email) {
	// Validate email format
	if( !this.recipientEmailValidation(email) ) {
		return Promise.reject("Invalid email format : "+email);
	}

	// Compute the listing url
	let urlWithParams = this._config.mailgunApi+"/"+this._config.emailDomain+"/events?recipient="+email;

	// Lets get and return it with a promise
	return axiosGet(urlWithParams, this._authOption);
}

/**
 * Validate the url parameter for a valid mailgun api URL.
 * This is to safeguard the getURL from api key leakage
 *
 * @param {String} url
 */
mailgunReader.prototype.getUrlValidation = function getUrlValidation(email) {
	// @TODO - the validation
	return true;
}

/**
 * Get the content of URL and return it, using the mailgun key.
 * This is useful for stored emails returned by the event stream.
 *
 * @param {String} url
 */
mailgunReader.prototype.getUrl = function getUrl(url) {
	// Validate the URL
	if( !this.getUrlValidation(url) ) {
		return Promise.reject("Invalid getUrl request : "+url);
	}

	// Lets get and return it with a promise
	return axiosGet(url, this._authOption);
}

/**
 * Get the content of URL and return it, using the mailgun key.
 * This is useful for stored emails returned by the event stream.
 *
 * @param {String} url
 */
mailgunReader.prototype.getKey = function getKey({region, key}) {

	// Inject the region to the mailgunApi
	let apiUrl = this._config.mailgunApi
	apiUrl = apiUrl.replace("://", "://storage-" + region + ".")
	let urlWithParams = apiUrl + "/domains/" + this._config.emailDomain + "/messages/" + key;
	
	// Lets get and return it with a promise
	return axiosGet(urlWithParams, this._authOption);
}

// Export the mailgunReader class
module.exports = mailgunReader;


================================================
FILE: api/test/mailgunReader.test.js
================================================
// Dependencies loading
const assert  = require('assert');
const delay   = require('delay');
const md5     = require('md5');
const uuidv4  = require('uuid/v4');
const shortid = require('shortid');

// MailgunReader class
const mailgunReader = require("../src/mailgunReader");
const mailgunConfig = require("../config/mailgunConfig");

// MailgunReader instance
const reader = new mailgunReader(mailgunConfig);

// Does the test suite
describe('mailgunReader', function() {

	//
	// Testing for valid "404" errors
	//
	describe('empty-guid-inbox-requests', function() {
		// when using a uuid (unless collision occured D=)
		it('should return empty event list', async () => {
			
			// Get the id to validate
			let id = md5(uuidv4());
			assert.notEqual(id, null);

			// Get the list of emails (as item)
			let eventListObj = await reader.recipientEventList(id+"@"+mailgunConfig.emailDomain);
			assert.notEqual(eventListObj, null);
			assert.equal( eventListObj.items.length , 0);
		});
	});

	//
	// Testing the sending, listing and reading of emails
	//
	describe('send-list-recieve', function() {
		// Get the emails to send and recieve from
		let sender = md5(uuidv4())+"@"+mailgunConfig.emailDomain;
		let reciever = md5(uuidv4())+"@"+mailgunConfig.emailDomain;
		let emailContent = md5(uuidv4());

		// Test timeout to use
		let thirtySeconds = 30 * 1000;

		// Sending of email
		it('sending-of-email', async () => {

			// Initialize the mailgun sender and its data
			let mailgunSender = require('mailgun-js')({apiKey: mailgunConfig.apiKey, domain: mailgunConfig.emailDomain});
			let data = {
				from : sender,
				to : reciever,
				subject : "Testing keys",
				text : emailContent
			}

			// Send the email in a promise
			let sendPromise = new Promise(function(good,bad) {
				mailgunSender.messages().send(data,function(error, body) {
					if( error ) {
						console.log(error);
						bad(error);
					} else {
						good(body);
					}
				});
			});

			// Wait for sending to complete
			let sendPromiseResult = await sendPromise;
		}).timeout(thirtySeconds);

		// The email event to use
		let emailEvent = null;

		// Listing of email
		it('listing-of-email', async () => {

			// Loop and get email event (there might be significant time delay)
			while(emailEvent == null) {
				// Lets not spam
				await delay(2500);

				// Get the list of emails (as item)
				let eventListObj = await reader.recipientEventList(reciever);
				assert.notEqual(eventListObj, null);

				// Continue loop if length is 0
				if( eventListObj.items.length == 0 ) {
					continue;
				}

				// Get the email event
				//assert.equal(eventListObj.items.length, 1);
				emailEvent = eventListObj.items[0];
				assert.notEqual(emailEvent, null);
			}

		}).timeout(thirtySeconds * 5);

		// Listing of email
		it('reading-of-email', async () => {
			assert.notEqual(emailEvent, null);

			// Get the email URL
			let emailUrl = emailEvent.storage.url;
			assert.notEqual(emailUrl, null);
			
			// Lets get the email content
			let content = await reader.getUrl(emailUrl);
			assert.notEqual(content, null);
			
			// Assert content exists with GUID, 
			// which is definitive proof that the test work,
			// unless multiple suns started exploding
			assert.equal( content["stripped-html"].indexOf(emailContent) >= 0, true );
		});
	});


});

================================================
FILE: api/webpack.config.js
================================================
module.exports = {
	optimization: {
		// We no not want to minimize our code.
		minimize: false
	}
}

================================================
FILE: build.sh
================================================
#!/bin/bash

# Deploy will terminate on any error
set -e

# Project directory detection
projectDir="`dirname \"$0\"`"
cd "$projectDir" || exit 1
projectDir="`pwd`"
echo ">> Assuming project directory of : $projectDir"

# Build the UI + CLI
echo ">> Building the UI (NPM install + run build)"
cd "$projectDir/ui"
npm install;
npm run build;

# Build the API
echo ">> Building the API (NPM install)"
cd "$projectDir/api"
npm install;
# npm run build;

# Build the CLI
echo ">> Building the CLI"
cd "$projectDir/cli"
make build;


================================================
FILE: cli/Makefile
================================================
# Change this to your respective main file
mainfile="inboxkitten.go"
outfile="inboxkitten"

# The go build command
dependencies:
	./go.sh get ./src/...

build:
	mkdir -p bin
	./go.sh build -o "bin/$(outfile)" "src/$(mainfile)" 

clean:
	rm -fr ./bin/*
	./go.sh clean


================================================
FILE: cli/README.md
================================================
# What is go.sh

> TLDR: Its a bash script that "rewrites" the `$GOPATH` to the folder holding `go.sh` sub `gopath` folder, and configuring `GOBIN` within it.

For the uninformed, `$GOPATH` is the global environment variable in which GO compile its projects and dependencies. Which by default is a shared user directory,
which is suppose to hold _all_ your, dependencies, and project code. This is great.... assuming all you go code works in harmony.

However the world is not a perfect harmony, something as simple as trying to clone an older version of the same project for debugging, while keeping the newer copy.
And having a 100 different projects in the same workspace clashing with one another. Becomes a huge chore quickly. 

With `go.sh` this whole project repository more self contained, and its files and build function like most other software projects (NPM, ant, gradle ...)
Down to having duplicate dependencies of possibly different versions across multiple projects.

So yea _insert swear word_ I hate `$GOPATH`

For summary on issue

+ https://www.reddit.com/r/golang/comments/7h02zn/whats_the_point_of_gopath/
+ https://www.reddit.com/r/golang/comments/40ps8k/really_wrestling_with_gopath/
+ https://news.ycombinator.com/item?id=14763493

Alternative Workarounds

+ https://github.com/getstream/vg

# Help : I do not have go installed.

Unfortunately, installing go is frankly kinda a multistep pain.
See the guides for more details.

For ubuntu : https://www.digitalocean.com/community/tutorials/how-to-install-go-1-6-on-ubuntu-16-04
For macos  : https://ahmadawais.com/install-go-lang-on-macos-with-homebrew/

For ubuntu you probably would need `sudo apt-get install build-essential` for make file support as well

================================================
FILE: cli/go.sh
================================================
#!/bin/bash

# Working directory
workingDir="`dirname \"$0\"`"
cd "$workingDir" || exit 1

# GOPATH overwrite
WORKING_PWD=`pwd $workingDir`
mkdir -p "$WORKING_PWD/gopath/src"
export GOPATH="$WORKING_PWD/GOPATH"
export GOBIN="$GOPATH/bin"

# Execute go with all other arguments
go $@


================================================
FILE: cli/src/inboxkitten.go
================================================
package main

//---------------------------------------------------------------------------------------
//
// Dependencies import
//
//---------------------------------------------------------------------------------------

import (
	"flag"
	"fmt"
	"os"
	"io/ioutil"
	"net/http"
	"time"
	"log"
	"bytes"
	"encoding/json"
	// "github.com/hokaccha/go-prettyjson"
)

//---------------------------------------------------------------------------------------
//
// Utility functions
//
//---------------------------------------------------------------------------------------

func jsonPrettifier( raw string ) string {
	buf := new(bytes.Buffer)
	json.Indent(buf, []byte(raw), "", "  ")
	return buf.String()
}

//---------------------------------------------------------------------------------------
//
// Http Request functions
//
//---------------------------------------------------------------------------------------

func doGetRequest( url string ) []byte {
	client := http.Client{
		Timeout: time.Second * 2,
	};

	req, err := http.NewRequest(http.MethodGet, url, nil);
	if err != nil {
		log.Fatal(err);
	}

	res, getErr := client.Do(req);
	if getErr != nil {
		log.Fatal(getErr);
	}
	
	body, readErr := ioutil.ReadAll(res.Body);
	if readErr != nil {
		log.Fatal(readErr);
	}

	return body;
}

//---------------------------------------------------------------------------------------
//
// Main CLI functions
//
// Note most of the CLI coding was done with referencing to :
//
// https://gobyexample.com/command-line-arguments
// https://gobyexample.com/command-line-flags
// https://blog.rapid7.com/2016/08/04/build-a-simple-cli-tool-with-golang/
//
//---------------------------------------------------------------------------------------

//
// Main CLI
//
func main() {

	// The api url with default value
	var apiDefault = "https://us-central1-ulicious-inbox-kitten.cloudfunctions.net/api-v1"
	var api string
	flag.StringVar(&api, "api", apiDefault, "URL to inbox kitten API")

	// Parse all the flags
	flag.Parse();
	
	// Output the api URL if its custom
	if( api != apiDefault ) {
		fmt.Printf("Using Custom API: %s \n", api)
	}

	// Post flag processing args
	var flagArgs = flag.Args();

	// Verify that a subcommand has been provided
	// flagArgs[0] is the main command
	// flagArgs[1] will be the subcommand
	var missingCommandError = "`list [email]` or `get [emailid]` subcommand is required\n";
	if len(flagArgs) <= 0 {
		fmt.Fprintf(os.Stderr, missingCommandError);
		flag.PrintDefaults();
		os.Exit(1);
	}

	// The list and get command
	getCommand := flag.NewFlagSet("get", flag.ExitOnError)
	listCommand := flag.NewFlagSet("list", flag.ExitOnError)

	// Switch on the subcommand
	// Parse the flags for appropriate FlagSet
	// FlagSet.Parse() requires a set of arguments to parse as input
	// flagArgs[2:] will be all arguments starting after the subcommand at flagArgs[1]
	switch flagArgs[0] {
		case "list":
			listCommand.Parse(flagArgs[1:])
		case "get":
			getCommand.Parse(flagArgs[1:])
		default:
			fmt.Fprintf(os.Stderr, missingCommandError);
			flag.PrintDefaults();
			os.Exit(1);
	}

	//
	// Processing of LIST command
	//
	if listCommand.Parsed() {
		var listArgs = listCommand.Args();
		if len(listArgs) <= 0 {
			fmt.Fprintf(os.Stderr, "`list [email]` missing email parameter\n");
			os.Exit(1);
		}

		var email = listArgs[0];
		
		var urlWithParams = api+"/mail/list?recipient="+email
		var body = doGetRequest(urlWithParams);
		fmt.Println( jsonPrettifier( string(body) ) );
	}

	//
	// Processing of GET command
	//
	if getCommand.Parsed() {
		var getArgs = getCommand.Args();
		if len(getArgs) <= 1 {
			fmt.Fprintf(os.Stderr, "`get [region] [key]` missing region and key parameter\n");
			os.Exit(1);
		}

		var region = getArgs[0];
		var key = getArgs[1];

		var urlWithParams = api+"/mail/getKey?mailKey="+region+"-"+key;
		var body = doGetRequest(urlWithParams);

		fmt.Println( jsonPrettifier( string(body) ) );
	}
}



================================================
FILE: config.sh
================================================
#!/bin/bash

# Deploy will terminate on any error
set -e

# Firebase Working directory
projectDir="`dirname \"$0\"`"
cd "$projectDir" || exit 1
projectDir="`pwd`"

#
# Scanning and installing dependencies
# (Assuming a mac)
#
if [ -z "$(which envsubst)" ]; then
	if [ -z "$(which brew)" ]; then
		echo ">> envsubst not detected : please install =["
	else
		echo ">> envsubst not detected : using brew to install"
		brew install gettext
		brew link --force gettext 
	fi
fi
if [ -z "$(which npm)" ]; then
	echo ">> NPM not detected : please install =["
	exit 1;
fi
if [ -z "$(which node)" ]; then
	echo ">> node not detected : please install =["
	exit 1;
fi
if [ -z "$(which go)" ]; then
	echo ">> go not detected : please install =["
	exit 1;
fi

#
# Getting the various configuration settings from command line / environment variable
#
if [ -z "$MAILGUN_EMAIL_DOMAIN" ]; then
	echo ">> Please type in your MAILGUN_EMAIL_DOMAIN (eg: inboxkitten.com)";
	read -p '>> MAILGUN_EMAIL_DOMAIN : ' MAILGUN_EMAIL_DOMAIN;
else
	echo ">> Detected MAILGUN_EMAIL_DOMAIN env variable : $MAILGUN_EMAIL_DOMAIN";
fi

if [ -z "$WEBSITE_DOMAIN" ]; then
	echo ">> Please type in your WEBSITE_DOMAIN (eg: inboxkitten.com)";
	read -p '>> WEBSITE_DOMAIN : ' WEBSITE_DOMAIN;
else
	echo ">> Detected WEBSITE_DOMAIN env variable : $WEBSITE_DOMAIN";
fi

if [ -z "$MAILGUN_API_KEY" ]; then
	echo ">> Please type in your MAILGUN_API_KEY";
	read -sp '>> MAILGUN_API_KEY : ' MAILGUN_API_KEY;
	echo "";
else
	echo ">> Detected MAILGUN_API_KEY env variable : [intentionally redacted]";
fi

#
# Exporting variables, for envsubst support
#
export MAILGUN_EMAIL_DOMAIN="$MAILGUN_EMAIL_DOMAIN"
export MAILGUN_API_KEY="$MAILGUN_API_KEY"
export WEBSITE_DOMAIN="$WEBSITE_DOMAIN"

#
# Applying the configuration
#
echo ">> Applying config settings"
cat "$projectDir/api/config/mailgunConfig.sample.js" | envsubst > "$projectDir/api/config/mailgunConfig.js"
cat "$projectDir/ui/config/apiconfig.sample.js" | envsubst > "$projectDir/ui/config/apiconfig.js"

================================================
FILE: deploy/cloudflare/deploy.sh
================================================
#!/bin/bash

# Deploy will terminate on any error
set -e

# cloudflare Working directory
cloudflareDir="`dirname \"$0\"`"
cd "$cloudflareDir" || exit 1
cloudflareDir="`pwd`"

# Project directory detection
projectDir="$(cd "$cloudflareDir/../.."; pwd)"
echo ">> Assuming project directory of : $projectDir"

# Clearing out cloudflare public / functions folder
rm -rf "$cloudflareDir/dist/"; 
mkdir -p "$cloudflareDir/dist/";

# Transfering files into fire base deploy folder
echo ">> Preparing build files for api cloudflare upload"
cd "$projectDir/api/"
npm run build-cloudflare
cp -a "$projectDir/api/dist/." "$cloudflareDir/dist/"

# Add in commit hash, to help debug deployment build
git rev-parse HEAD > "$cloudflareDir/dist/GITHASH"

# Debug for file tree
cd "$cloudflareDir"
if [ ! -z "DISPLAY_DEPLOY_FILE_TREE" ]; then
	tree -L 3;
fi

#
# Getting the various configuration settings from command line / environment variable
#

if [ -z "$CLOUDFLARE_EMAIL" ]; then
	echo ">> Please type in your CLOUDFLARE_EMAIL (eg: admin@yourdomain.com)";
	read -p '>> CLOUDFLARE_EMAIL : ' CLOUDFLARE_EMAIL;
else
	echo ">> Detected CLOUDFLARE_EMAIL env variable : $CLOUDFLARE_EMAIL";
fi

if [ -z "$CLOUDFLARE_API_KEY" ]; then
	echo ">> Please type in your CLOUDFLARE_API_KEY";
	read -sp '>> CLOUDFLARE_API_KEY : ' CLOUDFLARE_API_KEY;
	echo "";
else
	echo ">> Detected CLOUDFLARE_API_KEY env variable : [intentionally redacted]";
fi

if [ -z "$CLOUDFLARE_ZONE_ID" ]; then
	echo ">> Please type in your CLOUDFLARE_ZONE_ID";
	read -sp '>> CLOUDFLARE_ZONE_ID : ' CLOUDFLARE_ZONE_ID;
	echo "";
else
	echo ">> Detected CLOUDFLARE_ZONE_ID env variable : [intentionally redacted]";
fi

if [ -z "$MAILGUN_EMAIL_DOMAIN" ]; then
	echo ">> Please type in your MAILGUN_EMAIL_DOMAIN (eg: inboxkitten.com)";
	read -p '>> MAILGUN_EMAIL_DOMAIN : ' MAILGUN_EMAIL_DOMAIN;
else
	echo ">> Detected MAILGUN_EMAIL_DOMAIN env variable : $MAILGUN_EMAIL_DOMAIN";
fi
#
# Exporting variables, for envsubst support
#
export MAILGUN_EMAIL_DOMAIN="$MAILGUN_EMAIL_DOMAIN"
export CLOUDFLARE_ZONE_ID="$CLOUDFLARE_ZONE_ID"
export CLOUDFLARE_API_KEY="$CLOUDFLARE_API_KEY"
export CLOUDFLARE_EMAIL="$CLOUDFLARE_EMAIL"

# Calling cloudflare deploy, with parameters passing forward
echo ">> Deploying to cloudflare"
echo
curl -X PUT "https://api.cloudflare.com/client/v4/zones/"$CLOUDFLARE_ZONE_ID"/workers/script" -H "X-Auth-Email:$CLOUDFLARE_EMAIL" -H "X-Auth-Key:$CLOUDFLARE_API_KEY" -H "Content-Type:application/javascript" --data-binary "@./dist/main.js"

read -p ">> Set up route on cloudflare? (yes/no)" CLOUDFLARE_SETUP_ROUTE;
if [ "$CLOUDFLARE_SETUP_ROUTE" == "yes" ]; then
    echo ">> Setting route on cloudflare"
    curl -X POST "https://api.cloudflare.com/client/v4/zones/"$CLOUDFLARE_ZONE_ID"/workers/filters" -H "X-Auth-Email:$CLOUDFLARE_EMAIL" -H "X-Auth-Key:$CLOUDFLARE_API_KEY" -H "Content-type: application/json" -d '{"pattern": "'$MAILGUN_EMAIL_DOMAIN'/api/*", "enabled": true}'
fi

echo ">> Cloudflare script completed"

================================================
FILE: deploy/firebase/deploy.sh
================================================
#!/bin/bash

# Deploy will terminate on any error
set -e

# Firebase Working directory
firebaseDir="`dirname \"$0\"`"
cd "$firebaseDir" || exit 1
firebaseDir="`pwd`"

# Project directory detection
projectDir="$(cd "$firebaseDir/../.."; pwd)"
echo ">> Assuming project directory of : $projectDir"

# Clearing out firebase public / functions folder
rm -rf "$firebaseDir/functions/"; 
rm -rf "$firebaseDir/public/"; 
mkdir -p "$firebaseDir/public/cli/";
mkdir -p "$firebaseDir/functions/";

# Transfering files into fire base deploy folder
echo ">> Preparing build files for firebase upload"
cp -a "$projectDir/ui/dist/." "$firebaseDir/public/"
cp -a "$projectDir/cli/bin/." "$firebaseDir/public/cli/"
cp -a "$projectDir/api/." "$firebaseDir/functions/"

# Reconfigure the API function for firebase
cp "$firebaseDir/functions/firebase.js" "$firebaseDir/functions/index.js"

# Add in commit hash, to help debug deployment build
git rev-parse HEAD > "$firebaseDir/public/GITHASH"

# Debug for file tree
cd "$firebaseDir"
if [ ! -z "DISPLAY_DEPLOY_FILE_TREE" ]; then
	tree -L 3;
fi

# Calling firebase deploy, with parameters passing forward
echo ">> Deploying to firebase"
firebase deploy $@


================================================
FILE: deploy/firebase/firebase.json
================================================
{
	"hosting": {
		"public": "public",
		"ignore": [
			"firebase.json",
			"**/.*",
			"**/node_modules/**"
		],
		"rewrites": [
			{
				"source": "/api/v1/mail/**",
				"function": "firebase_api_v1"
			},
			{
				"source": "**",
				"destination": "/index.html"
			}
		]
	}
}


================================================
FILE: docker-dev-build.sh
================================================
#!/bin/bash

docker stop $(docker ps -a | grep $(basename "$PWD") | awk '{print $1}'); 
docker rm $(docker ps -a | grep $(basename "$PWD") | awk '{print $1}');
docker build -t $(basename "$PWD") .  
# docker run -d -P --name $(basename "$PWD") $(basename "$PWD");

================================================
FILE: docker-entrypoint.sh
================================================
#!/bin/sh

#
# Entrypoint start
#
echo ">>---------------------------------------------------------------------"
echo ">> Starting inboxkitten container : Get Mail Nyow!"
echo ">>---------------------------------------------------------------------"

#
# Getting the various configuration settings from command line / environment variable
#
if [ -z "$MAILGUN_EMAIL_DOMAIN" ]; then
	echo "[FATAL ERROR] Missing MAILGUN_EMAIL_DOMAIN (eg: inboxkitten.com)";
    exit 1;
else
	echo ">> Detected MAILGUN_EMAIL_DOMAIN env variable : $MAILGUN_EMAIL_DOMAIN";
fi

if [ -z "$MAILGUN_API_KEY" ]; then
	echo "[FATAL ERROR] Missing MAILGUN_API_KEY";
	exit 1;
else
	echo ">> Detected MAILGUN_API_KEY env variable : [intentionally redacted]";
fi

if [ -z "$WEBSITE_DOMAIN" ]; then
	echo ">> Missing WEBSITE_DOMAIN, using MAILGUN_EMAIL_DOMAIN : $MAILGUN_EMAIL_DOMAIN"
    export WEBSITE_DOMAIN="$MAILGUN_EMAIL_DOMAIN"
else
	echo ">> Detected WEBSITE_DOMAIN env variable : $WEBSITE_DOMAIN";
fi

#
# End of env variable checks
# Moving to config setups
#
echo ">>---------------------------------------------------------------------"

# Debug check
# ls /application/

#
# Setup the UI
#
echo ">> Setting up UI"

# Clone the files
rm -rf /application/api/public/
mkdir /application/api/public/
cp -r /application/ui-dist/* /application/api/public/

# Debug check
# ls /application/api/public/

# Search token (so that it does not get character substituted)
TOKEN_MAILGUN_EMAIL_DOMAIN='${MAILGUN_EMAIL_DOMAIN}'
TOKEN_WEBSITE_DOMAIN='${WEBSITE_DOMAIN}'

# Find and replace
find /application/api/public/ -type f -exec sed -i "s/$TOKEN_MAILGUN_EMAIL_DOMAIN/$MAILGUN_EMAIL_DOMAIN/g" {} +
find /application/api/public/ -type f -exec sed -i "s/$TOKEN_WEBSITE_DOMAIN/$WEBSITE_DOMAIN/g" {} +

#
# Setup the API
#
echo ">> Setting up API config"
cat "/application/api/config/mailgunConfig.sample.js" | envsubst > "/application/api/config/mailgunConfig.js"

#
# Start the server
#
echo ">>---------------------------------------------------------------------"
echo ">> Starting the server"
echo ">>---------------------------------------------------------------------"
cd /application/api/
npm start


================================================
FILE: docker-notes.md
================================================
# Docker build process

## inside the respective folder (eg: ./ssh/)
docker build -t {tagname} .
docker build -t $(basename "$PWD") .

## running the build
docker run -d -P --name {tagname} {container_name}
docker run -d -P --name $(basename "$PWD") $(basename "$PWD")

-d : detach mode
-P : auto publish all ports
-e : set env key=value pairs

## adding listener port
docker port containername XPortNum

# Useful command chains

## build, run
docker build -t $(basename "$PWD") . && docker run -d -P --name $(basename "$PWD") $(basename "$PWD");

## nuke all, build, run
docker rm $(docker ps -a -q); docker build -t $(basename "$PWD") . && docker run -d -P --name $(basename "$PWD") $(basename "$PWD");

## stop all, nuke all, build, run
docker stop $(docker ps -aq); docker rm $(docker ps -a -q); docker build -t $(basename "$PWD") . && docker run -d -P --name $(basename "$PWD") $(basename "$PWD");

# Stop current "basename", Nuke it, Build it, Run it
```
docker stop $(docker ps -a | grep $(basename "$PWD") | awk '{print $1}'); 
docker rm $(docker ps -a | grep $(basename "$PWD") | awk '{print $1}');
docker build -t $(basename "$PWD") . && 
docker run -d -P --name $(basename "$PWD") $(basename "$PWD");
```

Or as a single line

`docker stop $(docker ps -a | grep $(basename "$PWD") | awk '{print $1}'); docker rm $(docker ps -a | grep $(basename "$PWD") | awk '{print $1}'); docker build -t $(basename "$PWD") . && docker run -d -P --name $(basename "$PWD") $(basename "$PWD");`

# Docker commands

## Delete all containers
docker rm $(docker ps -a -q)
## Delete all images
docker rmi $(docker images -q)


# ENTRYPOINT vs CMD

- ENTRYPOINT is runs WITH cmd,
- CMD is commonly overwritten by user
- Technically user can overwrite ENTRYPOINT with additional command "flags"
- Final executed command : ENTRYPOINT CMD


================================================
FILE: ui/.gitignore
================================================
.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln


================================================
FILE: ui/README.md
================================================
# inboxkitten

> A personal disposable email ui

## Build Setup

``` bash
# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report
```

For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).


================================================
FILE: ui/commonshost.js
================================================
let commonsHostConfig = {
	root: './index.html',
	fallback: { 200: './index.html' },
	directories: { trailingSlash: 'never' },
	accessControl: { allowOrigin: '*' },
	headers: [
		{ fields: { 'X-Frame-Options': 'deny' } }
		// {
		// 	uri: '/static/{dir}/{filename}.{hash}.{type}',
		// 	fields: {
		// 		'Cache-Control': 'public, max-age=31536000, immutable'
		// 	}
		// }
	],
	manifest: [{ get: '/index.html', push: '/favicon.ico' }]
}

module.exports = {
	hosts: [
		Object.assign({ domain: 'inboxkitten.com' }, commonsHostConfig),
		Object.assign({ domain: 'commonshost-raw.inboxkitten.com' }, commonsHostConfig),
		Object.assign({ domain: 'commonshost.inboxkitten.com' }, commonsHostConfig)
	]
}

================================================
FILE: ui/config/apiconfig.sample.js
================================================
export default {
	apiUrl: '//${WEBSITE_DOMAIN}/api/v1/mail',
	domain: '${MAILGUN_EMAIL_DOMAIN}'
}


================================================
FILE: ui/config/carbonads.js
================================================
module.exports = {
	"enable": true,
	"carbon_ad_src": "https://cdn.carbonads.com/carbon.js?serve=CK7D5KQW&placement=inboxkittencom",
	"placement": ["InboxKittenLanding"]
}

================================================
FILE: ui/config/dev.env.js
================================================
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})


================================================
FILE: ui/config/index.js
================================================
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8000, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-

    // Use Eslint Loader?
    // If true, your code will be linted during bundling and
    // linting errors and warnings will be shown in the console.
    useEslint: true,
    // If true, eslint errors and warnings will also be shown in the error overlay
    // in the browser.
    showEslintErrorsInOverlay: false,

    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',

    /**
     * Source Maps
     */

    productionSourceMap: true,
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}


================================================
FILE: ui/config/prod.env.js
================================================
'use strict'
module.exports = {
  NODE_ENV: '"production"'
}


================================================
FILE: ui/config/shareConfig.js
================================================
export default {
  githubForkLink: "https://github.com/uilicious/inboxkitten/fork",
  githubAriaLabel: "Fork uilicious/inboxkitten on GitHub",
  tweetMessage: "InboxKitten is amazing! Check it out here at",
  tweetDataUrl: "https://inboxkitten.com",
  mainURL: "https://inboxkitten.com",
  enabled: true
}


================================================
FILE: ui/index.html
================================================
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
		<meta property="og:title" content="InboxKitten"/>
		<meta property="og:url" content="https://inboxkitten.com"/>
		<meta property="og:type" content="website"/>
		<meta property="og:description" content="An Open-source Disposable Email (powered by serverless kittens 🐱)"/>
		<meta property="og:image" content="https://inboxkitten.com/static/inbox-kitten-opengraph.jpg"/>

		<meta property="twitter:card" content="summary"/>
		<meta property="twitter:title" content="InboxKitten"/>
		<meta property="twitter:url" content="https://inboxkitten.com"/>
		<meta property="twitter:site:id" content="@inboxkitten"/>
		<meta property="twitter:description" content="An Open-source Disposable Email (powered by serverless kittens 🐱)"/>
		<meta property="twitter:image" content="https://inboxkitten.com/static/inbox-kitten-opengraph.jpg"/>
		<title>inboxkitten</title>
		<!-- favicon -->
		<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon" />
		<!--<script async defer src="https://buttons.github.io/buttons.js"></script>-->
		<!--<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>-->
	</head>
	<body>
		<div id="app"></div>
		<script type="module" src="/src/main.js"></script>
		<!-- built files will be auto injected -->
	</body>
</html>


================================================
FILE: ui/package.json
================================================
{
  "name": "inboxkitten-ui",
  "version": "1.1.0",
  "type": "module",
  "description": "UI for Inboxkitten - A personal disposable email",
  "keywords": [
    "inboxkitten"
  ],
  "author": "uilicious",
  "license": "MIT",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.5.0",
    "clipboard": "^2.0.11",
    "jquery": "^3.7.1",
    "moment": "^2.29.4",
    "normalize.css": "^8.0.1",
    "primer-tooltips": "^2.0.0",
    "random-words": "^2.0.0",
    "sass": "^1.68.0",
    "vue": "^2.7.14",
    "vue-router": "^3.6.5",
    "vue-spinner": "^1.0.4",
    "vuescroll": "^4.18.0",
    "vuex": "^3.6.2"
  },
  "devDependencies": {
    "@vitejs/plugin-vue2": "^2.2.0",
    "vite": "^4.4.5"
  }
}


================================================
FILE: ui/src/App.vue
================================================
<template>
	<div id="app">
		<!-- Github corners -->
		<a href="https://github.com/uilicious/inboxkitten" class="github-corner" aria-label="View source on Github" target="_blank" style="z-index:99;"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
		<!-- Product hunt banner -->
		<!-- <div>
			<a href="https://www.producthunt.com/posts/inboxkitten" target="_blank" class="product-hunt">
				<img src="@/assets/product-hunt-240.png" class="ph-icon"/>
				<p>Support our product hunt launch here</p>
			</a>
		</div> -->
		<!-- <div style="position:fixed; top:3vh; right:2vw; z-index:10;" v-if="isShare"> -->
			<!--<a href="https://twitter.com/intent/tweet?ref_src=twsrc%5Etfw" class="twitter-hashtag-button" data-size="large" :data-text="tweetMsg" :data-url="tweetDataUrl" data-show-count="false">Tweet</a>-->
			<!--<a class="github-button" :href="githubLink" data-size="large" :aria-label="githubAriaLabel">Fork</a>-->
		<!-- </div> -->
		<router-view class="app-router-view"/>
	</div>
</template>

<script>
import shareConfig from '@/../config/shareConfig.js'

export default {
	name: 'App',
	data: () => {
		return {
			githubLink: shareConfig.githubForkLink,
			githubAriaLabel: shareConfig.githubAriaLabel,
			tweetDataUrl: shareConfig.tweetDataUrl,
			tweetMsg: shareConfig.tweetMessage,
			isShare: shareConfig.enabled
		}
	}
}
</script>

<style lang="scss" rel="stylesheet/scss">

	@import url("https://unpkg.com/purecss@1.0.0/build/pure-min.css");
	@import "scss/_producthunt.scss";
	@import "scss/_common.scss";

	#app {
		font-family: 'Avenir', Helvetica, Arial, sans-serif;
		-webkit-font-smoothing: antialiased;
		-moz-osx-font-smoothing: grayscale;
		text-align: center;
		color: #2c3e50;
		width:100vw;
		display: flex;
		flex-direction: column;
	}

	.app-router-view {
		flex:1;
	}

	// Github corner styling
	.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}

	@media only screen and (max-width:800px) {
		.github-corner {
			visibility: hidden;
		}
	}
</style>


================================================
FILE: ui/src/components/CarbonAds.vue
================================================
<template>
	<div id="_carbon_ads_div"></div>
</template>

<script>
	import config from '@/../config/carbonads.js'
	export default {
		name: 'CarbonAds',
		data: () => {
			return {
				placement: ''
			}
		},
		mounted () {
			// // Skip if disabled
			// if (!config.enable) {
			// 	return
			// }

			// // Does a placement check if configured
			// if (config.placementList) {
			// 	let placementList = config.placementList
			// 	if (placementList.indexOf(this.placement) < 0) {
			// 		// No placement found, rejects
			// 		return
			// 	}
			// }

			// Lets do the script injection
			let scriptTag = document.createElement('script')
			scriptTag.setAttribute('src', config.carbon_ad_src)
			scriptTag.setAttribute('type', 'text/javascript')
			scriptTag.setAttribute('async', 'async')
			scriptTag.setAttribute('id', '_carbonads_js')

			document.getElementById('_carbon_ads_div').appendChild(scriptTag)
		}
	}
</script>
<style lang="scss" rel="stylesheet/scss">
#carbonads {
	display: block;
	overflow: hidden;
	padding: 10px;
	box-shadow: 0 1px 3px hsla(0, 0%, 0%, .05);
	border-radius: 4px;
	font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, Arial, sans-serif;
	line-height: 1.5;
	max-width: 300px;
	font-size: 12px;
	background-color: #fff;
	float:left;
	z-index: 4;
	position: absolute;
}

#carbonads a {
	text-decoration: none;
}

#carbonads span {
	position: relative;
	display: block;
	overflow: hidden;
}

.carbon-img {
	float: left;
	margin-right: 1em;
}

.carbon-img img {
	display: block;
}

.carbon-text {
	display: block;
	float: left;
	max-width: calc(100% - 130px - 1em);
	text-align: left;
	color: #637381;
}

.carbon-poweredby {
	position: absolute;
	left: 142px;
	bottom: 0;
	display: block;
	font-size: 8px;
	color: #c5cdd0;
	font-weight: 500;
	text-transform: uppercase;
	line-height: 1;
	letter-spacing: 1px;
}

@media only screen and (max-width:1300px) and (min-width: 760px) {
	#carbonads {
	display: block;
	overflow: hidden;
	font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
	padding: 1em;
	background: #fff;
	text-align: center;
	line-height: 1.5;
	font-size: 14px;
	max-width: 130px;
	}

	#carbonads a {
	color: inherit;
	text-decoration: none;
	}

	#carbonads a:hover {
	color: inherit;
	}

	#carbonads span {
	display: block;
	overflow: hidden;
	}

	.carbon-img {
	display: block;
	margin: 0 auto 8px;
	line-height: 1;
	}

	.carbon-text {
	display: block;
	margin-bottom: 8px;
	text-align: left;
	color: #637381;
	max-width:130px;
	}

	.carbon-poweredby {
	text-transform: uppercase;
	display: block;
	font-size: 10px;
	letter-spacing: 1px;
	line-height: 1;
	}
}

@media only screen and (max-width:800px){
	#carbonads {
		position:fixed;
		width:100vw;
		max-width: 100vw;
		z-index: 99;
	}
}

</style>


================================================
FILE: ui/src/components/NavBar.vue
================================================
<template>
	<nav class="nav">
		<div class="back-button" @click="backAPage"><i class="fa fa-arrow-left fa-3x"/></div>
		<div class="logo-box">
			<img class="logo" src="@/assets/logo_no_text.svg" @click="goMainPage"/>
		</div>

			<form v-on:submit.prevent="" class="form-box">
				<input class="input-email" name="email" aria-label="email" type="text" v-model="email" id="email-input"/>
				<div class="domain-text" id="div-domain" data-clipboard-target="#email-input">@{{domain}}</div>
				<input type="submit" class="submit" value="Go!" @click="changeInbox"/>
				<button class="refresh" @click="emitRefresh">Refresh</button>
			</form>
	</nav>
</template>

<script>
	import config from '@/../config/apiconfig.js'
	import 'normalize.css'
	import $ from 'jquery'
	import ClipboardJS from 'clipboard'

	export default {
		name: 'NavBar',
		data: () => {
			return {
				email: ''
			}
		},
		computed: {
			domain () {
				return config.domain
			}
		},
		mounted () {
			this.email = this.$route.params.email
			if (this.email === '') {
				this.goMainPage()
			}

			this.$clipboard = []

			let self = this

			this.$clipboard[0] = new ClipboardJS('#div-domain', {
				text: function (trigger) {
					if (self.email.includes('@' + config.domain)) {
						return self.email
					}
					return self.email + '@' + config.domain
				}
			})

			this.$clipboard[0].on('success', function (e) {
				$('#email-input').select()
				$('#div-domain').addClass('tooltipped tooltipped-s')
				$('#div-domain').attr('aria-label', 'Copied!')
				$('#div-domain').on('mouseleave', function () {
					$('#div-domain').removeClass('tooltipped tooltipped-s')
					$('#div-domain').removeAttr('aria-label')
				})
			})
		},
		beforeDestroy () {
			if (this.$clipboard !== null) {
				this.$clipboard.forEach((cb) => {
					cb.destroy()
				})
			}
		},
		methods: {
			goMainPage () {
				this.$router.push({
					name: 'Kitten Land'
				})
			},
			emitRefresh () {
				this.$eventHub.$emit('refresh', '')
			},
			changeInbox () {
				this.$router.push({
					name: 'List',
					params: {
						email: this.email
					}
				})
				this.$eventHub.$emit('refreshInbox', {email: this.email})
			},
			backAPage () {
				if (this.$route.name === 'List') {
					this.$router.push({
						name: 'Kitten Land'
					})
				} else {
					this.$router.push({
						name: 'List'
					})
				}
			}
		}
	}
</script>

<style lang="scss" rel="stylesheet/scss">
	@import "primer-tooltips/index.scss";
	@import "@/scss/_color.scss";
	.nav {
		background: #36D1DC;  /* fallback for old browsers */
		background: -webkit-linear-gradient(to right, #5B86E5, #36D1DC);  /* Chrome 10-25, Safari 5.1-6 */
		background: linear-gradient(to right, #5B86E5, #36D1DC); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
		width: 100vw;
		height: 10rem;
		text-align: left;
		padding-top:2rem;
		padding-bottom:2rem;
		top: 0;

		.logo-box{
			width:100%;
			height:2rem;
			text-align: center;
			vertical-align: center;
			display: block;
			overflow: hidden;

			.logo {
				width:6rem;
			}
			.logo:hover {
				cursor: pointer;
			}
		}

		.back-button {
			position:absolute;
			padding-left:30rem;
			padding-top:3rem;
			padding-right:3rem;
			padding-bottom:3rem;
			cursor: pointer;
			margin:0;
		}
	}

	//
	// Email form box
	//

	.form-box {
		display:flex;
		flex-direction: row;
		justify-content: center;
		align-items: stretch;

		.domain-text {
			display: none;
			width:14rem;
		}

		.input-email {
			text-align: center;
			border: 3px solid black;
		}
		.submit {
			background: $cta-base;
			color: $cta-base-text;
			border: 3px solid black;
			border-left-width: 0;
		}

		.submit:hover {
			background-color: $cta-hover;
			color: $cta-hover-text;
		}
		.refresh {
			background: #005CFF;
			color: $cta-base-text;
			border: 3px solid black;
			border-left-width: 0;
		}

		.refresh:hover {
			background-color: $cta-hover;
			color: $cta-hover-text;
		}
	}

	@media (min-width: 760px){ // IPad and above
		.nav{
			.logo-box{
				height:6rem;
				.logo{
					width:16rem;
				}
			}

			.back-button {
				padding-left:3rem;
			}
		}

		.form-box{
			.domain-text{
				display: inline-block;
				background:white;
				text-align: center;
				margin: 0;
				padding:0.2rem; // need help on this
				vertical-align: middle;
				border: 3px solid black;
				border-left-width: 0;
				background-color: $domain-base;
				cursor: pointer;
				width:10rem;
			}
		}
	}

	@media (max-width: 800px) { // IPad portrait
		.nav{
			.back-button{
				padding:3rem;
				padding-left:3rem;
			}
		}
	}

	@media (max-width:760px){ // Smartphones
		.nav {
			height:4rem;

			.back-button {
				padding: 1rem;
				padding-left: 2rem;

				font-size: 10px;
			}
		}

		.form-box{
			.input-email{
				width: 9rem;
			}
			.refresh {
				display:none;
			}
		}
	}

	@media (max-width: 320px){ // IPhone 5/SE

		.form-box{
			.input-email{
				width: 7rem;
			}
		}
	}

</style>


================================================
FILE: ui/src/components/mail/inbox.vue
================================================
<template>
  <div class="wrapper">
    <nav-bar class="nav-bar"></nav-bar>
    <router-view class="inbox"></router-view>
  </div>
</template>

<script>
  import NavBar from '../NavBar.vue'

  export default {
    name: 'inbox',
    components: {
      NavBar: NavBar
    }
  }
</script>
<style>
  .wrapper {
    display: flex;
    flex-direction: column;
    width:100%;
    height:100%;
  }
  .nav-bar{
  }
  .inbox{
    flex: 1;
    z-index:9;
  }

</style>


================================================
FILE: ui/src/components/mail/message_detail.vue
================================================
<template>
	<div class="message-details">
		<div class="subject">{{emailContent.subject}}</div>
		<div class="meta-info">
			<div class="left">
				<div class="sender"><b>{{emailContent.name}}</b>{{emailContent.emailAddress}}</div>
				<div class="to">to: {{emailContent.recipients}}</div>
			</div>
			<div class="date">{{emailContent.Date}}</div>
		</div>
		<iframe id="message-content" scrolling="yes" :src="src"></iframe>
	</div>
</template>

<script>
	import 'normalize.css'
	import config from '@/../config/apiconfig.js'
	import axios from 'axios'

	export default {
		name: 'MessageDetail',
		data: () => {
			return {
				emailContent: {},
				src: ''
			}
		},
		mounted () {
			if (this.$route.params.key === undefined) {
				this.$router.push({
					name: 'Kitten Land'
				})
			}

			this.getMessage()
			this.$eventHub.$on('refresh', this.getMessage)
		},
		beforeDestroy () {
			this.$eventHub.$off('refresh', this.getMessage)
		},
		methods: {
			getMessage () {
				let region = this.$route.params.region
				let key = this.$route.params.key
				this.src = `${config.apiUrl}/getHtml?region=${region}&key=${key}`
				axios.get(`${config.apiUrl}/getKey?region=${region}&key=${key}`)
					.then(res => {
						this.emailContent = res.data
					}).catch((e) => {
						this.emailContent.name = 'Kitten Squads'
						this.emailContent.recipients = 'Master'
						let iframe = document.getElementById('message-content')
						iframe.src = 'data:text/html;charset=utf-8,' + encodeURI('The kittens found no messages :(')
				})
			}
		}
	}
</script>

<style lang="scss" rel="stylesheet/scss">

	.message-details {
		height: auto;
		overflow:auto;
		display:flex;
		flex-direction: column;

		.subject {
			font-weight: bold;
			font-size:1.5rem;
			padding:1rem;
			padding-bottom:0;
			text-align: left;
		}

		.meta-info{
			display:flex;
			flex-direction: row;
			justify-content: space-between;
			text-align: left;
			padding: 1rem;
			border-bottom: 1px solid #20a0ff;
		}
	}

	@media (max-width: 760px) {
		.message-details{
			top: 8rem;
			.subject{
				font-size:1rem;
			}
			.meta-info{
				font-size:0.6rem;
			}
		}
		#message-content{
			flex:1;
		}
	}

	#message-content{
		width: 100%;
		height: 100%;
		overflow: auto;
		border: none;
	}
</style>


================================================
FILE: ui/src/components/mail/message_list.vue
================================================
<template>
    <vue-scroll :ops="vueScrollBarOps">
      <div class="table-box advisory" style="
          background: #fbc02d4f;
          padding: 0.5em;
      ">
        <i class="fas fa-exclamation-triangle" style="margin-right:0.5em"></i><a href="https://uilicious.com/blog/psa-inboxkitten-will-be-blocking-no-reply-google/" target="_blank"><b>PSA</b>: Please use inboxkitten, for only testing, or non critical emails. See here for more details.</a>
      </div>
      <pulse-loader v-if="refreshing" class="loading"></pulse-loader>
      <div class="email-list table-box" v-if="listOfMessages.length > 0">
        <div class="email-list-item" :class="rowCls(index)" v-for="(msg, index) in listOfMessages" :key="msg.url"
             @click="getMessage(msg)">

          <div class="row-info">
            <div class="row-name">{{extractEmail(msg.message.headers.from)}}</div>
            <div class="row-subject">{{(msg.message.headers.subject)}}</div>
          </div>

          <div class="row-time">{{calculateTime(msg)}}</div>
        </div>
      </div>
      <div class="no-mails" v-if="listOfMessages.length == 0">
        <p>
          There for no messages for this kitten :(<br/><br/>
          Press on the 'Refresh' button if you want to overwork the kittens...
        </p>
        <button class="refresh-button" @click="refreshList" v-if="!refreshing">Refresh</button>
      </div>
    </vue-scroll>
</template>

<script>
import NavBar from '../NavBar.vue'
import 'normalize.css'
import config from '@/../config/apiconfig.js'
import axios from 'axios'
import moment from 'moment'
import PulseLoader from 'vue-spinner/src/PulseLoader.vue'

export default {
  name: 'MessageList',
  data: () => {
    return {
      listOfMessages: [],
      vueScrollBarOps: {
        bar: {
          background: 'darkgrey'
        }
      },
      refreshing: false
    }
  },
  mounted () {
    let currentEmail = this.$route.params.email
    if (currentEmail === '') {
      this.$router.push({name: 'Kitten Land'})
    }

    this.getMessageList()

    this.retrieveMessage = window.setInterval(this.getMessageList, 10000)

    this.$eventHub.$on('refreshInbox', this.getMessageList)
    this.$eventHub.$on('refresh', this.getMessageList)
  },
  beforeDestroy () {
    window.clearInterval(this.retrieveMessage)

    this.$eventHub.$off('refreshInbox', this.getMessageList)
    this.$eventHub.$off('refresh', this.getMessageList)
  },
  methods: {
    refreshList () {
      this.refreshing = true
      this.getMessageList()
    },
    getMessageList () {
      this.refreshing = true
      let email = this.$route.params.email
      axios.get(config.apiUrl + '/list?recipient=' + email)
        .then(res => {
          this.listOfMessages = res.data
          this.refreshing = false
        }).catch((e) => {
        this.refreshing = false
      })
    },
    changeInbox () {
      this.$router.push({
        params: {
          email: this.email
        }
      })
      this.emailContent = {}
      this.$eventHub.$emit('iframe_content', '')
      this.refreshList()
    },

    getMessage (msg) {
      
      this.$router.push({
        name: 'Message',
        params: {
          region: msg.storage.region,
          key: msg.storage.key
        }
      })

      this.$eventHub.$emit('getMessage', '')
    },

    //
    // Utility Functions
    //

    calculateTime (msg) {
      let now = moment()
      let theDate = moment(msg.timestamp * 1000)
      let diff = now.diff(theDate, 'day')
      if (diff === 0) {
        return theDate.format('hh:mm a')
      } else if (diff > 0) {
        return theDate.format('DD MMM')
      }
    },

    extractEmail (sender) {
      let emails = sender.match(/[^@<\s]+@[^@\s>]+/g)

      // If there are any email in the matching, take the first and return
      if (emails) {
        return emails[0]
      }

      // Sender does not contain any formatted name, do not format them
      return sender
    },

    rowCls (index) {
      if (index % 2 === 0) {
        return 'table-row even'
      }
      return 'table-row odd'
    }
  },
  components: {
    NavBar: NavBar,
    PulseLoader: PulseLoader
  }
}
</script>

<style lang="scss" rel="stylesheet/scss">
  @import '@/scss/_color.scss';

  .table-box {
    width: 100%;
    height: auto;
    .table-row {
      display: flex;
      flex-direction: row;
      /*justify-content: space-evenly;*/
      justify-content: flex-start;
      border: 3px solid white;
      border-bottom: 3px solid #20a0ff;

      .row-info {
        width: 85%;

        .row-name {
          font-weight: bold;
          text-align: left;
        }
      }
    }
  }

  .table-row:hover {
    border: 3px solid black;
    background-color: $cta-hover;
  }

  .no-mails {
    text-align: center;
    vertical-align: center;
    overflow: auto;
    margin-top: 2rem;
    z-index: 10;
  }

  .refresh-button {
    border: 3px solid black;
    background-color: $cta-base;
    color: $cta-base-text;
  }

  .refresh-button:hover {
    background-color: $cta-hover;
    color: $cta-hover-text;
  }

  .loading {
    z-index:9;
    position:absolute;
    padding-top: 5rem;
    left:50%;
  }

  @media (min-width: 760px) {
    .table-row {
      padding: 1rem;
      .row-info {
        display: flex;
        flex-direction: row;
        justify-content: flex-start;
        .row-name {
          margin-right: 1em;
          width: 35%;
          min-width: 35%;
          max-width: 35%;
        }
      }
      border-bottom: 1px solid #20a0ff;
    }
  }

  @media (max-width: 760px) {
    .table-row {
      display: flex;
      flex-direction: row;
      width: 98vw;
      margin: auto;
      background-color: white;
      border-bottom: 1px solid #20a0ff;

      .row-info {
        text-align: left;
        padding-left: 0.5rem;
        .row-name {
          font-size: 1rem;
          font-weight: bold;
          padding: 0.5rem;
          /*background: #06FFAB;*/
          width: 100%;
          text-overflow: ellipsis;
          white-space: nowrap;
          overflow: hidden;
        }
        .row-subject {
          /*background-color: coral;*/
          width: 100%;
          padding-left: 0.5rem;
          padding-bottom: 0.5rem;
          font-size: 0.75rem;
          font-weight: bold;
          text-overflow: ellipsis;
          white-space: nowrap;
          overflow: hidden;
        }
      }

      .row-time {
        width: 20%;
        font-size: 12px;
        text-align: center;
        vertical-align: middle;
        padding: 0.5rem;
        padding-left: 0;
      }
    }

    .loading{
      left:40%;
    }
  }
</style>


================================================
FILE: ui/src/kittenrouter.vue
================================================
<template>
	<div>
		<!-- <carbon-ads placement="InboxKittenLanding"></carbon-ads> -->
		<div class="kittenrouter-navigation">
			<img class="kittenrouter-nav-main-logo" src="@/assets/inbox_kitten.png" @click="goToMainPage"/>
		</div>
		<div class="header-gradient-background">
			<div class="header">
				<div class="logo-box">
					<img class="logo" src="@/assets/kitten_router.png"/>
				</div>
			</div>
		</div>
		<div class="info-guide">
			<div class="features" style="max-width:1100px">
				<div class="feature-card">
                    <img class="logo" src="@/assets/elasticsearch.png" style="width:3.7rem"/>
					<h3>ElasticSearch</h3>
					<p>
						Log down your network traffic to your own elasticsearch server for analytics!
					</p>
				</div>
				<div class="feature-card">
                    <i class="fas fa-server fa-4x"></i>
					<h3>Find next available server</h3>
					<p>With the list of configured servers, KittenRouter can help you redirect the request to the next available server.</p>
				</div>
				<div class="feature-card">
                    <img class="logo" src="@/assets/cloudflare.png" style="width:10rem"/>
					<h3>Deploy it on Cloudflare</h3>
					<p>Kitten Router is built for Cloudflare Workers script!</p>
				</div>
			</div>
		</div>
		<div class="info-guide">
			<div class="deploy-segmet">
				<h2>Configuring Kitten Router</h2>
				<p>Before you use Kitten Router, make sure that your configuration settings are correct. Below is a sample configuration.</p>
			</div>
		</div>
		<div class="code config-code">
<pre style="color:#000020;background:#f6f8ff;"><span style="color:#200080; font-weight:bold; ">let</span> config <span style="color:#308080; ">=</span> <span style="color:#406080; ">{</span>

	<span style="color:#595979; ">// logging endpoint to use</span>
	<span style="color:#200080; font-weight:bold; ">log</span> <span style="color:#406080; ">:</span> <span style="color:#308080; ">[</span>
		<span style="color:#406080; ">{</span>
			type <span style="color:#406080; ">:</span> <span style="color:#800000; ">"</span><span style="color:#1060b6; ">elasticsearch</span><span style="color:#800000; ">"</span><span style="color:#308080; ">,</span>
			url <span style="color:#406080; ">:</span> <span style="color:#800000; ">"</span><span style="color:#1060b6; ">https://&lt;Your elasticsearch server url&gt;</span><span style="color:#800000; ">"</span><span style="color:#308080; ">,</span>

			<span style="color:#595979; ">//</span>
			<span style="color:#595979; ">// Authorization header (if needed)</span>
			<span style="color:#595979; ">//</span>
			basicAuthToken <span style="color:#406080; ">:</span> <span style="color:#800000; ">"</span><span style="color:#1060b6; ">username:password</span><span style="color:#800000; ">"</span><span style="color:#308080; ">,</span>

			<span style="color:#595979; ">//</span>
			<span style="color:#595979; ">// Index prefix for storing data, this is before the "YYYY.MM" is attached</span>
			<span style="color:#595979; ">//</span>
			indexPrefix <span style="color:#406080; ">:</span> <span style="color:#800000; ">"</span><span style="color:#1060b6; ">test-data-</span><span style="color:#800000; ">"</span><span style="color:#308080; ">,</span>

			<span style="color:#595979; ">// Enable logging of the full ipv4/6</span>
			<span style="color:#595979; ">//</span>
			<span style="color:#595979; ">// Else it mask (by default) the last digit of IPv4 address</span>
			<span style="color:#595979; ">// or the "network" routing for IPv6</span>
			<span style="color:#595979; ">// see : </span><span style="color:#5555dd; ">https://www.haproxy.com/blog/ip-masking-in-haproxy/</span>
			logTrueIP <span style="color:#406080; ">:</span> <span style="color:#0f4d75; ">false</span>
		<span style="color:#406080; ">}</span>
	<span style="color:#308080; ">]</span><span style="color:#308080; ">,</span>

	<span style="color:#595979; ">// Routing rules to evaluate, starting from 0 index</span>
	<span style="color:#595979; ">// these routes will always be processed in sequence</span>
	route <span style="color:#406080; ">:</span> <span style="color:#308080; ">[</span>
		<span style="color:#595979; ">// Lets load all requests to commonshost first</span>
		<span style="color:#800000; ">"</span><span style="color:#1060b6; ">commonshost.inboxkitten.com</span><span style="color:#800000; ">"</span>

		<span style="color:#595979; ">// If it fails, we fallback to firebase</span>
		<span style="color:#595979; ">//"firebase.inboxkitten.com"</span>
	<span style="color:#308080; ">]</span><span style="color:#308080; ">,</span>

	<span style="color:#595979; ">// Set to true to disable fallback to origin host </span>
	<span style="color:#595979; ">// when all routes fails</span>
	disableOriginFallback <span style="color:#406080; ">:</span> <span style="color:#0f4d75; ">false</span><span style="color:#308080; ">,</span>
<span style="color:#406080; ">}</span>
</pre>
		</div>
		<div class="info-guide">
			<div class="features steps">
			<div class="feature-card">
				<h2>Option 1: Using Kitten Router manually</h2>
				<div class="code deploy-code">
					<div style="text-align:justify">
						1. Copy the configuration<br/>
						2. Copy the index.js file in Github <br/>
						&nbsp;&nbsp;&nbsp;into your Cloudflare Worker script<br/>
						3. Initialize KittenRouter variable in your script<br/>
						&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let router = new KittenRouter(config)<br/>
						4. Use it to route all your desires
					</div>
				</div>
			</div>
			<div class="feature-card">
				<h2>Option 2: Using Kitten Router via NPM</h2>
				<div class="code deploy-code">
					<div style="text-align:justify">
						1. Copy the configuration<br/>
						2. Install KittenRouter via NPM<br/>
						&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;npm install --save kittenrouter<br/>
						3. Initialize KittenRouter class in your script<br/>
						&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const KittenRouter = require("kittenrouter")<br/>
						&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let router = new KittenRouter(config)<br/>
						4. Use it to route all your desires
					</div>
				</div>
			</div>
			</div>
		</div>

		<div class="info-guide">
			<div class="features">
                <div class="feature-card"></div>
                <div class="feature-card feature-card-hover">
                    <a href="https://github.com/uilicious/kittenrouter" target="_blank" style="text-decoration: none; color: inherit; cursor: pointer;">
                        <h2> Check out Kitten Router at Github to find out more! </h2>
<pre class="hljs" style="display: block; overflow-x: auto; padding: 0.5em; background: rgb(240, 240, 240); color: rgb(68, 68, 68);"><span>https://github.com/uilicious/kittenrouter</span></pre>
                    </a>
                </div>
                <div class="feature-card"></div>
			</div>
		</div>
        <div class="line-break"></div>
		<div class="love-notes">
			<p>
				made with <span style="color: #e25555;">&hearts;</span> by <a href="https://uilicious.com">uilicious</a> in <a href="https://www.google.com.sg/search?q=singapore">Singapore</a>
			</p>
		</div>
    </div>
</template>

<script>
import shareConfig from '@/../config/shareConfig.js'

// import CarbonAds from './components/CarbonAds.vue'

export default {
	name: 'App',
	components: {
		// CarbonAds: CarbonAds
	},
	data: () => {
		return {
			mainURL: shareConfig.mainURL
		}
    },
    mounted () {
        document.getElementsByClassName('github-corner')[0].href = 'https://github.com/uilicious/kittenrouter'
	},
	methods: {
		goToMainPage () {
			location.href = this.mainURL
		}
	}
}
</script>

<style lang="scss" rel="stylesheet/scss">
	@import url("https://use.fontawesome.com/releases/v5.3.1/css/all.css");
    @import "scss/landingpage.scss";

	.kittenrouter-navigation {
		display:flex;
		justify-content: flex-end;
		align-items: center;
		float:right;
		width: 100vw;
		height: 1rem;
		text-align: left;

		.kittenrouter-nav-main-logo {
			width:8rem;
			padding-top:7rem;
			padding-right: 4rem;
			z-index: 2;
			cursor: pointer;
		}

		.kittenrouter-nav-header {
			padding-top:6rem;
			padding-right: 3rem;
			cursor: pointer;
		}
	}

	@media only screen and (max-width:470px) {
		.kittenrouter-navigation .kittenrouter-nav-main-logo {
			padding-right: 4rem;
			width:5rem;
			z-index: 3;
		}
		.kittenrouter-nav-header {
			display: none;
		}
	}

	@media only screen and (max-width: 800px) {
		.kittenrouter-navigation {
			padding-top: 140px;
		}
	}
</style>


================================================
FILE: ui/src/landingpage.vue
================================================
<template>
	<div class="landing-page">
		<!-- <carbon-ads placement="InboxKittenLanding"></carbon-ads> -->
		<!-- <div><a href="/kittenrouter" class="product-hunt">Introducing Kitten Router 🐱 </a></div> -->
		<div class="landing-navigation">
			<router-link to="/kittenrouter">
				<img class="landing-nav-main-logo" src="@/assets/kitten_router.png" />
			</router-link>
		</div>
		<div class="header-gradient-background">
			<div class="header">
				<img class="logo" src="@/assets/inbox_kitten.png" />
				<h1>
					Open-Source
					<a></a> Disposable Email
				</h1>
				<h2>(Served by Serverless Kittens)</h2>
			</div>
			<div class="email-selection">
				<form v-on:submit.prevent="goToInbox">
					<div class="input-box">
						<input
							class="input-email"
							name="email"
							aria-label="email"
							type="text"
							v-model="randomName"
							id="email-input"
						/>
						<div class="input-suffix" id="div-domain" data-clipboard-target="#email-input">@{{domain}}</div>
					</div>
					<div class="submit-box">
						<input type="submit" class="submit" value="Get Mail Nyow!" />
					</div>
				</form>
			</div>
		</div>
		<div class="info-guide mx-a my-0 p-2" style="max-width: 1240px; ">
			<div class="pure-g mx-a" style="max-width: 1000px; background: #fbc02d4f;">
				<div class="pure-u-1-1">
						<section class="p-1">
						<h3>
							<i class="fas fa-exclamation-triangle"></i> PSA: Please use inboxkitten, for only testing, or non critical emails.
						</h3>
						<p><a href="https://uilicious.com/blog/psa-inboxkitten-will-be-blocking-no-reply-google/" target="_blank">We block "no-reply@google" to prevent abuse on our platform, see more details on our blog post (here)</a></p>
					</section>
				</div>
			</div>
			<div class="pure-g mx-a my-1" style="max-width: 1000px;">
				<div class="pure-u-1-2">
					<section class="p-1">
						<h3>
							<i class="fas fa-mail-bulk"></i> Use any inbox to avoid spam
						</h3>
						<p>Use inboxkitten when you don't want to get spammed by revealing your real email address.</p>
					</section>
				</div>
				<div class="pure-u-1-2">
					<section class="p-1">
						<h3>
							<i class="fas fa-trash-alt"></i> Email Auto-Deletes
						</h3>
						<p>Inboxkitten.com emails are in the public domain, and auto deletes after several hours. (officially 3 days according to mailgun)</p>
					</section>
				</div>
			</div>

			<section class="my-1 p-1" style="background-color: #eee;">
				<div class="mb-1">
					<h3>
						<i class="fas fa-clipboard-check"></i> Ideal for UI / QA Testing
					</h3>
					<p>
						Test your web application user signup flows, and email notification.
						<br />
						<a
							href="https://test.uilicious.com/test/public/7t74nVS828weKMtzGgJppF"
							target="_blank"
						>Or even better, automate testing with uilicious!</a>
					</p>
				</div>
				<div class="flex-row">
					<div class="code mb-0" style="flex: 1 0 auto;">
						<pre style="margin: 0; line-height: 125%"><span style="color: #66d9ef">let</span> <span style="color: #a6e22e">email</span> <span style="color: #f92672">=</span> <span style="color: #e6db74">"death-flew-61"</span>

<span style="color: #d5cccc">// Go to inbox</span>
<span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">goTo</span><span style="color: #f8f8f2">(</span><span style="color: #e6db74">"https://inboxkitten.com/inbox/"</span> <span style="color: #f92672">+</span> <span style="color: #a6e22e">email</span> <span style="color: #f92672">+</span> <span style="color: #e6db74">"/list"</span><span style="color: #f8f8f2">)</span>

<span style="color: #d5cccc">// Wait for some time for the email to arrive</span>
<span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">see</span><span style="color: #f8f8f2">(</span><span style="color: #e6db74">"@inboxkitten"</span><span style="color: #f8f8f2">)</span>
<span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">wait</span><span style="color: #f8f8f2">(</span><span style="color: #ae81ff">15</span><span style="color: #f8f8f2">)</span>

<span style="color: #d5cccc">// Open the mail</span>
<span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">see</span><span style="color: #f8f8f2">(</span><span style="color: #e6db74">"Reset your password"</span><span style="color: #f8f8f2">)</span>
<span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">click</span><span style="color: #f8f8f2">(</span><span style="color: #e6db74">"Reset your password"</span><span style="color: #f8f8f2">)</span>

<span style="color: #d5cccc">// click on the "reset password" button </span>
<span style="color: #a6e22e">UI</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">context</span><span style="color: #f8f8f2">(</span><span style="color: #e6db74">"#message-content"</span><span style="color: #f8f8f2">,</span> <span style="color: #f8f8f2">()</span><span style="color: #f92672">=&gt;</span><span style="color: #f8f8f2">{</span>
    <span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">see</span><span style="color: #f8f8f2">(</span><span style="color: #e6db74">"Here's your magic link"</span><span style="color: #f8f8f2">)</span>
    <span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">click</span><span style="color: #f8f8f2">(</span><span style="color: #e6db74">"Reset password"</span><span style="color: #f8f8f2">)</span>
<span style="color: #f8f8f2">})</span>

<span style="color: #d5cccc">// Check that I'm at the "Reset password" page</span>
<span style="color: #a6e22e">I</span><span style="color: #f8f8f2">.</span><span style="color: #a6e22e">amAt</span><span style="color: #f8f8f2">(</span><span
	style="color: #e6db74"
>"https://user.uilicious.com/resetPassword"</span><span
	style="color: #f8f8f2"
>)</span>
</pre>
					</div>

					<iframe
						src="https://snippet.uilicious.com/embed/test/public/FPNPw6BRyc4agxP5SV1nry?step=2&autoplay=1"
						frameborder="0"
						width="600px"
						height="400px;"
						class="ml-1"
						style="flex: 1 0 auto; border-radius: 5px; overflow: hidden;"
					></iframe>

				</div>
			</section>

			<div class="deploy-segmet my-1 p-2">
				<h3>
					<i class="fas fa-user-secret"></i> Need a private / secure / selfhosted version?
				</h3>
				<p>Clone and adopt your own inboxkitten using our self-hosting package</p>

				<a href="https://github.com/uilicious/inboxkitten" class="self-host-tier-link">
					<div class="self-host-tier">
						<div class="tier-title">
							<h3>Self-Host</h3>
						</div>
						<div class="price">
							<h3>$0</h3>(*per month)
						</div>
					</div>
				</a>

				<p>All you need to do is the following steps</p>
				<div class="code mx-a mb-1" style="max-width: 600px"><pre>git clone "https://github.com/uilicious/inboxkitten.git"
cd inboxkitten
./config.sh
./build.sh
firebase login
./deploy/firebase/deploy.sh
</pre></div>
				<p class="disclaimer">
					*You will need to have signed up to firebase and mailgun, where you can deploy on their "free" tier.
					<br />We take zero responsiblity over your email, laptop, or life (or lack of)
					<br />
					<br />Optionally you should have some basic programming knowledge
					<br />For more details (or other deplyment options) see our
					<a
						href="https://github.com/uilicious/inboxkitten"
					>github repository</a>
				</p>
			</div>
			<div class="line-break"></div>
		</div>
		<div class="love-notes">
			<p>
				made with
				<span style="color: #e25555;">&hearts;</span> by
				<a href="https://uilicious.com">uilicious</a> in
				<a href="https://www.google.com.sg/search?q=singapore">Singapore</a>
			</p>
		</div>
		<!--
		<div class="intermission-header">
			<p>Host your own InboxKitten!</p>
			<i class="fa fa-chevron-down" @click="scrollDown"></i>
		</div>
		Set up guide
		<section id="express-js">
			<h2>Coming Soon!</h2>
		</section>
		-->
	</div>
</template>
<script>
import $ from 'jquery'
import config from '@/../config/apiconfig.js'
import 'normalize.css'
import ClipboardJS from 'clipboard'
import { generate, count } from "random-words";

// import CarbonAds from './components/CarbonAds.vue'

export default {
	name: 'LandingPage',
	components: {
		// CarbonAds: CarbonAds
	},
	data () {
		return {
			randomName: ''
		}
	},
	mounted () {
		this.randomName = this.generateRandomName().toString()

		this.$clipboard = []

		let self = this

		this.$clipboard[0] = new ClipboardJS('#div-domain', {
			text: function (trigger) {
				if (self.randomName.includes('@' + config.domain)) {
					return self.randomName
				}
				return self.randomName + '@' + config.domain
			}
		})

		this.$clipboard[0].on('success', function (e) {
			$('#email-input').select()
			$('#div-domain').addClass('tooltipped tooltipped-s')
			$('#div-domain').attr('aria-label', 'Copied!')
			$('#div-domain').on('mouseleave', function () {
				$('#div-domain').removeClass('tooltipped tooltipped-s')
				$('#div-domain').removeAttr('aria-label')
			})
		})
	},
	beforeDestroy () {
		if (this.$clipboard !== null) {
			this.$clipboard.forEach(cb => {
				cb.destroy()
			})
		}
	},
	computed: {
		domain () {
			return config.domain
		}
	},
	methods: {
		generateRandomName () {
			
			return (
				generate({
					exactly: 1,
					wordsPerString: 2,
					separator: '-'
				}) +
				'-' +
				Math.floor(Math.random() * 90 + 10)
			)
		},
		goToInbox () {
			this.$router.push({
				name: 'Inbox',
				params: {
					email: this.randomName
				}
			})
		},
		goToKittenRouter () {
			location.href = '/kittenrouter'
		}
	}
}
</script>

<style lang="scss" rel="stylesheet/scss">
	@import url("https://use.fontawesome.com/releases/v5.3.1/css/all.css");
	@import "scss/landingpage.scss";
	@import "primer-tooltips/index.scss";

	.landing-navigation {
		display: flex;
		justify-content: flex-end;
		align-items: center;
		float: right;
		width: 100vw;
		height: 1rem;
		text-align: left;

		.landing-nav-main-logo {
			width: 8rem;
			padding-top: 7rem;
			padding-right: 4rem;
			z-index: 2;
		}

		.landing-nav-header {
			padding-top: 6rem;
			padding-right: 3rem;
			cursor: pointer;
		}
	}

	@media only screen and (max-width: 470px) {
		.landing-navigation .landing-nav-main-logo {
			padding-right: 4rem;
			width: 5rem;
			z-index: 3;
		}
		.landing-nav-header {
			display: none;
		}
	}
	@media only screen and (max-width: 800px) {
		.landing-navigation {
			padding-top: 140px;
		}
	}
</style>


================================================
FILE: ui/src/main.js
================================================
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from "./App.vue"
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: {App},
  template: '<App/>',
  created () {
    this.$eventHub = new Vue({
      name: 'EventHub',
      parent: this,
      functional: true
    })
  }
})



================================================
FILE: ui/src/router/index.js
================================================
import Vue from 'vue'
import Vuex from 'vuex'
import Router from 'vue-router'
import LandingPage from '@/landingpage.vue'
import KittenRouter from '@/kittenrouter.vue'
import Inbox from '@/components/mail/inbox.vue'
import MessageDetail from '@/components/mail/message_detail.vue'
import MessageList from '@/components/mail/message_list.vue'
import vuescroll from 'vuescroll'
import 'vuescroll/dist/vuescroll.css'

Vue.mixin({
  created () {
    // pass the event hub down to descendents
    if (!this.$eventHub && this.$root.$eventHub) {
      this.$eventHub = this.$root.$eventHub
    }
  }
})

Vue.use(Vuex)
Vue.use(Router)
Vue.use(vuescroll)

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Kitten Land',
      component: LandingPage
    },
    {
      path: '/inbox/:email',
      name: 'Inbox',
      redirect: {name: 'List'},
      component: Inbox,
      children: [
        {
          path: '',
          redirect: {name: 'List'}
        },
        {
          path: 'list',
          name: 'List',
          component: MessageList
        },
        {
          path: 'message/:region/:key',
          name: 'Message',
          component: MessageDetail
        },
        {
          path: '*',
          redirect: {name: 'List'}
        }
      ]
    },
    {
      path: '/kittenrouter',
      name: 'KittenRouter',
      component: KittenRouter
    },
    {
      path: '*',
      redirect: {name: 'Kitten Land'}
    }
  ]
})


================================================
FILE: ui/src/scss/_color.scss
================================================
//-----------------------------------------------
// Base Color
//-----------------------------------------------
$color1-base: #119DA4;
$color2-base: #0C7489;
$color3-base: #13505B;
$color4-base: #040404;
$color5-base: #D7D9CE;
$domain-base: #d7d9ce; //#06FFAB;

//-----------------------------------------------
// Dark Color varient
//-----------------------------------------------
$color1-dark: darken($color1-base, 10%);
$color2-dark: darken($color2-base, 10%);
$color3-dark: darken($color3-base, 10%);
$color4-dark: darken($color4-base, 10%);
$color5-dark: darken($color5-base, 10%);

//-----------------------------------------------
// Light Color varient
//-----------------------------------------------
$color1-light: lighten($color1-base, 10%);
$color2-light: lighten($color2-base, 10%);
$color3-light: lighten($color3-base, 10%);
$color4-light: lighten($color4-base, 10%);
$color5-light: lighten($color5-base, 10%);

//-----------------------------------------------
// Text colors
//-----------------------------------------------
$bright-text: #FFFFFF;
$dark-text: #040404;

//-----------------------------------------------
// CALL TO ACTION - colors
//-----------------------------------------------
$cta-base: #FF4377;
$cta-base-text: #FFFFFF;
$cta-hover: #ff0;
$cta-hover-text: #040404;

//-----------------------------------------------
// Header gradient background
//-----------------------------------------------
.header-gradient-background {
	background: #36D1DC;  /* fallback for old browsers */
	background: -webkit-linear-gradient(to right, #5B86E5, #36D1DC);  /* Chrome 10-25, Safari 5.1-6 */
	background: linear-gradient(to right, #5B86E5, #36D1DC); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}


================================================
FILE: ui/src/scss/_common.scss
================================================
// Fix scroll alignment issues
html,
body,
#app {
  min-height: 100%;
  height: 100%;
  position: relative;
}

html {
  min-height: 600px;
  margin: 0;
  padding: 0;
  overflow-x: hidden;
}

// Flex

.flex-row {
  display: flex;
  flex-direction: row;
}

// Text alignment

.text-left {
  text-align: left;
}

.text-center {
  text-align: center;
}

.text-right {
  text-align: right;
}

// Code

.code,
.code pre {
  font-family: monospace;

  text-align: left;

  background: #272822;
  color: white;

  overflow: auto;

  width: auto;

  border-radius: 5px;

  padding: 1em;
  margin-bottom: 1em;
}

// Margins

.m-0 {
  margin: 0;
}

.m-1 {
  margin: 1rem;
}

.ml-1 {
  margin-left: 1rem;
}

.mr-1 {
  margin-right: 1rem;
}

.mb-0 {
  margin-bottom: 0;
}

.mb-1 {
  margin-bottom: 1rem;
}

.mb-2 {
  margin-bottom: 1rem;
}

.mx-a {
  margin-left: auto;
  margin-right: auto;
}

.mx-1 {
  margin-left: 1rem;
  margin-right: 1rem;
}

.my-0 {
  margin-top: 0;
  margin-bottom: 0;
}

.my-1 {
  margin-top: 1rem;
  margin-bottom: 1rem;
}

.my-2 {
  margin-top: 2rem;
  margin-bottom: 2rem;
}

// Padding
.p-0 {
  padding: 0;
}

.p-1 {
  padding: 1rem;
}

.p-2 {
  padding: 2rem;
}

.pl-1 {
  padding-left: 1rem;
}

.pr-1 {
  padding-right: 1rem;
}

.pb-1 {
  padding-bottom: 1rem;
}

.px-a {
  padding-left: auto;
  padding-right: auto;
}

.px-1 {
  padding-left: 1rem;
  padding-right: 1rem;
}

.py-0 {
  padding-top: 0;
  padding-bottom: 0;
}

.py-1 {
  padding-top: 1rem;
  padding-bottom: 1rem;
}

.py-2 {
  padding-top: 2rem;
  padding-bottom: 2rem;
}


================================================
FILE: ui/src/scss/_producthunt.scss
================================================
//
// Product Hunt banner
//
.product-hunt {
	background-color: white;
	padding: 1rem;
	vertical-align: middle;
	color: black;
	width:100%;

	margin:auto;
	display:flex;
	flex-direction:row;
	justify-content:center;

	background: #fafad2;
	border-top: 1px solid black;
	border-bottom: 1px solid black;

	.ph-icon {
		height: 2rem;
		display:inline-block;
	}

	p {
		display:inline-block;
		padding:0;
		margin:0;

		margin-left:1rem;
		margin-top:0.5rem;
	}

	cursor: pointer;
	&:hover {
		background: darken(#fafad2, 25%);
	}

	@media (max-width:400px){
		font-size:14px;
		padding-left:0;
	}
}


================================================
FILE: ui/src/scss/landingpage.scss
================================================

// Loading of font awesome
// @import url("https://use.fontawesome.com/releases/v5.3.1/css/all.css");

// Loading of color scheme
@import "_color.scss";

// Min-width for body (for some sanity)
body {
	min-width: 280px;
	background: white;
}

//
// Landing page header and logo
//
.header {
	padding-top: 3rem;
	padding-bottom: 2rem;
	width: 100vw;

	.logo {
		width:40vw;
		max-width: 800px;
		margin-bottom:1rem;
	}

	h1 {
		color:$bright-text;
		padding-left:2rem;
		padding-right:2rem;
	}
	h2 {
		color:$dark-text;
		padding-left:2rem;
		padding-right:2rem;
	}

	h1,h2 {
		margin:0;
	}

	@media only screen and (max-width:760px) {
		.logo {
			width: 80vw;
		}

		// Line display hack to force a new block
		h1 a {
			display:block;
		}
	}
}

//
// Email selection box
//
.email-selection {
	width: 100vw;

	padding-top: 1rem;
	padding-bottom: 3rem;

	// Adust the input box elements borders
	// $input-box-el-border-radius: 0.4rem;
	$input-box-el-border-radius: 0rem;

	// The main input box
	.input-box {

		// Remove space between inline-blocks
		font-size:0;

		// Input-box radius : note you will need
		// to update input left top/bottom radius as well
		background-color:$color5-base;
		display: inline-block;
		
		border-radius: $input-box-el-border-radius;
		border: 3px solid black;

		// Common settings for input, and suffix label
		.input-email, .input-suffix {
			padding:0;
			margin:0;
			font-size:1rem;
			padding-top:0.25rem;
			padding-bottom:0.25rem;
		}

		// Input email styling
		.input-email {
			border:0px;
			width: 12rem;
			display: inline-block;
			text-align:center;

			// Border radius overwrite
			border-top-left-radius: $input-box-el-border-radius;
			border-bottom-left-radius: $input-box-el-border-radius;
			border-top-right-radius: 0rem;
			border-bottom-right-radius: 0rem;

			// input text color
			color: $dark-text;

			// Right border
			border-right: 3px solid black;
		}

		// Input email suffix
		.input-suffix {
			width: 10rem;
			display: inline-block;
			padding-left:1rem;
			padding-right:1rem;

			// Text with subtle fade out
			color: fade-out($dark-text, 0.3);

		}
	}

	// Submit box
	.submit-box {
		display:inline-block;
		margin-left:1rem;
	}
	// Submission button styling
	.submit  {
		padding: 0.25rem 1rem 0.25rem 1rem;
		margin:0;
		border: 0;

		font-size:1rem;
		font-weight: bold;

		background: $cta-base;
		color: $cta-base-text;

		border-radius: $input-box-el-border-radius;

		font-size: 100%;
		font-family: inherit;
		text-transform: none;
		-webkit-appearance: button;
		border: 3px solid black;
	}

	// Hover submit button styling
	.submit:hover {
		background: $cta-hover;
		color: $cta-hover-text;
	}

	/*
	@media only screen and (max-width:760px) {
		// Collapes the inbox input into 2 lines
		.input-box {
			display: inline-block;
			vertical-align: middle;
			width: 12rem;
			.input-email {
				border-top-right-radius: $input-box-el-border-radius;
				border-bottom-left-radius: 0;
			}
		}

		// Increase submit button height
		.submit-box {
			display: inline-block;
			vertical-align: middle;
			.submit {
				display: inline;
				height: 3.5rem;
			}
		}
	}
	*/

	@media only screen and (max-width:760px) {
		// Collapes the inbox input into 2 lines
		.input-box {
			display: inline-block;
			vertical-align: middle;
			width: 75%;
			.input-email {
				border-top-right-radius: $input-box-el-border-radius;
				border-bottom-left-radius: 0;
				width: 100%;
				height:2rem;
			}
		}

		// Increase submit button height
		.submit-box {
			margin-top:1.25rem;
			margin-left:0;

			display: block;
			vertical-align: middle;

			.submit {
				display: inline;
				width: 75%;
				height:2.5rem;
			}
		}
	}
}

//
// Love notes
//
.love-notes {
	padding: 1rem;
	background: white;

	p {
		font-size:1.2rem;
	}
}

//
// Line break
//
.line-break {
	width: 80vw;
	background: black;
	margin: auto;
	height: 1px;
}

//
// Deployment guide content
//
.info-guide {
	padding: 1rem;

	.features {
		padding-top:2rem;
		margin:auto;
		display:flex;
		flex-direction:row;
		justify-content:center;
	}

	.feature-card {
		flex-grow: 1;
		flex-shrink: 1;
		flex-basis: 0;
		padding-left:1rem;
		padding-right:1rem;
	}
	
	.feature-card-hover:hover {
		background-color:rgba(30,30,30,0.2);
		border-radius: 8px;
	}

	.fas {
		margin-right:0.5rem;
	}

	@media only screen and (max-width:800px) {
		.features {
			flex-direction:column;
		}
	}

	@media only screen and (max-width:1080px) {
		.steps {
			flex-direction: column;
		}
	}
}

.snippet {
	padding: 1rem;
	display: flex;
	flex-direction: row;
	margin:auto;
	justify-content: center;
	align-items: center;

	.features {
		padding: 0;
		background-color:rgba(30,30,30,0.1); 
		border-radius:8px;
	}
	.features:hover {
		background-color:rgba(30,30,30,0.2);
	}

	.feature-card {
		flex-grow: 1;
		flex-shrink: 1;
		flex-basis: 0;
		padding-left:1rem;
		padding-right:0.5rem;
		padding-top: 1rem;
		align-self: center;
	}

	.feature-gif {
		flex-grow: 1;
		flex-shrink: 1;
		flex-basis: 0;
		align-self: flex-end;
		padding-left:0.5rem;
		padding-right:1rem;
		padding-bottom: 1rem;
		padding-top: 1rem;
		
		img {
			width: 100%;
			height: 100%;
		}
	}
	
	.hljs {
		text-align: left;
		border-radius: 8px;
		display: block; 
		overflow-x: auto; 
		margin-top: 2rem;
		padding: 0.5em;
		background: rgb(35, 36, 31); 
		color: rgb(248, 248, 242);
	}

	@media only screen and (max-width:1170px) {
		.features {
			flex-direction:column;
			pre {
				font-size:0.75rem;
			}
		}

		.feature-gif {
			padding-top: 0;
			padding-left: 1rem;
		}
	}

	@media only screen and (max-width:630px){
		.feature-card {
			padding-right:1rem;
			img {
				width:100%;
				height: auto;
			}
			pre {
				font-size:0.5rem;
			}
		}

		.hljs {
			margin-top: 1rem;
		}
	}
}

.line-break {
	height:2px;

	// Fallback
	background:black;
	// Gradient
	background:linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.2), rgba(0,0,0,0));

	width:50%;
	margin:auto;
	margin-top:2rem;
	// margin-bottom:4rem;
}

.deploy-code {
	width: 30rem;
	max-width:90%;
	margin:auto;
	border-radius: 8px;
	scroll-behavior: auto;
}

.config-code {
	font-size:0.8rem;
	width:50rem;
	max-width: 90%;
	background: #F6F8FF;
	margin:auto;
	border-radius: 8px;
	scroll-behavior: auto;
	text-align:justify;
}

@media only screen and (max-width:800px) {
	.deploy-code {
		font-size:0.75rem;
	}

	.config-code {
		font-size: 0.75rem;
	}
}

.deploy-segmet {

	p {
		padding:1rem;
	}
	h3 {
		padding-left:1rem;
		padding-right:1rem;
	}

	.self-host-tier-link {
		text-decoration: none;
		cursor: pointer;
		display:inline-block;
		width:12rem;
	}

	.self-host-tier {
		border:3px solid black;
		border-radius:5px;
		display:flex;
		flex-direction: column;
		padding:0;
		max-width:12rem;
		margin:auto;
		background:white;

		text-decoration: none;

		.tier-title {
			
			margin:0;
			background: #5a86e4;
			padding:1rem;
			border-bottom:2px solid black;

			h3 {
				display: inline-block;
				color:white;
				margin:0;
				padding:0;
			}
		}

		.price {
			margin:0;
			padding:1rem;
			color:black;
			
			h3 {
				color:black;
				font-size: 4rem;
				margin:0;
				padding:0;
			}
		}
	}

	.disclaimer {
		font-size:0.75rem;
	}
}

================================================
FILE: ui/src/store/emailStore.js
================================================
import config from '@/../config/apiconfig.js'

// State
const state = {
  domain: config.domain,
  apiUrl: config.apiUrl
}

const mutations = {
}

const getters = {}

const actions = {}

export default {
  mutations,
  state,
  getters,
  actions
}


================================================
FILE: ui/uilicious-test/inboxkitten.test.js
================================================
//
// This is a https://uilicious.com/ test case
//
// See : https://test.uilicious.com/test/public/DRupiHvXGspN9wVUYNv3Re
// for an example of using this test script
//
// Alternatively signup to schedule a background task, or use their CLI =)
// #selfpromotion
//

//
// Testing for empty inbox
//

// Lets goto inbox kitten
I.goTo("https://inboxkitten.com");
I.see("Open-Source Disposable Email");

// Go to a random inbox inbox
I.fill("email", SAMPLE.id(22));
I.click("Get Mail Nyow!");

// Check that its empty
I.see("There for no messages for this kitten :(");

//
// Testing for regular email
// (sent using a jenkins perodic build)
//

// Lets go back inbox kitten mailbox
I.goTo("https://inboxkitten.com");
I.see("Open-Source Disposable Email");
I.fill("email", "ik-reciever-f7s1g28");
I.click("Get Mail Nyow!");

// See an email we expect, and click it
I.click("Testing inboxkitten subject");

// And validate the content
I.see("Testing inboxkitten text content");


================================================
FILE: ui/vite.config.js
================================================
import path from "path"
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue2'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      // resolve "@" - required for css imports
      "@": path.resolve(__dirname, "src"),
      // we need to use the vue build with the runtime template compiler included, otherwise vue will complain about the needing the runtime template compiler or to pre-compile the templates
      vue: 'vue/dist/vue.esm.js',
    }
  },
})
Download .txt
gitextract_chfcm49f/

├── .dockerignore
├── .gitignore
├── .travis.yml
├── CODE-GUIDE.md
├── DEPLOY-GUIDE-LOCALHOST.md
├── DEPLOY-GUIDE-SERVERLESS.md
├── Dockerfile
├── LICENSE
├── README.md
├── api/
│   ├── app.js
│   ├── cloudflare.js
│   ├── config/
│   │   ├── cacheControl.js
│   │   ├── kittenRouterConfig.sample.js
│   │   └── mailgunConfig.sample.js
│   ├── firebase.js
│   ├── package.json
│   ├── public/
│   │   ├── .gitignore
│   │   └── _anchor.txt
│   ├── src/
│   │   ├── api/
│   │   │   ├── mailGetHtml.js
│   │   │   ├── mailGetInfo.js
│   │   │   ├── mailGetUrl.js
│   │   │   └── mailList.js
│   │   ├── app-setup.js
│   │   ├── cloudflare-api/
│   │   │   ├── KittenRouter.js
│   │   │   ├── mailGetHtml.js
│   │   │   ├── mailGetKey.js
│   │   │   ├── mailList.js
│   │   │   └── optionsHandler.js
│   │   └── mailgunReader.js
│   ├── test/
│   │   └── mailgunReader.test.js
│   └── webpack.config.js
├── build.sh
├── cli/
│   ├── Makefile
│   ├── README.md
│   ├── go.sh
│   └── src/
│       └── inboxkitten.go
├── config.sh
├── deploy/
│   ├── cloudflare/
│   │   └── deploy.sh
│   └── firebase/
│       ├── deploy.sh
│       └── firebase.json
├── docker-dev-build.sh
├── docker-entrypoint.sh
├── docker-notes.md
└── ui/
    ├── .gitignore
    ├── README.md
    ├── commonshost.js
    ├── config/
    │   ├── apiconfig.sample.js
    │   ├── carbonads.js
    │   ├── dev.env.js
    │   ├── index.js
    │   ├── prod.env.js
    │   └── shareConfig.js
    ├── index.html
    ├── package.json
    ├── src/
    │   ├── App.vue
    │   ├── components/
    │   │   ├── CarbonAds.vue
    │   │   ├── NavBar.vue
    │   │   └── mail/
    │   │       ├── inbox.vue
    │   │       ├── message_detail.vue
    │   │       └── message_list.vue
    │   ├── kittenrouter.vue
    │   ├── landingpage.vue
    │   ├── main.js
    │   ├── router/
    │   │   └── index.js
    │   ├── scss/
    │   │   ├── _color.scss
    │   │   ├── _common.scss
    │   │   ├── _producthunt.scss
    │   │   └── landingpage.scss
    │   └── store/
    │       └── emailStore.js
    ├── uilicious-test/
    │   └── inboxkitten.test.js
    └── vite.config.js
Download .txt
SYMBOL INDEX (23 symbols across 8 files)

FILE: api/cloudflare.js
  function handleFetchEvent (line 12) | async function handleFetchEvent(event) {

FILE: api/src/api/mailGetInfo.js
  function formatName (line 55) | function formatName (sender) {

FILE: api/src/api/mailList.js
  function validateUsername (line 64) | function validateUsername(username) {

FILE: api/src/cloudflare-api/KittenRouter.js
  function getIPV4 (line 153) | function getIPV4(request, logTrueIP = false) {
  function getIPV6 (line 184) | function getIPV6(request, logTrueIP = false) {
  function logRequestWithConfigMap (line 209) | async function logRequestWithConfigMap(logConfig, request, response, rou...
  function logRequestWithConfigArray (line 300) | async function logRequestWithConfigArray(configArr, request, response, r...
  function cloneUrlWithNewOriginHostString (line 330) | function cloneUrlWithNewOriginHostString(inURL, originHostStr) {
  function setupResponseError (line 342) | function setupResponseError(errorCode, errorMsg, httpCode = 500) {
  function isGoodResponseObject (line 371) | function isGoodResponseObject(resObj) {
  function isKittenRouterException (line 381) | function isKittenRouterException(resObj) {
  function processOriginRoutingStr (line 401) | async function processOriginRoutingStr(originHostStr, inRequest) {
  function processRoutingRequest (line 426) | async function processRoutingRequest( configObj, fetchEvent, inRequest ) {
  function processFetchEvent (line 475) | async function processFetchEvent( configObj, fetchEvent ) {
  class KittenRouter (line 525) | class KittenRouter {
    method constructor (line 532) | constructor(inConfig) {
    method handleFetchEvent (line 543) | async handleFetchEvent(fetchEvent) {

FILE: api/src/cloudflare-api/mailGetKey.js
  function formatName (line 100) | function formatName (sender) {

FILE: cli/src/inboxkitten.go
  function jsonPrettifier (line 28) | func jsonPrettifier( raw string ) string {
  function doGetRequest (line 40) | func doGetRequest( url string ) []byte {
  function main (line 78) | func main() {

FILE: ui/src/main.js
  method created (line 15) | created () {

FILE: ui/src/router/index.js
  method created (line 13) | created () {
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (162K chars).
[
  {
    "path": ".dockerignore",
    "chars": 223,
    "preview": "#\n# Hidden files ignore\n#\n.*\n\n#\n# Remove uneeded files from docker context\n#\n/api/node_modules\n/ui/node_modules\n/ui/dist"
  },
  {
    "path": ".gitignore",
    "chars": 461,
    "preview": "# Hidden files\n**/.*\n\n# lib / build folders folder\n**/node_modules\n**/bin\n**/dist\n**/gopath\n\n# UI build folder ignore\nui"
  },
  {
    "path": ".travis.yml",
    "chars": 825,
    "preview": "sudo: false\nlanguage: go\n\n# Our CLI toolchain is based on node_js\nnode_js: \"8.10\"\n# Installation of subdependencies\nbefo"
  },
  {
    "path": "CODE-GUIDE.md",
    "chars": 3247,
    "preview": "# Code Guide\nIf you are interested to developing Inboxkitten, this is a brief guide of how  Inboxkitten has been structu"
  },
  {
    "path": "DEPLOY-GUIDE-LOCALHOST.md",
    "chars": 2703,
    "preview": "# Developing on localhost / Custom deployment\n\nNote: You will still need to do the mail gun setup in the firebase guide."
  },
  {
    "path": "DEPLOY-GUIDE-SERVERLESS.md",
    "chars": 2394,
    "preview": "# Serverless Deployment Guide\n\nFollow the 5 steps guide below to get started on Firebase!\n\n- [Step 0 - Clone Me](#step-0"
  },
  {
    "path": "Dockerfile",
    "chars": 2834,
    "preview": "#-------------------------------------------------------\n#\n# Base alpine images with all the runtime os dependencies\n#\n#"
  },
  {
    "path": "LICENSE",
    "chars": 1093,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Uilicious Private Limited.\n\nPermission is hereby granted, free of charge, to a"
  },
  {
    "path": "README.md",
    "chars": 3592,
    "preview": "[![inboxkitten header](./ui/static/inbox-kitten-opengraph.jpg)](https://inboxkitten.com)\n\n# Open-Source Disposable Email"
  },
  {
    "path": "api/app.js",
    "chars": 1303,
    "preview": "// Cache control settings\nconst cacheControl = require(\"./config/cacheControl\");\n\n// app package loading\nlet app = requi"
  },
  {
    "path": "api/cloudflare.js",
    "chars": 1612,
    "preview": "/**\n * Cloudflare fetch result handling\n */\naddEventListener('fetch', event => {\n\tevent.respondWith(handleFetchEvent(eve"
  },
  {
    "path": "api/config/cacheControl.js",
    "chars": 540,
    "preview": "/**\n * Configure the various level of cache controls\n */\nmodule.exports = {\n    // Frequent changing dynamic content\n   "
  },
  {
    "path": "api/config/kittenRouterConfig.sample.js",
    "chars": 1453,
    "preview": "//\n// Routing and logging options\n//\nmodule.exports = {\n\n\t// logging endpoint to use\n\tlog : [\n\t\t{\n\t\t\t// Currently only e"
  },
  {
    "path": "api/config/mailgunConfig.sample.js",
    "chars": 195,
    "preview": "//\n// API key and valid mailgun domain supported (using sandbox)\n//\nmodule.exports = {\n\t\"apiKey\"      : \"${MAILGUN_API_K"
  },
  {
    "path": "api/firebase.js",
    "chars": 680,
    "preview": "/**\n * This is configured for use within firebase cloud function (or GCP cloud functions)\n * And will be automatically r"
  },
  {
    "path": "api/package.json",
    "chars": 746,
    "preview": "{\n  \"name\": \"inboxkitten-api\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"dependencies\": {\n    "
  },
  {
    "path": "api/public/.gitignore",
    "chars": 26,
    "preview": "*\n!_anchor.txt\n!.gitignore"
  },
  {
    "path": "api/public/_anchor.txt",
    "chars": 58,
    "preview": "This directory is used for static file serving via the API"
  },
  {
    "path": "api/src/api/mailGetHtml.js",
    "chars": 1450,
    "preview": "// Loading mailgun reader and config\nconst mailgunReader = require(\"../mailgunReader\");\nconst mailgunConfig = require(\"."
  },
  {
    "path": "api/src/api/mailGetInfo.js",
    "chars": 1571,
    "preview": "// Loading mailgun reader and config\nconst mailgunReader = require(\"../mailgunReader\");\nconst mailgunConfig = require(\"."
  },
  {
    "path": "api/src/api/mailGetUrl.js",
    "chars": 830,
    "preview": "// Loading mailgun reader and config\nconst mailgunReader = require(\"../mailgunReader\");\nconst mailgunConfig = require(\"."
  },
  {
    "path": "api/src/api/mailList.js",
    "chars": 4046,
    "preview": "// Loading mailgun reader and config\nconst mailgunReader = require(\"../mailgunReader\");\nconst mailgunConfig = require(\"."
  },
  {
    "path": "api/src/app-setup.js",
    "chars": 581,
    "preview": "\n// Dependencies loading\nconst express       = require(\"express\");\nconst bodyParser    = require(\"body-parser\");\nconst c"
  },
  {
    "path": "api/src/cloudflare-api/KittenRouter.js",
    "chars": 18980,
    "preview": "/**\n * KittenRouter is a utility class used to \n * \n * - log request, for monitoring purposes (with ip masking for GDPR)"
  },
  {
    "path": "api/src/cloudflare-api/mailGetHtml.js",
    "chars": 2603,
    "preview": "/**\n * Making a curl request that looks like\n * curl -X POST --data 'key=world' example.com\n * or\n * curl -X POST --form"
  },
  {
    "path": "api/src/cloudflare-api/mailGetKey.js",
    "chars": 2722,
    "preview": "\n/**\n * Making a curl request that looks like\n * curl -X POST --data 'key=world' example.com\n * or\n * curl -X POST --for"
  },
  {
    "path": "api/src/cloudflare-api/mailList.js",
    "chars": 1652,
    "preview": "/**\n * Making a curl request that looks like\n * curl -X POST --data 'key=world' example.com\n * or\n * curl -X POST --form"
  },
  {
    "path": "api/src/cloudflare-api/optionsHandler.js",
    "chars": 755,
    "preview": "/**\n * CORS Options handling for cloudflare api\n */\nconst config = require(\"../../config/mailgunConfig\")\n\nconst corsHead"
  },
  {
    "path": "api/src/mailgunReader.js",
    "chars": 3726,
    "preview": "// AXIOS dependencies\nconst axios = require(\"axios\");\n\n/**\n* Simple axois get, with response data\n* @param {String} urlW"
  },
  {
    "path": "api/test/mailgunReader.test.js",
    "chars": 3348,
    "preview": "// Dependencies loading\nconst assert  = require('assert');\nconst delay   = require('delay');\nconst md5     = require('md"
  },
  {
    "path": "api/webpack.config.js",
    "chars": 100,
    "preview": "module.exports = {\n\toptimization: {\n\t\t// We no not want to minimize our code.\n\t\tminimize: false\n\t}\n}"
  },
  {
    "path": "build.sh",
    "chars": 526,
    "preview": "#!/bin/bash\n\n# Deploy will terminate on any error\nset -e\n\n# Project directory detection\nprojectDir=\"`dirname \\\"$0\\\"`\"\ncd"
  },
  {
    "path": "cli/Makefile",
    "chars": 267,
    "preview": "# Change this to your respective main file\nmainfile=\"inboxkitten.go\"\noutfile=\"inboxkitten\"\n\n# The go build command\ndepen"
  },
  {
    "path": "cli/README.md",
    "chars": 1733,
    "preview": "# What is go.sh\n\n> TLDR: Its a bash script that \"rewrites\" the `$GOPATH` to the folder holding `go.sh` sub `gopath` fold"
  },
  {
    "path": "cli/go.sh",
    "chars": 283,
    "preview": "#!/bin/bash\n\n# Working directory\nworkingDir=\"`dirname \\\"$0\\\"`\"\ncd \"$workingDir\" || exit 1\n\n# GOPATH overwrite\nWORKING_PW"
  },
  {
    "path": "cli/src/inboxkitten.go",
    "chars": 3962,
    "preview": "package main\n\n//---------------------------------------------------------------------------------------\n//\n// Dependenci"
  },
  {
    "path": "config.sh",
    "chars": 2012,
    "preview": "#!/bin/bash\n\n# Deploy will terminate on any error\nset -e\n\n# Firebase Working directory\nprojectDir=\"`dirname \\\"$0\\\"`\"\ncd "
  },
  {
    "path": "deploy/cloudflare/deploy.sh",
    "chars": 2990,
    "preview": "#!/bin/bash\n\n# Deploy will terminate on any error\nset -e\n\n# cloudflare Working directory\ncloudflareDir=\"`dirname \\\"$0\\\"`"
  },
  {
    "path": "deploy/firebase/deploy.sh",
    "chars": 1187,
    "preview": "#!/bin/bash\n\n# Deploy will terminate on any error\nset -e\n\n# Firebase Working directory\nfirebaseDir=\"`dirname \\\"$0\\\"`\"\ncd"
  },
  {
    "path": "deploy/firebase/firebase.json",
    "chars": 279,
    "preview": "{\n\t\"hosting\": {\n\t\t\"public\": \"public\",\n\t\t\"ignore\": [\n\t\t\t\"firebase.json\",\n\t\t\t\"**/.*\",\n\t\t\t\"**/node_modules/**\"\n\t\t],\n\t\t\"rewr"
  },
  {
    "path": "docker-dev-build.sh",
    "chars": 263,
    "preview": "#!/bin/bash\n\ndocker stop $(docker ps -a | grep $(basename \"$PWD\") | awk '{print $1}'); \ndocker rm $(docker ps -a | grep "
  },
  {
    "path": "docker-entrypoint.sh",
    "chars": 2171,
    "preview": "#!/bin/sh\n\n#\n# Entrypoint start\n#\necho \">>---------------------------------------------------------------------\"\necho \">"
  },
  {
    "path": "docker-notes.md",
    "chars": 1825,
    "preview": "# Docker build process\n\n## inside the respective folder (eg: ./ssh/)\ndocker build -t {tagname} .\ndocker build -t $(basen"
  },
  {
    "path": "ui/.gitignore",
    "chars": 154,
    "preview": ".DS_Store\nnode_modules/\n/dist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vsc"
  },
  {
    "path": "ui/README.md",
    "chars": 482,
    "preview": "# inboxkitten\n\n> A personal disposable email ui\n\n## Build Setup\n\n``` bash\n# install dependencies\nnpm install\n\n# serve wi"
  },
  {
    "path": "ui/commonshost.js",
    "chars": 700,
    "preview": "let commonsHostConfig = {\n\troot: './index.html',\n\tfallback: { 200: './index.html' },\n\tdirectories: { trailingSlash: 'nev"
  },
  {
    "path": "ui/config/apiconfig.sample.js",
    "chars": 98,
    "preview": "export default {\n\tapiUrl: '//${WEBSITE_DOMAIN}/api/v1/mail',\n\tdomain: '${MAILGUN_EMAIL_DOMAIN}'\n}\n"
  },
  {
    "path": "ui/config/carbonads.js",
    "chars": 171,
    "preview": "module.exports = {\n\t\"enable\": true,\n\t\"carbon_ad_src\": \"https://cdn.carbonads.com/carbon.js?serve=CK7D5KQW&placement=inbo"
  },
  {
    "path": "ui/config/dev.env.js",
    "chars": 156,
    "preview": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEn"
  },
  {
    "path": "ui/config/index.js",
    "chars": 2291,
    "preview": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path ="
  },
  {
    "path": "ui/config/prod.env.js",
    "chars": 61,
    "preview": "'use strict'\nmodule.exports = {\n  NODE_ENV: '\"production\"'\n}\n"
  },
  {
    "path": "ui/config/shareConfig.js",
    "chars": 306,
    "preview": "export default {\n  githubForkLink: \"https://github.com/uilicious/inboxkitten/fork\",\n  githubAriaLabel: \"Fork uilicious/i"
  },
  {
    "path": "ui/index.html",
    "chars": 1454,
    "preview": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-sca"
  },
  {
    "path": "ui/package.json",
    "chars": 778,
    "preview": "{\n  \"name\": \"inboxkitten-ui\",\n  \"version\": \"1.1.0\",\n  \"type\": \"module\",\n  \"description\": \"UI for Inboxkitten - A persona"
  },
  {
    "path": "ui/src/App.vue",
    "chars": 3299,
    "preview": "<template>\n\t<div id=\"app\">\n\t\t<!-- Github corners -->\n\t\t<a href=\"https://github.com/uilicious/inboxkitten\" class=\"github-"
  },
  {
    "path": "ui/src/components/CarbonAds.vue",
    "chars": 2904,
    "preview": "<template>\n\t<div id=\"_carbon_ads_div\"></div>\n</template>\n\n<script>\n\timport config from '@/../config/carbonads.js'\n\texpor"
  },
  {
    "path": "ui/src/components/NavBar.vue",
    "chars": 4967,
    "preview": "<template>\n\t<nav class=\"nav\">\n\t\t<div class=\"back-button\" @click=\"backAPage\"><i class=\"fa fa-arrow-left fa-3x\"/></div>\n\t\t"
  },
  {
    "path": "ui/src/components/mail/inbox.vue",
    "chars": 460,
    "preview": "<template>\n  <div class=\"wrapper\">\n    <nav-bar class=\"nav-bar\"></nav-bar>\n    <router-view class=\"inbox\"></router-view>"
  },
  {
    "path": "ui/src/components/mail/message_detail.vue",
    "chars": 2275,
    "preview": "<template>\n\t<div class=\"message-details\">\n\t\t<div class=\"subject\">{{emailContent.subject}}</div>\n\t\t<div class=\"meta-info\""
  },
  {
    "path": "ui/src/components/mail/message_list.vue",
    "chars": 6663,
    "preview": "<template>\n    <vue-scroll :ops=\"vueScrollBarOps\">\n      <div class=\"table-box advisory\" style=\"\n          background: #"
  },
  {
    "path": "ui/src/kittenrouter.vue",
    "chars": 8575,
    "preview": "<template>\n\t<div>\n\t\t<!-- <carbon-ads placement=\"InboxKittenLanding\"></carbon-ads> -->\n\t\t<div class=\"kittenrouter-navigat"
  },
  {
    "path": "ui/src/landingpage.vue",
    "chars": 10713,
    "preview": "<template>\n\t<div class=\"landing-page\">\n\t\t<!-- <carbon-ads placement=\"InboxKittenLanding\"></carbon-ads> -->\n\t\t<!-- <div><"
  },
  {
    "path": "ui/src/main.js",
    "chars": 488,
    "preview": "// The Vue build version to load with the `import` command\n// (runtime-only or standalone) has been set in webpack.base."
  },
  {
    "path": "ui/src/router/index.js",
    "chars": 1488,
    "preview": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport Router from 'vue-router'\nimport LandingPage from '@/landingpage.vue"
  },
  {
    "path": "ui/src/scss/_color.scss",
    "chars": 1754,
    "preview": "//-----------------------------------------------\n// Base Color\n//-----------------------------------------------\n$color"
  },
  {
    "path": "ui/src/scss/_common.scss",
    "chars": 1556,
    "preview": "// Fix scroll alignment issues\nhtml,\nbody,\n#app {\n  min-height: 100%;\n  height: 100%;\n  position: relative;\n}\n\nhtml {\n  "
  },
  {
    "path": "ui/src/scss/_producthunt.scss",
    "chars": 596,
    "preview": "//\n// Product Hunt banner\n//\n.product-hunt {\n\tbackground-color: white;\n\tpadding: 1rem;\n\tvertical-align: middle;\n\tcolor: "
  },
  {
    "path": "ui/src/scss/landingpage.scss",
    "chars": 7232,
    "preview": "\n// Loading of font awesome\n// @import url(\"https://use.fontawesome.com/releases/v5.3.1/css/all.css\");\n\n// Loading of co"
  },
  {
    "path": "ui/src/store/emailStore.js",
    "chars": 249,
    "preview": "import config from '@/../config/apiconfig.js'\n\n// State\nconst state = {\n  domain: config.domain,\n  apiUrl: config.apiUrl"
  },
  {
    "path": "ui/uilicious-test/inboxkitten.test.js",
    "chars": 975,
    "preview": "//\n// This is a https://uilicious.com/ test case\n//\n// See : https://test.uilicious.com/test/public/DRupiHvXGspN9wVUYNv3"
  },
  {
    "path": "ui/vite.config.js",
    "chars": 535,
    "preview": "import path from \"path\"\nimport { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue2'\n\n// https://vitejs.dev"
  }
]

About this extraction

This page contains the full source code of the uilicious/inboxkitten GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (141.8 KB), approximately 42.1k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!