Skip to content

Commit 6328212

Browse files
authored
Merge pull request #37 from chriskyfung/refactor/improve-code-and-docs
feat: Introduce dateFormatterFactory and update documentation
2 parents bec98a2 + 90eed32 commit 6328212

File tree

6 files changed

+187
-73
lines changed

6 files changed

+187
-73
lines changed

README.md

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,85 @@
1-
# Gmail Old Email Cleaner
1+
# Gmail Regex Cleaner
22

33
[![Build Status](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/actions/workflows/ci.yml/badge.svg)](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/actions/workflows/ci.yml)
44
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
55
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](/LICENSE)
6+
[![GitHub issues](https://img.shields.io/github/issues/chriskyfung/gmail-regex-cleaner-apps-script)](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/issues)
7+
[![GitHub stars](https://img.shields.io/github/stars/chriskyfung/gmail-regex-cleaner-apps-script)](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/stargazers)
68

7-
This is a Google Apps Script project that helps you delete old emails in Gmail that match your custom regex filters. This can help you save space and keep your inbox organized.
9+
A Google Apps Script that helps you delete old emails in Gmail that match your custom regex filters. This can help you save space and keep your inbox organized.
810

9-
## Features
11+
## Table of Contents
1012

11-
- Delete old emails in Gmail that match a regular expression
12-
- Specify the number of days to keep the emails
13-
- Exclude starred, important, or labeled emails from deletion
14-
- Run the script manually or on a schedule
15-
- Log the deleted emails and errors
13+
* [About The Project](#about-the-project)
14+
* [Getting Started](#getting-started)
15+
* [Usage](#usage)
16+
* [Development](#development)
17+
* [Contributing](#contributing)
18+
* [License](#license)
19+
* [Disclaimer](#disclaimer)
1620

17-
## How to use
21+
## About The Project
22+
23+
This project provides a flexible way to automatically clean up your Gmail inbox by deleting old emails that match specific criteria defined by regular expressions. It's perfect for managing recurring emails like newsletters, notifications, and alerts that you don't need to keep forever.
24+
25+
### Features
26+
27+
* Delete old emails in Gmail that match a regular expression
28+
* Specify the number of days to keep the emails
29+
* Exclude starred, important, or labeled emails from deletion
30+
* Run the script manually or on a schedule
31+
* Log the deleted emails and errors
32+
33+
## Getting Started
34+
35+
> [!WARNING]
36+
> This script will delete your emails permanently, without moving them to the trash. Please use it with caution and make sure you have a backup of your important emails. You can run the script with the `isDryRun` option set to `true` first to see what emails will be deleted.
37+
38+
This section will guide you through the process of setting up and running the script.
39+
40+
### Prerequisites
41+
42+
* A Google account with access to Gmail and Google Drive.
43+
44+
### Installation
45+
46+
1. Create a new Google Apps Script project in Google Drive.
47+
2. Copy and paste the code from `dist/code.js` and `dist/examples.js` into the script editor.
48+
3. From the `examples.js` file, choose a function that matches your needs, or create a new one. You can then run this function from the Apps Script editor.
49+
50+
For example, to run one of the pre-made functions, you would select it in the editor's function list and click **Run**.
51+
52+
> [!IMPORTANT]
53+
> When running the script for the first time, you may need to authorize it to access your Gmail account.
54+
55+
4. Optionally, set up a trigger to run a function periodically. You can do this by clicking the **Triggers** icon in the left sidebar, then clicking the **Add a trigger** button, and choosing the options you want. For example, you can set the script to run every day, week, or month.
56+
57+
## Usage
1858

1959
For detailed instructions on how to set up and use this script, please see the [**Usage Guide**](./docs/usage.md).
2060

2161
## Development
2262

2363
This project uses ESLint for linting, Prettier for formatting, Jest for testing, and Rollup for building. For more details on the development setup and build process, please see the [**Development Guide**](./docs/development.md).
2464

25-
## Disclaimer
65+
## Contributing
2666

27-
This script is provided as is, without any warranty or liability. Use it at your own risk. Make sure to test the script before using it on your Gmail account. The script may delete emails that you want to keep, or fail to delete emails that you want to remove. The script may also exceed the quota limits of Google Apps Script or Gmail API, resulting in errors or partial execution. The author is not responsible for any loss or damage caused by the use of this script.
67+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
68+
69+
1. Fork the Project
70+
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
71+
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
72+
4. Push to the Branch (`git push origin feature/AmazingFeature`)
73+
5. Open a Pull Request
74+
75+
Please read the [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing.
76+
77+
If you have a bug report or a feature request, please open an issue on the [GitHub Issues page](https://github.com/chriskyfung/gmail-regex-cleaner-apps-script/issues).
2878

2979
## License
3080

3181
This project is distributed under the AGPL-3.0 license. You can use, modify, and distribute this project, as long as you comply with the terms and conditions in the [LICENSE](/LICENSE) file.
82+
83+
## Disclaimer
84+
85+
This script is provided as is, without any warranty or liability. Use it at your own risk. Make sure to test the script before using it on your Gmail account. The script may delete emails that you want to keep, or fail to delete emails that you want to remove. The script may also exceed the quota limits of Google Apps Script or Gmail API, resulting in errors or partial execution. The author is not responsible for any loss or damage caused by the use of this script.

docs/development.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,22 @@ To update the timezone, run the following command **after** the build command:
6262
```bash
6363
npm run update-timezone
6464
```
65+
66+
### `dateFormatterFactory`
67+
68+
The `dateFormatterFactory` function is a helper function that creates a `dateFormatter` function for you. It takes a regular expression pattern and an optional boolean `useLastMessageYear` as arguments.
69+
70+
The pattern should contain named capture groups for `year`, `month1`, `month2` (optional), and `enddate`. The factory will then generate a function that extracts these parts from a date string and returns a formatted date string.
71+
72+
Here is an example of how to use it:
73+
74+
```js
75+
const dateFormatter = dateFormatterFactory(
76+
/(?<month1>\w{3})\s\d+-(?<month2>\w{3})?\s?(?<enddate>\d+)/
77+
);
78+
79+
// The generated dateFormatter can then be passed to the main function.
80+
main(query, pattern, { dateFormatter });
81+
```
82+
83+
This is useful for creating complex date formatters without writing the same boilerplate code every time.

docs/usage.md

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
# How to Use
2-
3-
1. Create a new Google Apps Script project in Google Drive.
4-
2. Copy and paste the code from `dist/code.js` and `dist/examples.js` into the script editor.
5-
3. From the `examples.js` file, choose a function that matches your needs, or create a new one. You can then run this function from the Apps Script editor.
6-
7-
For example, to run one of the pre-made functions, you would select it in the editor's function list and click **Run**.
8-
9-
The `main` function, which does the core work, has been updated to be more flexible. Here is how you would call it inside a custom function:
1+
The `main` function, which does the core work, has been updated to be more flexible. Here is how you would call it inside a custom function:
102

113
```js
124
function removeOldGoogleAlerts() {
@@ -41,13 +33,7 @@
4133
> - **`isDryRun`**: A boolean value that indicates whether to run the script in test mode or not. If `true` (the default), the script will only log the emails that match the query and the regex, but will not delete them. If `false`, the script will delete the emails permanently. It is recommended to run the script with `isDryRun` set to `true` first to make sure it works as expected.
4234
> - **`mode`**: A string that specifies whether to process the email body as plain text or HTML. Can be either `'plain'` (default) or `'html'`. If set to `'html'`, the script will strip all HTML tags from the email body before searching for the regex pattern.
4335
> - **`dateFormatter`**: A function that takes two parameters: `textWithDate` and `lastMessageDate`. The `textWithDate` is a string that contains the date part extracted from the email body. The `lastMessageDate` is a date object that represents the latest date of the email thread. The function should return a date string like `yyyy-MM-dd` that can be parsed by `new Date()`.
36+
> > [!TIP]
37+
> > For common date formats, you can use the `dateFormatterFactory` function to create a `dateFormatter` for you. See the [Development Guide](./development.md#dateformatterfactory) for more details.
4438
4539
4. Save and run the desired function from the script editor. You can use the **Run** menu or the **Run** button in the toolbar.
46-
47-
> [!IMPORTANT]
48-
> When running the script for the first time, you may need to authorize it to access your Gmail account.
49-
50-
5. Optionally, set up a trigger to run a function periodically. You can do this by clicking the **Triggers** icon in the left sidebar, then clicking the **Add a trigger** button, and choosing the options you want. For example, you can set the script to run every day, week, or month.
51-
52-
> [!WARNING]
53-
> This script will delete your emails permanently, without moving them to the trash. Please use it with caution and make sure you have a backup of your important emails. You can run the script with the `isDryRun` option set to `true` first to see what emails will be deleted.

src/code.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,32 @@ function findMatchGroup(text, pattern) {
103103
return result === null ? null : result.groups?.exp || true;
104104
}
105105

106+
/**
107+
* Creates a date formatter function from a regular expression pattern.
108+
* @param {RegExp} pattern - The regular expression to find the date parts.
109+
* @param {boolean} [useLastMessageYear=true] - Whether to use the last message's year.
110+
* @returns {function(string, Date): string|null} A function that takes text and last message date, and returns a formatted date string or null.
111+
*/
112+
function dateFormatterFactory(pattern, useLastMessageYear = true) {
113+
return (textWithDate, lastMessageDate) => {
114+
const matches = pattern.exec(textWithDate);
115+
if (!matches) {
116+
return null;
117+
}
118+
const { groups } = matches;
119+
const year = useLastMessageYear
120+
? Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy')
121+
: groups.year;
122+
const month = groups.month2 || groups.month1;
123+
const day = groups.enddate;
124+
125+
if (day && month) {
126+
return `${year}-${month}.${day}`;
127+
}
128+
return null;
129+
};
130+
}
131+
106132
/**
107133
* The main function searches for messages that match a given query string, extracts text using a
108134
* pattern, and checks if the extracted text is overdue based on a date formatter function.
@@ -174,6 +200,7 @@ globalThis.findMatchGroup = findMatchGroup;
174200
globalThis.getLastMessageDate = getLastMessageDate;
175201
globalThis.findMessages = findMessages;
176202
globalThis.main = main;
203+
globalThis.dateFormatterFactory = dateFormatterFactory;
177204

178205
if (typeof module !== 'undefined' && module.exports) {
179206
module.exports = {
@@ -182,5 +209,6 @@ if (typeof module !== 'undefined' && module.exports) {
182209
findMatchGroup,
183210
getLastMessageDate,
184211
main,
212+
dateFormatterFactory,
185213
};
186214
}

src/examples.js

Lines changed: 15 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* global dateFormatterFactory */
2+
13
/**
24
* The function `removeAboutMeWeeklyStats()` removes weekly stats emails from about.me with the subject
35
* line "your weekly stats from about.me" from the trash folder. The regex pattern looks for instances
@@ -32,19 +34,9 @@ function removeBrookstoneAffiliateInfo() {
3234
const datePattern =
3335
// eslint-disable-next-line no-useless-escape
3436
/Brookstone:[\w\s\d!$%\/-]*?(?<exp>(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{1,2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)?\s?\d{1,2})/gm;
35-
const dateFormatter = (textWithDate, lastMessageDate) => {
36-
const year = Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy');
37-
const re = /(?<month1>\w{3})\s\d+-(?<month2>\w{3})?\s?(?<enddate>\d+)/;
38-
const matches = re.exec(textWithDate);
39-
if (!matches) {
40-
return null;
41-
}
42-
if (matches.groups.month2) {
43-
return `${year}-${matches.groups.month2}.${matches.groups.enddate}`;
44-
} else {
45-
return `${year}-${matches.groups.month1}.${matches.groups.enddate}`;
46-
}
47-
};
37+
const dateFormatter = dateFormatterFactory(
38+
/(?<month1>\w{3})\s\d+-(?<month2>\w{3})?\s?(?<enddate>\d+)/
39+
);
4840
main(query, datePattern, { isDryRun: false, dateFormatter });
4941
}
5042

@@ -92,15 +84,10 @@ function removeIATeamInfo() {
9284
function removeMoneyHeroInfo() {
9385
const query = 'from:(MoneyHero <noreply@promo.moneyhero.com.hk>) is:trash';
9486
const datePattern = /\d{4}\d{1,2}\d{1,2}/gm;
95-
const dateFormatter = (textWithDate, lastMessageDate) => {
96-
const re =
97-
/(?<year>\d{4})(?<month1>\d{1,2})(?<enddate>\d{1,2})/;
98-
const matches = re.exec(textWithDate);
99-
if (!matches) {
100-
return null;
101-
}
102-
return `${matches.groups.year}-${matches.groups.month1}.${matches.groups.enddate}`;
103-
};
87+
const dateFormatter = dateFormatterFactory(
88+
/(?<year>\d{4})(?<month1>\d{1,2})(?<enddate>\d{1,2})/,
89+
false
90+
);
10491
main(query, datePattern, { isDryRun: false, dateFormatter });
10592
}
10693

@@ -114,15 +101,9 @@ function removeNamecheapAffiliateInfo() {
114101
const query = 'from:(Namecheap Affiliate Team) -label:affiliate-program';
115102
const datePattern =
116103
/(?<exp>(January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}-\d{1,2}\.?$)/gm;
117-
const dateFormatter = (textWithDate, lastMessageDate) => {
118-
const year = Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy');
119-
const re = /(?<month1>\w+)\s(\d+-)?s?(?<enddate>\d+)/;
120-
const matches = re.exec(textWithDate);
121-
if (!matches) {
122-
return null;
123-
}
124-
return `${year}-${matches.groups.month1}.${matches.groups.enddate}`;
125-
};
104+
const dateFormatter = dateFormatterFactory(
105+
/(?<month1>\w+)\s(\d+-)?s?(?<enddate>\d+)/
106+
);
126107
main(query, datePattern, { isDryRun: false, mode: 'html', dateFormatter });
127108
}
128109

@@ -146,18 +127,8 @@ function removeWondershareAffiliateInfo() {
146127
function removeYandexWebmasterInfo() {
147128
const query = 'from:(Yandex.Webmaster <devnull@webmaster.yandex.ru>)';
148129
const datePattern = /for the week of (?<exp>\d{1,2} ?\w* \W \d{1,2} \w+)/gm;
149-
const dateFormatter = (textWithDate, lastMessageDate) => {
150-
const year = Utilities.formatDate(lastMessageDate, 'GMT', 'yyyy');
151-
const re = /\d{1,2} ?(?<month1>\w*) \W ?(?<enddate>\d{1,2}) (?<month2>\w+)/;
152-
const matches = re.exec(textWithDate);
153-
if (!matches) {
154-
return null;
155-
}
156-
if (matches.groups.month2) {
157-
return `${year}-${matches.groups.month2}.${matches.groups.enddate}`;
158-
} else {
159-
return `${year}-${matches.groups.month1}.${matches.groups.enddate}`;
160-
}
161-
};
130+
const dateFormatter = dateFormatterFactory(
131+
/\d{1,2} ?(?<month1>\w*) \W ?(?<enddate>\d{1,2}) (?<month2>\w+)/
132+
);
162133
main(query, datePattern, { isDryRun: false, dateFormatter });
163134
}

test/code.test.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { checkOverdue, findMessages } = require('../src/code.js');
1+
const { checkOverdue, findMessages, dateFormatterFactory } = require('../src/code.js');
22

33
describe('Core Functions', () => {
44

@@ -94,3 +94,59 @@ describe('findMessages', () => {
9494
expect(result[0].plainBody).toBe('Hello');
9595
});
9696
});
97+
98+
describe('dateFormatterFactory', () => {
99+
global.Utilities = {
100+
formatDate: jest.fn((date, timeZone, format) => {
101+
if (format === 'yyyy') {
102+
return date.getFullYear().toString();
103+
}
104+
return '';
105+
}),
106+
};
107+
108+
it('should return a function', () => {
109+
const pattern = /a/;
110+
const formatter = dateFormatterFactory(pattern);
111+
expect(typeof formatter).toBe('function');
112+
});
113+
114+
it('should return null if pattern does not match', () => {
115+
const pattern = /a/;
116+
const formatter = dateFormatterFactory(pattern);
117+
const result = formatter('b', new Date());
118+
expect(result).toBeNull();
119+
});
120+
121+
it('should format date correctly using last message year', () => {
122+
const pattern = /(?<month1>\w{3})\s(?<enddate>\d{1,2})/;
123+
const formatter = dateFormatterFactory(pattern);
124+
const lastMessageDate = new Date('2023-01-01');
125+
const result = formatter('Jan 15', lastMessageDate);
126+
expect(result).toBe('2023-Jan.15');
127+
});
128+
129+
it('should format date correctly using year from pattern', () => {
130+
const pattern = /(?<year>\d{4})-(?<month1>\w{3})-(?<enddate>\d{1,2})/;
131+
const formatter = dateFormatterFactory(pattern, false);
132+
const result = formatter('2024-Feb-20', new Date());
133+
expect(result).toBe('2024-Feb.20');
134+
});
135+
136+
it('should use month2 if available', () => {
137+
const pattern =
138+
/(?<month1>\w{3})\s\d{1,2}\s-\s(?<month2>\w{3})\s(?<enddate>\d{1,2})/;
139+
const formatter = dateFormatterFactory(pattern);
140+
const lastMessageDate = new Date('2023-01-01');
141+
const result = formatter('Jan 10 - Feb 20', lastMessageDate);
142+
expect(result).toBe('2023-Feb.20');
143+
});
144+
145+
it('should return null if day or month is missing', () => {
146+
const pattern = /(?<month1>\w{3})/;
147+
const formatter = dateFormatterFactory(pattern);
148+
const lastMessageDate = new Date('2023-01-01');
149+
const result = formatter('Jan', lastMessageDate);
150+
expect(result).toBeNull();
151+
});
152+
});

0 commit comments

Comments
 (0)