diff --git a/backend/mailru/mailru.go b/backend/mailru/mailru.go
index f799aec3e..6afd563bc 100644
--- a/backend/mailru/mailru.go
+++ b/backend/mailru/mailru.go
@@ -27,6 +27,7 @@ import (
 	"github.com/rclone/rclone/fs/config/configmap"
 	"github.com/rclone/rclone/fs/config/configstruct"
 	"github.com/rclone/rclone/fs/config/obscure"
+	"github.com/rclone/rclone/fs/encodings"
 	"github.com/rclone/rclone/fs/fserrors"
 	"github.com/rclone/rclone/fs/fshttp"
 	"github.com/rclone/rclone/fs/hash"
@@ -41,6 +42,8 @@ import (
 	"golang.org/x/oauth2"
 )
 
+const enc = encodings.Mailru
+
 // Global constants
 const (
 	minSleepPacer   = 10 * time.Millisecond
@@ -519,7 +522,7 @@ func (f *Fs) accessToken() (string, error) {
 
 // absPath converts root-relative remote to absolute home path
 func (f *Fs) absPath(remote string) string {
-	return "/" + path.Join(f.root, strings.Trim(remote, "/"))
+	return path.Join("/", f.root, remote)
 }
 
 // relPath converts absolute home path to root-relative remote
@@ -604,7 +607,7 @@ func (f *Fs) readItemMetaData(ctx context.Context, path string) (entry fs.DirEnt
 		Path:   "/api/m1/file",
 		Parameters: url.Values{
 			"access_token": {token},
-			"home":         {path},
+			"home":         {enc.FromStandardPath(path)},
 			"offset":       {"0"},
 			"limit":        {strconv.Itoa(maxInt32)},
 		},
@@ -639,7 +642,7 @@ func (f *Fs) readItemMetaData(ctx context.Context, path string) (entry fs.DirEnt
 //   =0 - for an empty directory
 //   >0 - for a non-empty directory
 func (f *Fs) itemToDirEntry(ctx context.Context, item *api.ListItem) (entry fs.DirEntry, dirSize int, err error) {
-	remote, err := f.relPath(item.Home)
+	remote, err := f.relPath(enc.ToStandardPath(item.Home))
 	if err != nil {
 		return nil, -1, err
 	}
@@ -705,7 +708,7 @@ func (f *Fs) listM1(ctx context.Context, dirPath string, offset int, limit int)
 	params.Set("limit", strconv.Itoa(limit))
 
 	data := url.Values{}
-	data.Set("home", dirPath)
+	data.Set("home", enc.FromStandardPath(dirPath))
 
 	opts := rest.Opts{
 		Method:      "POST",
@@ -753,7 +756,7 @@ func (f *Fs) listBin(ctx context.Context, dirPath string, depth int) (entries fs
 
 	req := api.NewBinWriter()
 	req.WritePu16(api.OperationFolderList)
-	req.WriteString(dirPath)
+	req.WriteString(enc.FromStandardPath(dirPath))
 	req.WritePu32(int64(depth))
 	req.WritePu32(int64(options))
 	req.WritePu32(0)
@@ -889,7 +892,7 @@ func (t *treeState) NextRecord() (fs.DirEntry, error) {
 	if (head & 4096) != 0 {
 		t.dunnoNodeID = r.ReadNBytes(api.DunnoNodeIDLength)
 	}
-	name := string(r.ReadBytesByLength())
+	name := enc.FromStandardPath(string(r.ReadBytesByLength()))
 	t.dunno1 = int(r.ReadULong())
 	t.dunno2 = 0
 	t.dunno3 = 0
@@ -1028,7 +1031,7 @@ func (f *Fs) CreateDir(ctx context.Context, path string) error {
 	req := api.NewBinWriter()
 	req.WritePu16(api.OperationCreateFolder)
 	req.WritePu16(0) // revision
-	req.WriteString(path)
+	req.WriteString(enc.FromStandardPath(path))
 	req.WritePu32(0)
 
 	token, err := f.accessToken()
@@ -1183,7 +1186,7 @@ func (f *Fs) delete(ctx context.Context, path string, hardDelete bool) error {
 		return err
 	}
 
-	data := url.Values{"home": {path}}
+	data := url.Values{"home": {enc.FromStandardPath(path)}}
 	opts := rest.Opts{
 		Method: "POST",
 		Path:   "/api/m1/file/remove",
@@ -1240,8 +1243,8 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	}
 
 	data := url.Values{}
-	data.Set("home", srcPath)
-	data.Set("folder", parentDir(dstPath))
+	data.Set("home", enc.FromStandardPath(srcPath))
+	data.Set("folder", enc.FromStandardPath(parentDir(dstPath)))
 	data.Set("email", f.opt.Username)
 	data.Set("x-email", f.opt.Username)
 
@@ -1279,7 +1282,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 		return nil, fmt.Errorf("copy failed with code %d", response.Status)
 	}
 
-	tmpPath := response.Body
+	tmpPath := enc.ToStandardPath(response.Body)
 	if tmpPath != dstPath {
 		fs.Debugf(f, "rename temporary file %q -> %q\n", tmpPath, dstPath)
 		err = f.moveItemBin(ctx, tmpPath, dstPath, "rename temporary file")
@@ -1354,9 +1357,9 @@ func (f *Fs) moveItemBin(ctx context.Context, srcPath, dstPath, opName string) e
 	req := api.NewBinWriter()
 	req.WritePu16(api.OperationRename)
 	req.WritePu32(0) // old revision
-	req.WriteString(srcPath)
+	req.WriteString(enc.FromStandardPath(srcPath))
 	req.WritePu32(0) // new revision
-	req.WriteString(dstPath)
+	req.WriteString(enc.FromStandardPath(dstPath))
 	req.WritePu32(0) // dunno
 
 	opts := rest.Opts{
@@ -1447,7 +1450,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err er
 	}
 
 	data := url.Values{}
-	data.Set("home", f.absPath(remote))
+	data.Set("home", enc.FromStandardPath(f.absPath(remote)))
 	data.Set("email", f.opt.Username)
 	data.Set("x-email", f.opt.Username)
 
@@ -2012,7 +2015,7 @@ func (o *Object) addFileMetaData(ctx context.Context, overwrite bool) error {
 	req := api.NewBinWriter()
 	req.WritePu16(api.OperationAddFile)
 	req.WritePu16(0) // revision
-	req.WriteString(o.absPath())
+	req.WriteString(enc.FromStandardPath(o.absPath()))
 	req.WritePu64(o.size)
 	req.WritePu64(o.modTime.Unix())
 	req.WritePu32(0)
@@ -2110,7 +2113,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
 	opts := rest.Opts{
 		Method:  "GET",
 		Options: options,
-		Path:    url.PathEscape(strings.TrimLeft(o.absPath(), "/")),
+		Path:    url.PathEscape(strings.TrimLeft(enc.FromStandardPath(o.absPath()), "/")),
 		Parameters: url.Values{
 			"client_id": {api.OAuthClientID},
 			"token":     {token},
diff --git a/docs/content/mailru.md b/docs/content/mailru.md
index 63e612a70..a90f4f23f 100644
--- a/docs/content/mailru.md
+++ b/docs/content/mailru.md
@@ -134,6 +134,25 @@ This command does not take any path arguments.
 To view your current quota you can use the `rclone about remote:`
 command which will display your usage limit (quota) and the current usage.
 
+#### Restricted filename characters
+
+In addition to the [default restricted characters set](/overview/#restricted-characters)
+the following characters are also replaced:
+
+| Character | Value | Replacement |
+| --------- |:-----:|:-----------:|
+| "         | 0x22  | "          |
+| *         | 0x2A  | *          |
+| :         | 0x3A  | :          |
+| <         | 0x3C  | <          |
+| >         | 0x3E  | >          |
+| ?         | 0x3F  | ?          |
+| \         | 0x5C  | \          |
+| \|        | 0x7C  | |          |
+
+Invalid UTF-8 bytes will also be [replaced](/overview/#invalid-utf8),
+as they can't be used in JSON strings.
+
 ### Limitations ###
 
 File size limits depend on your account. A single file size is limited by 2G
diff --git a/fs/encodings/encodings.go b/fs/encodings/encodings.go
index d28c70c03..6764bd3c1 100644
--- a/fs/encodings/encodings.go
+++ b/fs/encodings/encodings.go
@@ -133,6 +133,15 @@ const Koofr = encoder.MultiEncoder(
 		encoder.EncodeBackSlash |
 		encoder.EncodeInvalidUtf8)
 
+// Mailru is the encoding used by the mailru backend
+//
+// Encode invalid UTF-8 bytes as json doesn't handle them properly.
+const Mailru = encoder.MultiEncoder(
+	uint(Display) |
+		encoder.EncodeWin | // :?"*<>|
+		encoder.EncodeBackSlash |
+		encoder.EncodeInvalidUtf8)
+
 // Mega is the encoding used by the mega backend
 //
 // Encode invalid UTF-8 bytes as json doesn't handle them properly.
@@ -372,6 +381,8 @@ func ByName(name string) encoder.Encoder {
 		return LocalUnix
 	case "local-macos", "macos":
 		return LocalMacOS
+	case "mailru":
+		return Mailru
 	case "mega":
 		return Mega
 	case "onedrive":
diff --git a/fs/encodings/encodings_noencode.go b/fs/encodings/encodings_noencode.go
index 6f29e5966..e89a4dfaf 100644
--- a/fs/encodings/encodings_noencode.go
+++ b/fs/encodings/encodings_noencode.go
@@ -25,6 +25,7 @@ const (
 	GoogleCloudStorage = Base
 	JottaCloud         = Base
 	Koofr              = Base
+	Mailru             = Base
 	Mega               = Base
 	OneDrive           = Base
 	OpenDrive          = Base